Skip to main content

azul_css/
compact_cache.rs

1//! Compact layout property cache — three-tier numeric encoding
2//!
3//! Replaces BTreeMap-based CSS property lookups with cache-friendly arrays.
4//!
5//! - **Tier 1**: `Vec<u64>` — ALL 21 enum properties bitpacked (8 B/node)
6//! - **Tier 2 hot**: `Vec<CompactNodeProps>` — layout-critical numeric dimensions (68 B/node)
7//! - **Tier 2 cold**: `Vec<CompactNodePropsCold>` — paint-only properties (28 B/node)
8//! - **Tier 2b**: `Vec<CompactTextProps>` — text/IFC properties (24 B/node)
9//!
10//! Non-compact properties (background, box-shadow, transform, etc.) are
11//! resolved via the slow cascade path in `CssPropertyCache::get_property_slow()`.
12
13// The `*_from_u8` decoders below intentionally give an explicit arm for the byte
14// that maps to each enum's default (e.g. `0 => Block`) even though the `_`
15// catch-all returns the same default — this keeps the decode table a 1:1 mirror
16// of the `*_to_u8` encoders. clippy::match_same_arms flags those explicit arms as
17// duplicates of `_`; merging them would drop the encoding documentation, so allow
18// it for this codec module (false positive for the intent here).
19#![allow(clippy::match_same_arms)]
20
21use crate::props::basic::length::{FloatValue, SizeMetric};
22use crate::props::basic::pixel::PixelValue;
23use crate::props::layout::{
24    display::LayoutDisplay,
25    dimensions::{LayoutHeight, LayoutWidth, LayoutMaxHeight, LayoutMaxWidth, LayoutMinHeight, LayoutMinWidth},
26    flex::{
27        LayoutAlignContent, LayoutAlignItems, LayoutAlignSelf, LayoutFlexDirection, LayoutFlexWrap,
28        LayoutJustifyContent,
29    },
30    grid::{LayoutGridAutoFlow, LayoutJustifySelf, LayoutJustifyItems},
31    overflow::LayoutOverflow,
32    position::LayoutPosition,
33    wrapping::{LayoutClear, LayoutWritingMode},
34    table::StyleBorderCollapse,
35};
36use crate::props::layout::display::LayoutFloat;
37use crate::props::layout::dimensions::LayoutBoxSizing;
38use crate::props::basic::font::{StyleFontStyle, StyleFontWeight};
39use crate::props::basic::color::ColorU;
40use crate::props::style::{StyleTextAlign, StyleVerticalAlign, StyleVisibility, StyleWhiteSpace, StyleDirection};
41use crate::props::style::border::BorderStyle;
42use crate::props::property::{CssProperty, CssPropertyType};
43use crate::css::CssPropertyValue;
44use alloc::boxed::Box;
45use alloc::vec::Vec;
46
47// =============================================================================
48// Sentinel Constants
49// =============================================================================
50
51/// u16 sentinel values (for resolved-px ×10 and flex ×100)
52pub const U16_SENTINEL: u16 = 0xFFFF;
53/// Any u16 value >= this threshold is a sentinel, not a real value
54pub const U16_SENTINEL_THRESHOLD: u16 = 0xFFF9;
55
56/// i16 sentinel values (for signed resolved-px ×10)
57pub const I16_SENTINEL: i16 = 0x7FFF;       // 32767
58pub const I16_AUTO: i16 = 0x7FFE;           // 32766
59pub const I16_INHERIT: i16 = 0x7FFD;        // 32765
60pub const I16_INITIAL: i16 = 0x7FFC;        // 32764
61/// Any i16 value >= this threshold is a sentinel
62pub const I16_SENTINEL_THRESHOLD: i16 = 0x7FFC; // 32764
63
64/// u32 sentinel values (for dimension properties with unit info)
65pub const U32_SENTINEL: u32 = 0xFFFF_FFFF;
66pub const U32_AUTO: u32 = 0xFFFF_FFFE;
67pub const U32_NONE: u32 = 0xFFFF_FFFD;
68pub const U32_INHERIT: u32 = 0xFFFF_FFFC;
69pub const U32_INITIAL: u32 = 0xFFFF_FFFB;
70pub const U32_MIN_CONTENT: u32 = 0xFFFF_FFFA;
71pub const U32_MAX_CONTENT: u32 = 0xFFFF_FFF9;
72/// Any u32 value >= this threshold is a sentinel
73pub const U32_SENTINEL_THRESHOLD: u32 = 0xFFFF_FFF9;
74
75// =============================================================================
76// Tier 1: u64 bitfield — ALL enum properties
77// =============================================================================
78//
79// Bit layout (52 bits used, 12 spare):
80//   [4:0]    display          5 bits  (22 variants)
81//   [7:5]    position         3 bits  (5 variants)
82//   [9:8]    float            2 bits  (3 variants)
83//   [12:10]  overflow_x       3 bits  (5 variants)
84//   [15:13]  overflow_y       3 bits  (5 variants)
85//   [16]     box_sizing       1 bit   (2 variants)
86//   [18:17]  flex_direction   2 bits  (4 variants)
87//   [20:19]  flex_wrap        2 bits  (3 variants)
88//   [23:21]  justify_content  3 bits  (8 variants)
89//   [26:24]  align_items      3 bits  (5 variants)
90//   [29:27]  align_content    3 bits  (6 variants)
91//   [31:30]  writing_mode     2 bits  (3 variants)
92//   [33:32]  clear            2 bits  (4 variants)
93//   [37:34]  font_weight      4 bits  (11 variants)
94//   [39:38]  font_style       2 bits  (3 variants)
95//   [42:40]  text_align       3 bits  (6 variants)
96//   [44:43]  visibility       2 bits  (3 variants)
97//   [47:45]  white_space      3 bits  (6 variants)
98//   [48]     direction        1 bit   (2 variants)
99//   [51:49]  vertical_align   3 bits  (8 variants)
100//   [52]     border_collapse  1 bit   (2 variants)
101//   [63:53]  (spare / sentinel flags)
102
103// Bit offsets within u64
104pub const DISPLAY_SHIFT: u32 = 0;
105pub const POSITION_SHIFT: u32 = 5;
106pub const FLOAT_SHIFT: u32 = 8;
107pub const OVERFLOW_X_SHIFT: u32 = 10;
108pub const OVERFLOW_Y_SHIFT: u32 = 13;
109pub const BOX_SIZING_SHIFT: u32 = 16;
110pub const FLEX_DIRECTION_SHIFT: u32 = 17;
111pub const FLEX_WRAP_SHIFT: u32 = 19;
112pub const JUSTIFY_CONTENT_SHIFT: u32 = 21;
113pub const ALIGN_ITEMS_SHIFT: u32 = 24;
114pub const ALIGN_CONTENT_SHIFT: u32 = 27;
115pub const WRITING_MODE_SHIFT: u32 = 30;
116pub const CLEAR_SHIFT: u32 = 32;
117pub const FONT_WEIGHT_SHIFT: u32 = 34;
118pub const FONT_STYLE_SHIFT: u32 = 38;
119pub const TEXT_ALIGN_SHIFT: u32 = 40;
120pub const VISIBILITY_SHIFT: u32 = 43;
121pub const WHITE_SPACE_SHIFT: u32 = 45;
122pub const DIRECTION_SHIFT: u32 = 48;
123pub const VERTICAL_ALIGN_SHIFT: u32 = 49;
124pub const BORDER_COLLAPSE_SHIFT: u32 = 52;
125
126// Bit masks
127pub const DISPLAY_MASK: u64 = 0x1F;     // 5 bits
128pub const POSITION_MASK: u64 = 0x07;    // 3 bits
129pub const FLOAT_MASK: u64 = 0x03;       // 2 bits
130pub const OVERFLOW_MASK: u64 = 0x07;    // 3 bits
131pub const BOX_SIZING_MASK: u64 = 0x01;  // 1 bit
132pub const FLEX_DIR_MASK: u64 = 0x03;    // 2 bits
133pub const FLEX_WRAP_MASK: u64 = 0x03;   // 2 bits
134pub const JUSTIFY_MASK: u64 = 0x07;     // 3 bits
135pub const ALIGN_MASK: u64 = 0x07;       // 3 bits
136pub const WRITING_MODE_MASK: u64 = 0x03;// 2 bits
137pub const CLEAR_MASK: u64 = 0x03;       // 2 bits
138pub const FONT_WEIGHT_MASK: u64 = 0x0F; // 4 bits
139pub const FONT_STYLE_MASK: u64 = 0x03;  // 2 bits
140pub const TEXT_ALIGN_MASK: u64 = 0x07;  // 3 bits
141pub const VISIBILITY_MASK: u64 = 0x03;  // 2 bits
142pub const WHITE_SPACE_MASK: u64 = 0x07; // 3 bits
143pub const DIRECTION_MASK: u64 = 0x01;   // 1 bit
144pub const VERTICAL_ALIGN_MASK: u64 = 0x07; // 3 bits
145pub const BORDER_COLLAPSE_MASK: u64 = 0x01; // 1 bit
146
147pub const ALIGN_SELF_SHIFT: u32 = 53;
148pub const JUSTIFY_SELF_SHIFT: u32 = 56;
149pub const GRID_AUTO_FLOW_SHIFT: u32 = 59;
150pub const JUSTIFY_ITEMS_SHIFT: u32 = 61;
151pub const ALIGN_SELF_MASK: u64 = 0x07;     // 3 bits
152pub const JUSTIFY_SELF_MASK: u64 = 0x07;   // 3 bits
153pub const GRID_AUTO_FLOW_MASK: u64 = 0x03; // 2 bits (row/col × dense)
154pub const JUSTIFY_ITEMS_MASK: u64 = 0x03;  // 2 bits (start/center/end/stretch)
155
156/// Special value stored in the spare bits [63:51] to indicate this node has
157/// NO tier-1 data (i.e., all defaults).
158///
159/// 0 is a valid all-defaults encoding,
160/// so we use bit 63 as a "tier1 populated" flag. If bit 63 is 0 and all other
161/// bits are 0, it means "all defaults" (`Display::Block`, `Position::Static`, etc.).
162/// We set bit 63 = 1 to mark that the node HAS been populated.
163pub const TIER1_POPULATED_BIT: u64 = 1 << 63;
164
165// =============================================================================
166// Safe from_u8 conversion functions (no transmute!)
167// =============================================================================
168
169/// Decode display from u8. **0 = Block** (most common HTML default).
170/// Value 31 (0x1F) = sentinel: look up in slow path for uncommon values.
171/// Returns default (Block) on invalid input.
172#[inline]
173#[must_use] pub const fn layout_display_from_u8(v: u8) -> LayoutDisplay {
174    match v {
175        0 => LayoutDisplay::Block,        // default when bits are 0
176        1 => LayoutDisplay::Inline,
177        2 => LayoutDisplay::InlineBlock,
178        3 => LayoutDisplay::Flex,
179        4 => LayoutDisplay::None,
180        5 => LayoutDisplay::InlineFlex,
181        6 => LayoutDisplay::Table,
182        7 => LayoutDisplay::InlineTable,
183        8 => LayoutDisplay::TableRowGroup,
184        9 => LayoutDisplay::TableHeaderGroup,
185        10 => LayoutDisplay::TableFooterGroup,
186        11 => LayoutDisplay::TableRow,
187        12 => LayoutDisplay::TableColumnGroup,
188        13 => LayoutDisplay::TableColumn,
189        14 => LayoutDisplay::TableCell,
190        15 => LayoutDisplay::TableCaption,
191        16 => LayoutDisplay::FlowRoot,
192        17 => LayoutDisplay::ListItem,
193        18 => LayoutDisplay::RunIn,
194        19 => LayoutDisplay::Marker,
195        20 => LayoutDisplay::Grid,
196        21 => LayoutDisplay::InlineGrid,
197        22 => LayoutDisplay::Contents,
198        _ => LayoutDisplay::Block, // fallback + sentinel (31)
199    }
200}
201
202/// Encode display to u8. **0 = Block** (most common HTML default).
203#[inline]
204#[must_use] pub const fn layout_display_to_u8(v: LayoutDisplay) -> u8 {
205    match v {
206        LayoutDisplay::Block => 0,         // 0 = default when bits unset
207        LayoutDisplay::Inline => 1,
208        LayoutDisplay::InlineBlock => 2,
209        LayoutDisplay::Flex => 3,
210        LayoutDisplay::None => 4,
211        LayoutDisplay::InlineFlex => 5,
212        LayoutDisplay::Table => 6,
213        LayoutDisplay::InlineTable => 7,
214        LayoutDisplay::TableRowGroup => 8,
215        LayoutDisplay::TableHeaderGroup => 9,
216        LayoutDisplay::TableFooterGroup => 10,
217        LayoutDisplay::TableRow => 11,
218        LayoutDisplay::TableColumnGroup => 12,
219        LayoutDisplay::TableColumn => 13,
220        LayoutDisplay::TableCell => 14,
221        LayoutDisplay::TableCaption => 15,
222        LayoutDisplay::FlowRoot => 16,
223        LayoutDisplay::ListItem => 17,
224        LayoutDisplay::RunIn => 18,
225        LayoutDisplay::Marker => 19,
226        LayoutDisplay::Grid => 20,
227        LayoutDisplay::InlineGrid => 21,
228        LayoutDisplay::Contents => 22,
229    }
230}
231
232#[inline]
233#[must_use] pub const fn layout_position_from_u8(v: u8) -> LayoutPosition {
234    match v {
235        0 => LayoutPosition::Static,
236        1 => LayoutPosition::Relative,
237        2 => LayoutPosition::Absolute,
238        3 => LayoutPosition::Fixed,
239        4 => LayoutPosition::Sticky,
240        _ => LayoutPosition::Static,
241    }
242}
243
244#[inline]
245#[must_use] pub const fn layout_position_to_u8(v: LayoutPosition) -> u8 {
246    match v {
247        LayoutPosition::Static => 0,
248        LayoutPosition::Relative => 1,
249        LayoutPosition::Absolute => 2,
250        LayoutPosition::Fixed => 3,
251        LayoutPosition::Sticky => 4,
252    }
253}
254
255/// Decode float from u8. **0 = None** (CSS initial value).
256#[inline]
257#[must_use] pub const fn layout_float_from_u8(v: u8) -> LayoutFloat {
258    match v {
259        0 => LayoutFloat::None,            // default when bits unset
260        1 => LayoutFloat::Left,
261        2 => LayoutFloat::Right,
262        _ => LayoutFloat::None,
263    }
264}
265
266/// Encode float to u8. **0 = None** (CSS initial value).
267#[inline]
268#[must_use] pub const fn layout_float_to_u8(v: LayoutFloat) -> u8 {
269    match v {
270        LayoutFloat::None => 0,
271        LayoutFloat::Left => 1,
272        LayoutFloat::Right => 2,
273    }
274}
275
276/// Decode overflow from u8. **0 = Visible** (CSS initial value).
277#[inline]
278#[must_use] pub const fn layout_overflow_from_u8(v: u8) -> LayoutOverflow {
279    match v {
280        0 => LayoutOverflow::Visible,      // default when bits unset
281        1 => LayoutOverflow::Hidden,
282        2 => LayoutOverflow::Scroll,
283        3 => LayoutOverflow::Auto,
284        4 => LayoutOverflow::Clip,
285        _ => LayoutOverflow::Visible,
286    }
287}
288
289/// Encode overflow to u8. **0 = Visible** (CSS initial value).
290#[inline]
291#[must_use] pub const fn layout_overflow_to_u8(v: LayoutOverflow) -> u8 {
292    match v {
293        LayoutOverflow::Visible => 0,      // 0 = default when bits unset
294        LayoutOverflow::Hidden => 1,
295        LayoutOverflow::Scroll => 2,
296        LayoutOverflow::Auto => 3,
297        LayoutOverflow::Clip => 4,
298    }
299}
300
301#[inline]
302#[must_use] pub const fn layout_box_sizing_from_u8(v: u8) -> LayoutBoxSizing {
303    match v {
304        0 => LayoutBoxSizing::ContentBox,
305        1 => LayoutBoxSizing::BorderBox,
306        _ => LayoutBoxSizing::ContentBox,
307    }
308}
309
310#[inline]
311#[must_use] pub const fn layout_box_sizing_to_u8(v: LayoutBoxSizing) -> u8 {
312    match v {
313        LayoutBoxSizing::ContentBox => 0,
314        LayoutBoxSizing::BorderBox => 1,
315    }
316}
317
318#[inline]
319#[must_use] pub const fn layout_flex_direction_from_u8(v: u8) -> LayoutFlexDirection {
320    match v {
321        0 => LayoutFlexDirection::Row,
322        1 => LayoutFlexDirection::RowReverse,
323        2 => LayoutFlexDirection::Column,
324        3 => LayoutFlexDirection::ColumnReverse,
325        _ => LayoutFlexDirection::Row,
326    }
327}
328
329#[inline]
330#[must_use] pub const fn layout_flex_direction_to_u8(v: LayoutFlexDirection) -> u8 {
331    match v {
332        LayoutFlexDirection::Row => 0,
333        LayoutFlexDirection::RowReverse => 1,
334        LayoutFlexDirection::Column => 2,
335        LayoutFlexDirection::ColumnReverse => 3,
336    }
337}
338
339/// 0 = `NoWrap` (CSS initial value for flex-wrap)
340#[inline]
341#[must_use] pub const fn layout_flex_wrap_from_u8(v: u8) -> LayoutFlexWrap {
342    match v {
343        0 => LayoutFlexWrap::NoWrap,       // CSS initial
344        1 => LayoutFlexWrap::Wrap,
345        2 => LayoutFlexWrap::WrapReverse,
346        _ => LayoutFlexWrap::NoWrap,
347    }
348}
349
350#[inline]
351#[must_use] pub const fn layout_flex_wrap_to_u8(v: LayoutFlexWrap) -> u8 {
352    match v {
353        LayoutFlexWrap::NoWrap => 0,
354        LayoutFlexWrap::Wrap => 1,
355        LayoutFlexWrap::WrapReverse => 2,
356    }
357}
358
359#[inline]
360#[must_use] pub const fn layout_justify_content_from_u8(v: u8) -> LayoutJustifyContent {
361    match v {
362        0 => LayoutJustifyContent::FlexStart,
363        1 => LayoutJustifyContent::FlexEnd,
364        2 => LayoutJustifyContent::Start,
365        3 => LayoutJustifyContent::End,
366        4 => LayoutJustifyContent::Center,
367        5 => LayoutJustifyContent::SpaceBetween,
368        6 => LayoutJustifyContent::SpaceAround,
369        7 => LayoutJustifyContent::SpaceEvenly,
370        _ => LayoutJustifyContent::FlexStart,
371    }
372}
373
374#[inline]
375#[must_use] pub const fn layout_justify_content_to_u8(v: LayoutJustifyContent) -> u8 {
376    match v {
377        LayoutJustifyContent::FlexStart => 0,
378        LayoutJustifyContent::FlexEnd => 1,
379        LayoutJustifyContent::Start => 2,
380        LayoutJustifyContent::End => 3,
381        LayoutJustifyContent::Center => 4,
382        LayoutJustifyContent::SpaceBetween => 5,
383        LayoutJustifyContent::SpaceAround => 6,
384        LayoutJustifyContent::SpaceEvenly => 7,
385    }
386}
387
388#[inline]
389#[must_use] pub const fn layout_align_items_from_u8(v: u8) -> LayoutAlignItems {
390    match v {
391        0 => LayoutAlignItems::Stretch,
392        1 => LayoutAlignItems::Center,
393        2 => LayoutAlignItems::Start,
394        3 => LayoutAlignItems::End,
395        4 => LayoutAlignItems::Baseline,
396        _ => LayoutAlignItems::Stretch,
397    }
398}
399
400#[inline]
401#[must_use] pub const fn layout_align_items_to_u8(v: LayoutAlignItems) -> u8 {
402    match v {
403        LayoutAlignItems::Stretch => 0,
404        LayoutAlignItems::Center => 1,
405        LayoutAlignItems::Start => 2,
406        LayoutAlignItems::End => 3,
407        LayoutAlignItems::Baseline => 4,
408    }
409}
410
411#[inline]
412#[must_use] pub const fn layout_align_self_to_u8(v: LayoutAlignSelf) -> u8 {
413    match v {
414        LayoutAlignSelf::Auto => 0,
415        LayoutAlignSelf::Stretch => 1,
416        LayoutAlignSelf::Center => 2,
417        LayoutAlignSelf::Start => 3,
418        LayoutAlignSelf::End => 4,
419        LayoutAlignSelf::Baseline => 5,
420    }
421}
422
423#[inline]
424#[must_use] pub const fn layout_align_self_from_u8(v: u8) -> LayoutAlignSelf {
425    match v {
426        0 => LayoutAlignSelf::Auto,
427        1 => LayoutAlignSelf::Stretch,
428        2 => LayoutAlignSelf::Center,
429        3 => LayoutAlignSelf::Start,
430        4 => LayoutAlignSelf::End,
431        5 => LayoutAlignSelf::Baseline,
432        _ => LayoutAlignSelf::Auto,
433    }
434}
435
436#[inline]
437#[must_use] pub const fn layout_justify_self_to_u8(v: LayoutJustifySelf) -> u8 {
438    match v {
439        LayoutJustifySelf::Auto => 0,
440        LayoutJustifySelf::Start => 1,
441        LayoutJustifySelf::End => 2,
442        LayoutJustifySelf::Center => 3,
443        LayoutJustifySelf::Stretch => 4,
444    }
445}
446
447#[inline]
448#[must_use] pub const fn layout_justify_self_from_u8(v: u8) -> LayoutJustifySelf {
449    match v {
450        0 => LayoutJustifySelf::Auto,
451        1 => LayoutJustifySelf::Start,
452        2 => LayoutJustifySelf::End,
453        3 => LayoutJustifySelf::Center,
454        4 => LayoutJustifySelf::Stretch,
455        _ => LayoutJustifySelf::Auto,
456    }
457}
458
459// Tier1 uses 0 as the "unset" sentinel for every enum. For justify-items
460// the CSS default is `normal` which behaves as `stretch` on grid items,
461// so 0 must decode to Stretch (not Start). Getting this wrong leaves
462// every unset grid container reporting justify-items: Start, which
463// forces taffy to content-size items instead of stretching them across
464// their column tracks — exactly the calc.c regression.
465#[inline]
466#[must_use] pub const fn layout_justify_items_to_u8(v: LayoutJustifyItems) -> u8 {
467    match v {
468        LayoutJustifyItems::Stretch => 0,
469        LayoutJustifyItems::Start => 1,
470        LayoutJustifyItems::End => 2,
471        LayoutJustifyItems::Center => 3,
472    }
473}
474
475#[inline]
476#[must_use] pub const fn layout_justify_items_from_u8(v: u8) -> LayoutJustifyItems {
477    match v {
478        0 => LayoutJustifyItems::Stretch,
479        1 => LayoutJustifyItems::Start,
480        2 => LayoutJustifyItems::End,
481        3 => LayoutJustifyItems::Center,
482        _ => LayoutJustifyItems::Stretch,
483    }
484}
485
486#[inline]
487#[must_use] pub const fn layout_grid_auto_flow_to_u8(v: LayoutGridAutoFlow) -> u8 {
488    match v {
489        LayoutGridAutoFlow::Row => 0,
490        LayoutGridAutoFlow::Column => 1,
491        LayoutGridAutoFlow::RowDense => 2,
492        LayoutGridAutoFlow::ColumnDense => 3,
493    }
494}
495
496#[inline]
497#[must_use] pub const fn layout_grid_auto_flow_from_u8(v: u8) -> LayoutGridAutoFlow {
498    match v {
499        0 => LayoutGridAutoFlow::Row,
500        1 => LayoutGridAutoFlow::Column,
501        2 => LayoutGridAutoFlow::RowDense,
502        3 => LayoutGridAutoFlow::ColumnDense,
503        _ => LayoutGridAutoFlow::Row,
504    }
505}
506
507#[inline]
508#[must_use] pub const fn layout_align_content_from_u8(v: u8) -> LayoutAlignContent {
509    match v {
510        0 => LayoutAlignContent::Stretch,
511        1 => LayoutAlignContent::Center,
512        2 => LayoutAlignContent::Start,
513        3 => LayoutAlignContent::End,
514        4 => LayoutAlignContent::SpaceBetween,
515        5 => LayoutAlignContent::SpaceAround,
516        _ => LayoutAlignContent::Stretch,
517    }
518}
519
520#[inline]
521#[must_use] pub const fn layout_align_content_to_u8(v: LayoutAlignContent) -> u8 {
522    match v {
523        LayoutAlignContent::Stretch => 0,
524        LayoutAlignContent::Center => 1,
525        LayoutAlignContent::Start => 2,
526        LayoutAlignContent::End => 3,
527        LayoutAlignContent::SpaceBetween => 4,
528        LayoutAlignContent::SpaceAround => 5,
529    }
530}
531
532#[inline]
533#[must_use] pub const fn layout_writing_mode_from_u8(v: u8) -> LayoutWritingMode {
534    match v {
535        0 => LayoutWritingMode::HorizontalTb,
536        1 => LayoutWritingMode::VerticalRl,
537        2 => LayoutWritingMode::VerticalLr,
538        _ => LayoutWritingMode::HorizontalTb,
539    }
540}
541
542#[inline]
543#[must_use] pub const fn layout_writing_mode_to_u8(v: LayoutWritingMode) -> u8 {
544    match v {
545        LayoutWritingMode::HorizontalTb => 0,
546        LayoutWritingMode::VerticalRl => 1,
547        LayoutWritingMode::VerticalLr => 2,
548    }
549}
550
551#[inline]
552#[must_use] pub const fn layout_clear_from_u8(v: u8) -> LayoutClear {
553    match v {
554        0 => LayoutClear::None,
555        1 => LayoutClear::Left,
556        2 => LayoutClear::Right,
557        3 => LayoutClear::Both,
558        _ => LayoutClear::None,
559    }
560}
561
562#[inline]
563#[must_use] pub const fn layout_clear_to_u8(v: LayoutClear) -> u8 {
564    match v {
565        LayoutClear::None => 0,
566        LayoutClear::Left => 1,
567        LayoutClear::Right => 2,
568        LayoutClear::Both => 3,
569    }
570}
571
572#[inline]
573/// 0 = Normal/400 (CSS initial value for font-weight)
574#[must_use] pub const fn style_font_weight_from_u8(v: u8) -> StyleFontWeight {
575    match v {
576        0 => StyleFontWeight::Normal,     // CSS initial (400)
577        1 => StyleFontWeight::W100,
578        2 => StyleFontWeight::W200,
579        3 => StyleFontWeight::W300,
580        4 => StyleFontWeight::W500,
581        5 => StyleFontWeight::W600,
582        6 => StyleFontWeight::Bold,       // 700
583        7 => StyleFontWeight::W800,
584        8 => StyleFontWeight::W900,
585        9 => StyleFontWeight::Lighter,
586        10 => StyleFontWeight::Bolder,
587        _ => StyleFontWeight::Normal,
588    }
589}
590
591#[inline]
592/// 0 = Normal/400 (CSS initial value for font-weight)
593#[must_use] pub const fn style_font_weight_to_u8(v: StyleFontWeight) -> u8 {
594    match v {
595        StyleFontWeight::Normal => 0,     // CSS initial (400)
596        StyleFontWeight::W100 => 1,
597        StyleFontWeight::W200 => 2,
598        StyleFontWeight::W300 => 3,
599        StyleFontWeight::W500 => 4,
600        StyleFontWeight::W600 => 5,
601        StyleFontWeight::Bold => 6,       // 700
602        StyleFontWeight::W800 => 7,
603        StyleFontWeight::W900 => 8,
604        StyleFontWeight::Lighter => 9,
605        StyleFontWeight::Bolder => 10,
606    }
607}
608
609#[inline]
610#[must_use] pub const fn style_font_style_from_u8(v: u8) -> StyleFontStyle {
611    match v {
612        0 => StyleFontStyle::Normal,
613        1 => StyleFontStyle::Italic,
614        2 => StyleFontStyle::Oblique,
615        _ => StyleFontStyle::Normal,
616    }
617}
618
619#[inline]
620#[must_use] pub const fn style_font_style_to_u8(v: StyleFontStyle) -> u8 {
621    match v {
622        StyleFontStyle::Normal => 0,
623        StyleFontStyle::Italic => 1,
624        StyleFontStyle::Oblique => 2,
625    }
626}
627
628#[inline]
629#[must_use] pub const fn style_text_align_from_u8(v: u8) -> StyleTextAlign {
630    // Code 0 must decode to the CSS-initial value so an un-written tier1 field (all
631    // zero) yields `start`, not physical `left` — otherwise direction:rtl never
632    // right-aligns (start→right needs the value to actually be `start`). `left`
633    // therefore takes code 4 (swapped with `start`).
634    match v {
635        0 => StyleTextAlign::Start,
636        1 => StyleTextAlign::Center,
637        2 => StyleTextAlign::Right,
638        3 => StyleTextAlign::Justify,
639        4 => StyleTextAlign::Left,
640        5 => StyleTextAlign::End,
641        _ => StyleTextAlign::Start,
642    }
643}
644
645#[inline]
646#[must_use] pub const fn style_text_align_to_u8(v: StyleTextAlign) -> u8 {
647    // `start` encodes to 0 (the CSS-initial / zero-baseline code); `left` takes 4.
648    match v {
649        StyleTextAlign::Start => 0,
650        StyleTextAlign::Center => 1,
651        StyleTextAlign::Right => 2,
652        StyleTextAlign::Justify => 3,
653        StyleTextAlign::Left => 4,
654        StyleTextAlign::End => 5,
655    }
656}
657
658#[inline]
659#[must_use] pub const fn style_visibility_from_u8(v: u8) -> StyleVisibility {
660    match v {
661        0 => StyleVisibility::Visible,
662        1 => StyleVisibility::Hidden,
663        2 => StyleVisibility::Collapse,
664        _ => StyleVisibility::Visible,
665    }
666}
667
668#[inline]
669#[must_use] pub const fn style_visibility_to_u8(v: StyleVisibility) -> u8 {
670    match v {
671        StyleVisibility::Visible => 0,
672        StyleVisibility::Hidden => 1,
673        StyleVisibility::Collapse => 2,
674    }
675}
676
677#[inline]
678#[must_use] pub const fn style_white_space_from_u8(v: u8) -> StyleWhiteSpace {
679    match v {
680        0 => StyleWhiteSpace::Normal,
681        1 => StyleWhiteSpace::Pre,
682        2 => StyleWhiteSpace::Nowrap,
683        3 => StyleWhiteSpace::PreWrap,
684        4 => StyleWhiteSpace::PreLine,
685        5 => StyleWhiteSpace::BreakSpaces,
686        _ => StyleWhiteSpace::Normal,
687    }
688}
689
690#[inline]
691#[must_use] pub const fn style_white_space_to_u8(v: StyleWhiteSpace) -> u8 {
692    match v {
693        StyleWhiteSpace::Normal => 0,
694        StyleWhiteSpace::Pre => 1,
695        StyleWhiteSpace::Nowrap => 2,
696        StyleWhiteSpace::PreWrap => 3,
697        StyleWhiteSpace::PreLine => 4,
698        StyleWhiteSpace::BreakSpaces => 5,
699    }
700}
701
702#[inline]
703#[must_use] pub const fn style_direction_from_u8(v: u8) -> StyleDirection {
704    match v {
705        0 => StyleDirection::Ltr,
706        1 => StyleDirection::Rtl,
707        _ => StyleDirection::Ltr,
708    }
709}
710
711#[inline]
712#[must_use] pub const fn style_direction_to_u8(v: StyleDirection) -> u8 {
713    match v {
714        StyleDirection::Ltr => 0,
715        StyleDirection::Rtl => 1,
716    }
717}
718
719#[inline]
720#[must_use] pub const fn style_vertical_align_from_u8(v: u8) -> StyleVerticalAlign {
721    match v {
722        0 => StyleVerticalAlign::Baseline,
723        1 => StyleVerticalAlign::Top,
724        2 => StyleVerticalAlign::Middle,
725        3 => StyleVerticalAlign::Bottom,
726        4 => StyleVerticalAlign::Sub,
727        5 => StyleVerticalAlign::Superscript,
728        6 => StyleVerticalAlign::TextTop,
729        7 => StyleVerticalAlign::TextBottom,
730        _ => StyleVerticalAlign::Baseline,
731    }
732}
733
734#[inline]
735#[must_use] pub const fn style_vertical_align_to_u8(v: StyleVerticalAlign) -> u8 {
736    match v {
737        StyleVerticalAlign::Baseline => 0,
738        StyleVerticalAlign::Top => 1,
739        StyleVerticalAlign::Middle => 2,
740        StyleVerticalAlign::Bottom => 3,
741        StyleVerticalAlign::Sub => 4,
742        StyleVerticalAlign::Superscript => 5,
743        StyleVerticalAlign::TextTop => 6,
744        StyleVerticalAlign::TextBottom => 7,
745        // Percentage/Length cannot be stored in the 3-bit compact cache field;
746        // fall back to 0 (Baseline). Callers must use the slow cascade path
747        // for vertical-align values with length/percentage units.
748        StyleVerticalAlign::Percentage(_) | StyleVerticalAlign::Length(_) => 0,
749    }
750}
751
752#[inline]
753#[must_use] pub const fn border_collapse_from_u8(v: u8) -> StyleBorderCollapse {
754    match v {
755        0 => StyleBorderCollapse::Separate,
756        1 => StyleBorderCollapse::Collapse,
757        _ => StyleBorderCollapse::Separate,
758    }
759}
760
761#[inline]
762#[must_use] pub const fn border_collapse_to_u8(v: StyleBorderCollapse) -> u8 {
763    match v {
764        StyleBorderCollapse::Separate => 0,
765        StyleBorderCollapse::Collapse => 1,
766    }
767}
768
769#[inline]
770#[must_use] pub const fn border_style_from_u8(v: u8) -> BorderStyle {
771    match v {
772        0 => BorderStyle::None,
773        1 => BorderStyle::Solid,
774        2 => BorderStyle::Double,
775        3 => BorderStyle::Dotted,
776        4 => BorderStyle::Dashed,
777        5 => BorderStyle::Hidden,
778        6 => BorderStyle::Groove,
779        7 => BorderStyle::Ridge,
780        8 => BorderStyle::Inset,
781        9 => BorderStyle::Outset,
782        _ => BorderStyle::None,
783    }
784}
785
786#[inline]
787#[must_use] pub const fn border_style_to_u8(v: BorderStyle) -> u8 {
788    match v {
789        BorderStyle::None => 0,
790        BorderStyle::Solid => 1,
791        BorderStyle::Double => 2,
792        BorderStyle::Dotted => 3,
793        BorderStyle::Dashed => 4,
794        BorderStyle::Hidden => 5,
795        BorderStyle::Groove => 6,
796        BorderStyle::Ridge => 7,
797        BorderStyle::Inset => 8,
798        BorderStyle::Outset => 9,
799    }
800}
801
802/// Encode 4 border styles into a u16: [3:0]=top, [7:4]=right, [11:8]=bottom, [15:12]=left
803#[inline]
804#[must_use] pub const fn encode_border_styles_packed(top: BorderStyle, right: BorderStyle, bottom: BorderStyle, left: BorderStyle) -> u16 {
805    (border_style_to_u8(top) as u16)
806        | ((border_style_to_u8(right) as u16) << 4)
807        | ((border_style_to_u8(bottom) as u16) << 8)
808        | ((border_style_to_u8(left) as u16) << 12)
809}
810
811/// Decode border-top-style from packed u16
812#[inline]
813#[must_use] pub const fn decode_border_top_style(packed: u16) -> BorderStyle {
814    border_style_from_u8((packed & 0x0F) as u8)
815}
816
817/// Decode border-right-style from packed u16
818#[inline]
819#[must_use] pub const fn decode_border_right_style(packed: u16) -> BorderStyle {
820    border_style_from_u8(((packed >> 4) & 0x0F) as u8)
821}
822
823/// Decode border-bottom-style from packed u16
824#[inline]
825#[must_use] pub const fn decode_border_bottom_style(packed: u16) -> BorderStyle {
826    border_style_from_u8(((packed >> 8) & 0x0F) as u8)
827}
828
829/// Decode border-left-style from packed u16
830#[inline]
831#[must_use] pub const fn decode_border_left_style(packed: u16) -> BorderStyle {
832    border_style_from_u8(((packed >> 12) & 0x0F) as u8)
833}
834
835/// Encode a `ColorU` as u32 (0xRRGGBBAA). Returns 0 for sentinel/unset.
836#[inline]
837#[must_use] pub const fn encode_color_u32(c: &ColorU) -> u32 {
838    ((c.r as u32) << 24) | ((c.g as u32) << 16) | ((c.b as u32) << 8) | (c.a as u32)
839}
840
841/// Decode a u32 back to `ColorU`. Returns `None` if sentinel (`0x00000000`).
842///
843/// **Limitation:** `rgba(0,0,0,0)` (fully transparent black) also encodes as
844/// `0x00000000` and will be decoded as `None` (unset). This is acceptable
845/// because fully transparent black is visually indistinguishable from unset.
846#[inline]
847#[must_use] pub const fn decode_color_u32(v: u32) -> Option<ColorU> {
848    if v == 0 { return None; }
849    Some(ColorU {
850        r: ((v >> 24) & 0xFF) as u8,
851        g: ((v >> 16) & 0xFF) as u8,
852        b: ((v >> 8) & 0xFF) as u8,
853        a: (v & 0xFF) as u8,
854    })
855}
856
857// =============================================================================
858// Tier 1: Encode / Decode
859// =============================================================================
860
861/// Pack all 21 enum properties into a single u64.
862#[inline]
863#[must_use] pub const fn encode_tier1(
864    display: LayoutDisplay,
865    position: LayoutPosition,
866    float: LayoutFloat,
867    overflow_x: LayoutOverflow,
868    overflow_y: LayoutOverflow,
869    box_sizing: LayoutBoxSizing,
870    flex_direction: LayoutFlexDirection,
871    flex_wrap: LayoutFlexWrap,
872    justify_content: LayoutJustifyContent,
873    align_items: LayoutAlignItems,
874    align_content: LayoutAlignContent,
875    writing_mode: LayoutWritingMode,
876    clear: LayoutClear,
877    font_weight: StyleFontWeight,
878    font_style: StyleFontStyle,
879    text_align: StyleTextAlign,
880    visibility: StyleVisibility,
881    white_space: StyleWhiteSpace,
882    direction: StyleDirection,
883    vertical_align: StyleVerticalAlign,
884    border_collapse: StyleBorderCollapse,
885) -> u64 {
886    let mut v: u64 = TIER1_POPULATED_BIT;
887    v |= (layout_display_to_u8(display) as u64) << DISPLAY_SHIFT;
888    v |= (layout_position_to_u8(position) as u64) << POSITION_SHIFT;
889    v |= (layout_float_to_u8(float) as u64) << FLOAT_SHIFT;
890    v |= (layout_overflow_to_u8(overflow_x) as u64) << OVERFLOW_X_SHIFT;
891    v |= (layout_overflow_to_u8(overflow_y) as u64) << OVERFLOW_Y_SHIFT;
892    v |= (layout_box_sizing_to_u8(box_sizing) as u64) << BOX_SIZING_SHIFT;
893    v |= (layout_flex_direction_to_u8(flex_direction) as u64) << FLEX_DIRECTION_SHIFT;
894    v |= (layout_flex_wrap_to_u8(flex_wrap) as u64) << FLEX_WRAP_SHIFT;
895    v |= (layout_justify_content_to_u8(justify_content) as u64) << JUSTIFY_CONTENT_SHIFT;
896    v |= (layout_align_items_to_u8(align_items) as u64) << ALIGN_ITEMS_SHIFT;
897    v |= (layout_align_content_to_u8(align_content) as u64) << ALIGN_CONTENT_SHIFT;
898    v |= (layout_writing_mode_to_u8(writing_mode) as u64) << WRITING_MODE_SHIFT;
899    v |= (layout_clear_to_u8(clear) as u64) << CLEAR_SHIFT;
900    v |= (style_font_weight_to_u8(font_weight) as u64) << FONT_WEIGHT_SHIFT;
901    v |= (style_font_style_to_u8(font_style) as u64) << FONT_STYLE_SHIFT;
902    v |= (style_text_align_to_u8(text_align) as u64) << TEXT_ALIGN_SHIFT;
903    v |= (style_visibility_to_u8(visibility) as u64) << VISIBILITY_SHIFT;
904    v |= (style_white_space_to_u8(white_space) as u64) << WHITE_SPACE_SHIFT;
905    v |= (style_direction_to_u8(direction) as u64) << DIRECTION_SHIFT;
906    v |= (style_vertical_align_to_u8(vertical_align) as u64) << VERTICAL_ALIGN_SHIFT;
907    v |= (border_collapse_to_u8(border_collapse) as u64) << BORDER_COLLAPSE_SHIFT;
908    v
909}
910
911// Decode individual enum properties from a Tier 1 u64.
912// Each function is `#[inline]` for zero-cost extraction.
913
914#[inline]
915#[must_use] pub const fn decode_display(t1: u64) -> LayoutDisplay {
916    layout_display_from_u8(((t1 >> DISPLAY_SHIFT) & DISPLAY_MASK) as u8)
917}
918
919#[inline]
920#[must_use] pub const fn decode_position(t1: u64) -> LayoutPosition {
921    layout_position_from_u8(((t1 >> POSITION_SHIFT) & POSITION_MASK) as u8)
922}
923
924#[inline]
925#[must_use] pub const fn decode_float(t1: u64) -> LayoutFloat {
926    layout_float_from_u8(((t1 >> FLOAT_SHIFT) & FLOAT_MASK) as u8)
927}
928
929#[inline]
930#[must_use] pub const fn decode_overflow_x(t1: u64) -> LayoutOverflow {
931    layout_overflow_from_u8(((t1 >> OVERFLOW_X_SHIFT) & OVERFLOW_MASK) as u8)
932}
933
934#[inline]
935#[must_use] pub const fn decode_overflow_y(t1: u64) -> LayoutOverflow {
936    layout_overflow_from_u8(((t1 >> OVERFLOW_Y_SHIFT) & OVERFLOW_MASK) as u8)
937}
938
939#[inline]
940#[must_use] pub const fn decode_box_sizing(t1: u64) -> LayoutBoxSizing {
941    layout_box_sizing_from_u8(((t1 >> BOX_SIZING_SHIFT) & BOX_SIZING_MASK) as u8)
942}
943
944#[inline]
945#[must_use] pub const fn decode_flex_direction(t1: u64) -> LayoutFlexDirection {
946    layout_flex_direction_from_u8(((t1 >> FLEX_DIRECTION_SHIFT) & FLEX_DIR_MASK) as u8)
947}
948
949#[inline]
950#[must_use] pub const fn decode_flex_wrap(t1: u64) -> LayoutFlexWrap {
951    layout_flex_wrap_from_u8(((t1 >> FLEX_WRAP_SHIFT) & FLEX_WRAP_MASK) as u8)
952}
953
954#[inline]
955#[must_use] pub const fn decode_justify_content(t1: u64) -> LayoutJustifyContent {
956    layout_justify_content_from_u8(((t1 >> JUSTIFY_CONTENT_SHIFT) & JUSTIFY_MASK) as u8)
957}
958
959#[inline]
960#[must_use] pub const fn decode_align_items(t1: u64) -> LayoutAlignItems {
961    layout_align_items_from_u8(((t1 >> ALIGN_ITEMS_SHIFT) & ALIGN_MASK) as u8)
962}
963
964#[inline]
965#[must_use] pub const fn decode_align_content(t1: u64) -> LayoutAlignContent {
966    layout_align_content_from_u8(((t1 >> ALIGN_CONTENT_SHIFT) & ALIGN_MASK) as u8)
967}
968
969#[inline]
970#[must_use] pub const fn decode_writing_mode(t1: u64) -> LayoutWritingMode {
971    layout_writing_mode_from_u8(((t1 >> WRITING_MODE_SHIFT) & WRITING_MODE_MASK) as u8)
972}
973
974#[inline]
975#[must_use] pub const fn decode_clear(t1: u64) -> LayoutClear {
976    layout_clear_from_u8(((t1 >> CLEAR_SHIFT) & CLEAR_MASK) as u8)
977}
978
979#[inline]
980#[must_use] pub const fn decode_font_weight(t1: u64) -> StyleFontWeight {
981    style_font_weight_from_u8(((t1 >> FONT_WEIGHT_SHIFT) & FONT_WEIGHT_MASK) as u8)
982}
983
984#[inline]
985#[must_use] pub const fn decode_font_style(t1: u64) -> StyleFontStyle {
986    style_font_style_from_u8(((t1 >> FONT_STYLE_SHIFT) & FONT_STYLE_MASK) as u8)
987}
988
989#[inline]
990#[must_use] pub const fn decode_text_align(t1: u64) -> StyleTextAlign {
991    style_text_align_from_u8(((t1 >> TEXT_ALIGN_SHIFT) & TEXT_ALIGN_MASK) as u8)
992}
993
994#[inline]
995#[must_use] pub const fn decode_visibility(t1: u64) -> StyleVisibility {
996    style_visibility_from_u8(((t1 >> VISIBILITY_SHIFT) & VISIBILITY_MASK) as u8)
997}
998
999#[inline]
1000#[must_use] pub const fn decode_white_space(t1: u64) -> StyleWhiteSpace {
1001    style_white_space_from_u8(((t1 >> WHITE_SPACE_SHIFT) & WHITE_SPACE_MASK) as u8)
1002}
1003
1004#[inline]
1005#[must_use] pub const fn decode_direction(t1: u64) -> StyleDirection {
1006    style_direction_from_u8(((t1 >> DIRECTION_SHIFT) & DIRECTION_MASK) as u8)
1007}
1008
1009#[inline]
1010#[must_use] pub const fn decode_vertical_align(t1: u64) -> StyleVerticalAlign {
1011    style_vertical_align_from_u8(((t1 >> VERTICAL_ALIGN_SHIFT) & VERTICAL_ALIGN_MASK) as u8)
1012}
1013
1014#[inline]
1015#[must_use] pub const fn decode_border_collapse(t1: u64) -> StyleBorderCollapse {
1016    border_collapse_from_u8(((t1 >> BORDER_COLLAPSE_SHIFT) & BORDER_COLLAPSE_MASK) as u8)
1017}
1018
1019/// Returns true if the tier1 u64 was actually populated by `encode_tier1`.
1020#[inline]
1021#[cfg(test)]
1022#[must_use] pub const fn tier1_is_populated(t1: u64) -> bool {
1023    (t1 & TIER1_POPULATED_BIT) != 0
1024}
1025
1026// =============================================================================
1027// Tier 2: CompactNodeProps — numeric dimensions (64 bytes/node)
1028// =============================================================================
1029
1030/// u32 encoding for dimension properties (width, height, min-*, max-*, flex-basis, font-size).
1031///
1032/// Layout: `[3:0] SizeMetric (4 bits) | [31:4] signed fixed-point ×1000 (28 bits)`
1033///
1034/// This matches `FloatValue`'s internal representation (isize × 1000).
1035/// Range: ±134,217.727 at 0.001 precision (28-bit signed = ±2^27 = ±134,217,728 / 1000).
1036///
1037/// Sentinel values use the top of the u32 range (0xFFFFFFF9..0xFFFFFFFF).
1038///
1039/// Encode a `PixelValue` into u32 with `SizeMetric`. Returns `U32_SENTINEL` if out of range.
1040#[inline]
1041#[must_use] pub fn encode_pixel_value_u32(pv: &PixelValue) -> u32 {
1042    let metric = u32::from(size_metric_to_u8(pv.metric));
1043    let raw = pv.number.number; // already × 1000 (FloatValue internal repr)
1044    // 28-bit signed range: -134_217_728 ..= +134_217_727
1045    if !(-134_217_728..=134_217_727).contains(&raw) {
1046        return U32_SENTINEL; // overflow → tier 3
1047    }
1048    // Pack: low 4 bits = metric, upper 28 bits = value (as unsigned offset)
1049    // raw is range-checked to 28 bits above; reinterpret its low 32 bits for packing.
1050    let value_bits = i32::try_from(raw).unwrap_or(0).cast_unsigned() << 4;
1051    let packed = value_bits | metric;
1052    // A legitimate small NEGATIVE value with a high metric nibble (e.g. -1 in
1053    // vh/vmin/vmax packs to 0xFFFF_FFF9/FA/FB) lands in the reserved sentinel band
1054    // [U32_SENTINEL_THRESHOLD, U32_SENTINEL] and decode would misread it as an unset
1055    // sentinel. Escape to tier 3 so the real value is stored losslessly instead.
1056    if packed >= U32_SENTINEL_THRESHOLD {
1057        return U32_SENTINEL;
1058    }
1059    packed
1060}
1061
1062/// Decode a u32 back to `PixelValue`. Returns None for sentinel values.
1063#[inline]
1064#[must_use] pub const fn decode_pixel_value_u32(encoded: u32) -> Option<PixelValue> {
1065    if encoded >= U32_SENTINEL_THRESHOLD {
1066        return None; // sentinel
1067    }
1068    let metric = size_metric_from_u8((encoded & 0xF) as u8);
1069    // Cast to i32 FIRST, then arithmetic right-shift to preserve sign bit
1070    let value_bits = encoded.cast_signed() >> 4;
1071    let raw = value_bits as isize; // × 1000
1072    Some(PixelValue {
1073        metric,
1074        number: FloatValue { number: raw },
1075    })
1076}
1077
1078/// Encode an i16 resolved px value (×10). Returns `I16_SENTINEL` if out of range.
1079/// Range: -3276.8 ..= +3276.3 px at 0.1px precision.
1080#[inline]
1081#[must_use] pub fn encode_resolved_px_i16(px: f32) -> i16 {
1082    let scaled = crate::cast::f32_to_i32((px * 10.0).round());
1083    if scaled < -32768 || scaled > i32::from(I16_SENTINEL_THRESHOLD) - 1 {
1084        return I16_SENTINEL; // overflow or too large → tier 3
1085    }
1086    i16::try_from(scaled).unwrap_or(I16_SENTINEL)
1087}
1088
1089/// Decode an i16 back to resolved px. Returns None for sentinel values.
1090#[inline]
1091#[must_use] pub fn decode_resolved_px_i16(v: i16) -> Option<f32> {
1092    if v >= I16_SENTINEL_THRESHOLD {
1093        return None;
1094    }
1095    Some(f32::from(v) / 10.0)
1096}
1097
1098/// Encode a u16 flex value (×100). Returns `U16_SENTINEL` if out of range.
1099/// Range: 0.00 ..= 655.27 at 0.01 precision.
1100#[inline]
1101#[must_use] pub fn encode_flex_u16(value: f32) -> u16 {
1102    let scaled = crate::cast::f32_to_i32((value * 100.0).round());
1103    if scaled < 0 || scaled >= i32::from(U16_SENTINEL_THRESHOLD) {
1104        return U16_SENTINEL;
1105    }
1106    u16::try_from(scaled).unwrap_or(U16_SENTINEL)
1107}
1108
1109/// Decode a u16 flex value back to f32. Returns None for sentinel values.
1110#[inline]
1111#[must_use] pub fn decode_flex_u16(v: u16) -> Option<f32> {
1112    if v >= U16_SENTINEL_THRESHOLD {
1113        return None;
1114    }
1115    Some(f32::from(v) / 100.0)
1116}
1117
1118/// `SizeMetric` → u8 (4 bits, 12 variants)
1119#[inline]
1120#[must_use] pub const fn size_metric_to_u8(m: SizeMetric) -> u8 {
1121    match m {
1122        SizeMetric::Px => 0,
1123        SizeMetric::Pt => 1,
1124        SizeMetric::Em => 2,
1125        SizeMetric::Rem => 3,
1126        SizeMetric::In => 4,
1127        SizeMetric::Cm => 5,
1128        SizeMetric::Mm => 6,
1129        SizeMetric::Percent => 7,
1130        SizeMetric::Vw => 8,
1131        SizeMetric::Vh => 9,
1132        SizeMetric::Vmin => 10,
1133        SizeMetric::Vmax => 11,
1134    }
1135}
1136
1137/// u8 → `SizeMetric`
1138#[inline]
1139#[must_use] pub const fn size_metric_from_u8(v: u8) -> SizeMetric {
1140    match v {
1141        0 => SizeMetric::Px,
1142        1 => SizeMetric::Pt,
1143        2 => SizeMetric::Em,
1144        3 => SizeMetric::Rem,
1145        4 => SizeMetric::In,
1146        5 => SizeMetric::Cm,
1147        6 => SizeMetric::Mm,
1148        7 => SizeMetric::Percent,
1149        8 => SizeMetric::Vw,
1150        9 => SizeMetric::Vh,
1151        10 => SizeMetric::Vmin,
1152        11 => SizeMetric::Vmax,
1153        _ => SizeMetric::Px,
1154    }
1155}
1156
1157/// Layout-hot compact numeric properties for a single node (68 bytes).
1158/// Only fields accessed during the constraint-solving loop.
1159/// All dimensions use MSB-sentinel encoding.
1160#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1161#[repr(C)]
1162pub struct CompactNodeProps {
1163    // --- Dimensions needing unit (u32 MSB-sentinel) ---
1164    pub width: u32,
1165    pub height: u32,
1166    pub min_width: u32,
1167    pub max_width: u32,
1168    pub min_height: u32,
1169    pub max_height: u32,
1170    pub flex_basis: u32,
1171    pub font_size: u32,
1172
1173    // --- Resolved px values (i16 MSB-sentinel, ×10) ---
1174    pub padding_top: i16,
1175    pub padding_right: i16,
1176    pub padding_bottom: i16,
1177    pub padding_left: i16,
1178    pub margin_top: i16,
1179    pub margin_right: i16,
1180    pub margin_bottom: i16,
1181    pub margin_left: i16,
1182    pub border_top_width: i16,
1183    pub border_right_width: i16,
1184    pub border_bottom_width: i16,
1185    pub border_left_width: i16,
1186    pub top: i16,
1187    pub right: i16,
1188    pub bottom: i16,
1189    pub left: i16,
1190
1191    // --- Flex (u16 MSB-sentinel, ×100) ---
1192    pub flex_grow: u16,
1193    pub flex_shrink: u16,
1194
1195    // --- Gap (i16 px×10, 0 = default) ---
1196    pub row_gap: i16,
1197    pub column_gap: i16,
1198}
1199
1200/// Paint-cold compact properties for a single node.
1201/// Only accessed during display list generation, table layout, or text shaping.
1202#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1203#[repr(C)]
1204pub struct CompactNodePropsCold {
1205    // --- Border colors (u32 RGBA as 0xRRGGBBAA, 0 = unset sentinel) ---
1206    pub border_top_color: u32,
1207    pub border_right_color: u32,
1208    pub border_bottom_color: u32,
1209    pub border_left_color: u32,
1210
1211    // --- Border radii (i16 px × 10, I16_SENTINEL = unset/default = 0) ---
1212    pub border_top_left_radius: i16,
1213    pub border_top_right_radius: i16,
1214    pub border_bottom_left_radius: i16,
1215    pub border_bottom_right_radius: i16,
1216
1217    // --- Other ---
1218    pub z_index: i16,   // range ±32764, sentinel = 0x7FFF
1219    /// Border styles packed: [3:0]=top, [7:4]=right, [11:8]=bottom, [15:12]=left
1220    pub border_styles_packed: u16,
1221    pub border_spacing_h: i16,
1222    pub border_spacing_v: i16,
1223    pub tab_size: i16,
1224    /// Grid column start (`I16_AUTO` = auto, positive = line number, negative = span)
1225    pub grid_col_start: i16,
1226    /// Grid column end
1227    pub grid_col_end: i16,
1228    /// Grid row start
1229    pub grid_row_start: i16,
1230    /// Grid row end
1231    pub grid_row_end: i16,
1232
1233    // --- GPU / hot paint props ---
1234    /// Opacity × 254 (0 = fully transparent, 254 = opaque). 255 = unset/default (= 1.0).
1235    pub opacity: u8,
1236    /// Bitflags for properties that are usually unset. Lets the getter
1237    /// short-circuit without a cascade walk when the value is the default.
1238    ///
1239    /// bit 0: `has_transform`                (slow-walk only when set)
1240    /// bit 1: `has_transform_origin`
1241    /// bit 2: `has_box_shadow`
1242    /// bit 3: `has_text_decoration`          (slow-walk only when set)
1243    /// bits 4-5: `scrollbar_gutter` (0 = auto default, 1 = stable, 2 = both-edges, 3 = mirror)
1244    /// bit 6: `has_background`                (slow-walk only when set; ≈ negative fast path)
1245    /// bit 7: `has_clip_path`                 (slow-walk only when set)
1246    pub hot_flags: u8,
1247    /// Second byte of flags for rarely-set properties.
1248    ///
1249    /// bit 0: `has_any_scrollbar_css`
1250    ///        OR of all -azul-scrollbar-* / scrollbar-color / scrollbar-width props.
1251    ///        When clear, `get_scrollbar_style` can skip 8 cascade walks and use
1252    ///        the UA-default result.
1253    /// bit 1: `has_counter`      (counter-reset OR counter-increment)
1254    /// bit 2: `has_break`        (break-before OR break-after)
1255    /// bit 3: `has_text_orientation`
1256    /// bit 4: `has_text_shadow`
1257    /// bit 5: `has_backdrop_filter`
1258    /// bit 6: `has_filter`
1259    /// bit 7: `has_mix_blend_mode`
1260    pub extra_flags: u8,
1261}
1262
1263pub const OPACITY_SENTINEL: u8 = 255;
1264pub const HOT_FLAG_HAS_TRANSFORM: u8 = 1 << 0;
1265pub const HOT_FLAG_HAS_TRANSFORM_ORIGIN: u8 = 1 << 1;
1266pub const HOT_FLAG_HAS_BOX_SHADOW: u8 = 1 << 2;
1267pub const HOT_FLAG_HAS_TEXT_DECORATION: u8 = 1 << 3;
1268pub const HOT_FLAG_SCROLLBAR_GUTTER_SHIFT: u8 = 4;
1269pub const HOT_FLAG_SCROLLBAR_GUTTER_MASK: u8 = 0b0011_0000;
1270pub const HOT_FLAG_HAS_BACKGROUND: u8 = 1 << 6;
1271pub const HOT_FLAG_HAS_CLIP_PATH: u8 = 1 << 7;
1272pub const EXTRA_FLAG_HAS_SCROLLBAR_CSS: u8 = 1 << 0;
1273pub const EXTRA_FLAG_HAS_COUNTER: u8 = 1 << 1;
1274pub const EXTRA_FLAG_HAS_BREAK: u8 = 1 << 2;
1275pub const EXTRA_FLAG_HAS_TEXT_ORIENTATION: u8 = 1 << 3;
1276pub const EXTRA_FLAG_HAS_TEXT_SHADOW: u8 = 1 << 4;
1277pub const EXTRA_FLAG_HAS_BACKDROP_FILTER: u8 = 1 << 5;
1278pub const EXTRA_FLAG_HAS_FILTER: u8 = 1 << 6;
1279pub const EXTRA_FLAG_HAS_MIX_BLEND_MODE: u8 = 1 << 7;
1280
1281// ---- DOM-level rare text prop flags (stored on CompactLayoutCache) ----
1282// Each bit = "some node in this DOM declared this property".
1283// When clear, cascade walks for that prop anywhere in the DOM
1284// necessarily return None → callers can skip the walk and use
1285// the default value. Eliminates ~N × IFC-count walks per layout
1286// in typical pages where these props are never declared.
1287pub const DOM_HAS_SHAPE_INSIDE: u32 = 1 << 0;
1288pub const DOM_HAS_SHAPE_OUTSIDE: u32 = 1 << 1;
1289pub const DOM_HAS_TEXT_JUSTIFY: u32 = 1 << 2;
1290pub const DOM_HAS_TEXT_INDENT: u32 = 1 << 3;
1291pub const DOM_HAS_COLUMN_COUNT: u32 = 1 << 4;
1292pub const DOM_HAS_COLUMN_GAP: u32 = 1 << 5;
1293pub const DOM_HAS_INITIAL_LETTER: u32 = 1 << 6;
1294pub const DOM_HAS_INITIAL_LETTER_ALIGN: u32 = 1 << 7;
1295pub const DOM_HAS_LINE_CLAMP: u32 = 1 << 8;
1296pub const DOM_HAS_HANGING_PUNCTUATION: u32 = 1 << 9;
1297pub const DOM_HAS_TEXT_COMBINE_UPRIGHT: u32 = 1 << 10;
1298pub const DOM_HAS_EXCLUSION_MARGIN: u32 = 1 << 11;
1299pub const DOM_HAS_HYPHENATION_LANGUAGE: u32 = 1 << 12;
1300pub const DOM_HAS_UNICODE_BIDI: u32 = 1 << 13;
1301pub const DOM_HAS_TEXT_BOX_TRIM: u32 = 1 << 14;
1302pub const DOM_HAS_HYPHENS: u32 = 1 << 15;
1303pub const DOM_HAS_WORD_BREAK: u32 = 1 << 16;
1304pub const DOM_HAS_OVERFLOW_WRAP: u32 = 1 << 17;
1305pub const DOM_HAS_LINE_BREAK: u32 = 1 << 18;
1306pub const DOM_HAS_TEXT_ALIGN_LAST: u32 = 1 << 19;
1307pub const DOM_HAS_LINE_HEIGHT: u32 = 1 << 20;
1308pub const DOM_HAS_COLUMN_WIDTH: u32 = 1 << 21;
1309pub const DOM_HAS_SHAPE_MARGIN: u32 = 1 << 22;
1310pub const SCROLLBAR_GUTTER_AUTO: u8 = 0;
1311pub const SCROLLBAR_GUTTER_STABLE: u8 = 1;
1312pub const SCROLLBAR_GUTTER_BOTH_EDGES: u8 = 2;
1313pub const SCROLLBAR_GUTTER_MIRROR: u8 = 3;
1314
1315impl Default for CompactNodeProps {
1316    fn default() -> Self {
1317        Self {
1318            // All dimensions default to Auto
1319            width: U32_AUTO,
1320            height: U32_AUTO,
1321            min_width: U32_AUTO,
1322            max_width: U32_NONE,
1323            min_height: U32_AUTO,
1324            max_height: U32_NONE,
1325            flex_basis: U32_AUTO,
1326            font_size: U32_INITIAL,
1327            // All resolved px default to 0
1328            padding_top: 0,
1329            padding_right: 0,
1330            padding_bottom: 0,
1331            padding_left: 0,
1332            margin_top: 0,
1333            margin_right: 0,
1334            margin_bottom: 0,
1335            margin_left: 0,
1336            border_top_width: 0,
1337            border_right_width: 0,
1338            border_bottom_width: 0,
1339            border_left_width: 0,
1340            top: I16_AUTO,
1341            right: I16_AUTO,
1342            bottom: I16_AUTO,
1343            left: I16_AUTO,
1344            // Flex defaults
1345            flex_grow: 0,
1346            flex_shrink: encode_flex_u16(1.0), // CSS default: flex-shrink: 1
1347
1348            // Gap defaults
1349            row_gap: 0,
1350            column_gap: 0,
1351        }
1352    }
1353}
1354
1355impl Default for CompactNodePropsCold {
1356    fn default() -> Self {
1357        Self {
1358            // Border colors default to 0 (sentinel/unset)
1359            border_top_color: 0,
1360            border_right_color: 0,
1361            border_bottom_color: 0,
1362            border_left_color: 0,
1363            // Border radii: I16_SENTINEL means "no rounded corner" (skip slow walk)
1364            border_top_left_radius: I16_SENTINEL,
1365            border_top_right_radius: I16_SENTINEL,
1366            border_bottom_left_radius: I16_SENTINEL,
1367            border_bottom_right_radius: I16_SENTINEL,
1368            // Other
1369            z_index: I16_AUTO,
1370            border_styles_packed: 0, // all BorderStyle::None
1371            border_spacing_h: 0,
1372            border_spacing_v: 0,
1373            tab_size: I16_SENTINEL, // default is 8em, needs resolution → sentinel
1374            grid_col_start: I16_AUTO,
1375            grid_col_end: I16_AUTO,
1376            grid_row_start: I16_AUTO,
1377            grid_row_end: I16_AUTO,
1378            opacity: OPACITY_SENTINEL,
1379            hot_flags: 0,
1380            extra_flags: 0,
1381        }
1382    }
1383}
1384
1385// =============================================================================
1386// Tier 2b: CompactTextProps — IFC/text properties (24 bytes/node)
1387// =============================================================================
1388
1389/// Compact text/IFC properties for a single node (24 bytes).
1390#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1391#[repr(C)]
1392pub struct CompactTextProps {
1393    pub text_color: u32,       // RGBA as 0xRRGGBBAA (0 = transparent/unset)
1394    pub font_family_hash: u64, // FxHash of font-family list (0 = sentinel/unset)
1395    pub line_height: i16,      // px × 10, sentinel = I16_SENTINEL
1396    pub letter_spacing: i16,   // px × 10
1397    pub word_spacing: i16,     // px × 10
1398    pub text_indent: i16,      // px × 10
1399}
1400
1401impl Default for CompactTextProps {
1402    fn default() -> Self {
1403        Self {
1404            text_color: 0,
1405            font_family_hash: 0,
1406            line_height: I16_SENTINEL, // "normal" → sentinel
1407            letter_spacing: 0,
1408            word_spacing: 0,
1409            text_indent: 0,
1410        }
1411    }
1412}
1413
1414// =============================================================================
1415// Tier 3: Overflow map — rare/complex properties
1416// =============================================================================
1417
1418// Overflow properties that couldn't fit in Tier 1/2 encoding.
1419// Contains the original `CssProperty` values for properties that:
1420// - Have `calc()` expressions
1421// - Exceed the numeric range of compact encoding
1422// - Are rare CSS properties (grid, transforms, etc.)
1423// =============================================================================
1424// CompactLayoutCache — the top-level container
1425// =============================================================================
1426
1427/// Three-tier compact layout property cache.
1428///
1429/// Allocated once per restyle, indexed by node index (same as `NodeId`).
1430/// Provides O(1) array-indexed access to all layout properties.
1431///
1432/// Non-compact properties (background, box-shadow, transform, etc.) are
1433/// resolved via the slow cascade path in `CssPropertyCache::get_property_slow()`.
1434#[derive(Debug, Clone, PartialEq, Eq)]
1435pub struct CompactLayoutCache {
1436    /// Tier 1: ALL enum properties bitpacked into u64 per node (8 B/node)
1437    pub tier1_enums: Vec<u64>,
1438    /// Tier 2 hot: Layout-critical numeric dimensions per node (68 B/node)
1439    pub tier2_dims: Vec<CompactNodeProps>,
1440    /// Tier 2 cold: Paint-only properties per node (28 B/node)
1441    pub tier2_cold: Vec<CompactNodePropsCold>,
1442    /// Tier 2b: Text/IFC properties per node (24 B/node)
1443    pub tier2b_text: Vec<CompactTextProps>,
1444    /// Indices of nodes whose `font_family_hash` changed since the last frame.
1445    ///
1446    /// Enables **per-node** font dirty tracking instead of the global all-or-nothing
1447    /// `font_stacks_hash` XOR approach. When this list is non-empty, only the
1448    /// font chains for these specific nodes need to be re-resolved, avoiding O(N)
1449    /// re-resolution when a single node's `font-family` changes.
1450    ///
1451    /// Populated during `build_compact_cache()` by comparing each node's
1452    /// `font_family_hash` against `prev_font_hashes`.
1453    pub font_dirty_nodes: Vec<usize>,
1454    /// Previous frame's per-node `font_family_hash` values.
1455    ///
1456    /// Stored after each compact cache build so that the next build can detect
1457    /// which specific nodes' font-family changed (rather than relying on a
1458    /// collision-prone global XOR hash).
1459    pub prev_font_hashes: Vec<u64>,
1460    /// Reverse map: `font_family_hash` (u64) → actual `StyleFontFamilyVec`.
1461    ///
1462    /// Populated during `build_compact_cache()` as a byproduct of hash computation.
1463    /// Consumers use this to look up font family names from the compact cache hash
1464    /// without going through `get_property_slow()` (which fails for inherited values
1465    /// on text nodes).
1466    pub font_hash_to_families: alloc::collections::BTreeMap<u64, crate::props::basic::font::StyleFontFamilyVec>,
1467    /// Bitfield tracking which rare text props are declared *anywhere* in the DOM.
1468    /// Built once during `build_compact_cache_with_inheritance`. When a bit is
1469    /// clear, callers (e.g. `translate_to_text3_constraints`) can skip the
1470    /// cascade walk for that property — its slow path would always return
1471    /// `None` and fall back to the default. See `DOM_HAS_*` constants.
1472    pub dom_declared_flags: u32,
1473}
1474
1475impl CompactLayoutCache {
1476    /// Create an empty cache (no nodes).
1477    #[must_use] pub const fn empty() -> Self {
1478        Self {
1479            tier1_enums: Vec::new(),
1480            tier2_dims: Vec::new(),
1481            tier2_cold: Vec::new(),
1482            tier2b_text: Vec::new(),
1483            font_dirty_nodes: Vec::new(),
1484            prev_font_hashes: Vec::new(),
1485            font_hash_to_families: alloc::collections::BTreeMap::new(),
1486            dom_declared_flags: 0,
1487        }
1488    }
1489
1490    /// Create a cache pre-allocated for `node_count` nodes, filled with defaults.
1491    #[must_use] pub fn with_capacity(node_count: usize) -> Self {
1492        Self {
1493            tier1_enums: vec![0u64; node_count],
1494            tier2_dims: vec![CompactNodeProps::default(); node_count],
1495            tier2_cold: vec![CompactNodePropsCold::default(); node_count],
1496            tier2b_text: vec![CompactTextProps::default(); node_count],
1497            font_dirty_nodes: Vec::new(),
1498            prev_font_hashes: vec![0u64; node_count],
1499            font_hash_to_families: alloc::collections::BTreeMap::new(),
1500            dom_declared_flags: 0,
1501        }
1502    }
1503
1504    /// Number of nodes in this cache.
1505    #[inline]
1506    #[must_use] pub const fn node_count(&self) -> usize {
1507        self.tier1_enums.len()
1508    }
1509
1510    // -- Tier 1 getters (enum properties) --
1511
1512    #[inline]
1513    #[must_use] pub fn get_display(&self, node_idx: usize) -> LayoutDisplay {
1514        decode_display(self.tier1_enums[node_idx])
1515    }
1516
1517    #[inline]
1518    #[must_use] pub fn get_position(&self, node_idx: usize) -> LayoutPosition {
1519        decode_position(self.tier1_enums[node_idx])
1520    }
1521
1522    #[inline]
1523    #[must_use] pub fn get_float(&self, node_idx: usize) -> LayoutFloat {
1524        decode_float(self.tier1_enums[node_idx])
1525    }
1526
1527    #[inline]
1528    #[must_use] pub fn get_overflow_x(&self, node_idx: usize) -> LayoutOverflow {
1529        decode_overflow_x(self.tier1_enums[node_idx])
1530    }
1531
1532    #[inline]
1533    #[must_use] pub fn get_overflow_y(&self, node_idx: usize) -> LayoutOverflow {
1534        decode_overflow_y(self.tier1_enums[node_idx])
1535    }
1536
1537    #[inline]
1538    #[must_use] pub fn get_box_sizing(&self, node_idx: usize) -> LayoutBoxSizing {
1539        decode_box_sizing(self.tier1_enums[node_idx])
1540    }
1541
1542    #[inline]
1543    #[must_use] pub fn get_flex_direction(&self, node_idx: usize) -> LayoutFlexDirection {
1544        decode_flex_direction(self.tier1_enums[node_idx])
1545    }
1546
1547    #[inline]
1548    #[must_use] pub fn get_flex_wrap(&self, node_idx: usize) -> LayoutFlexWrap {
1549        decode_flex_wrap(self.tier1_enums[node_idx])
1550    }
1551
1552    #[inline]
1553    #[must_use] pub fn get_justify_content(&self, node_idx: usize) -> LayoutJustifyContent {
1554        decode_justify_content(self.tier1_enums[node_idx])
1555    }
1556
1557    #[inline]
1558    #[must_use] pub fn get_align_items(&self, node_idx: usize) -> LayoutAlignItems {
1559        decode_align_items(self.tier1_enums[node_idx])
1560    }
1561
1562    #[inline]
1563    #[must_use] pub fn get_align_content(&self, node_idx: usize) -> LayoutAlignContent {
1564        decode_align_content(self.tier1_enums[node_idx])
1565    }
1566
1567    #[inline]
1568    #[must_use] pub fn get_writing_mode(&self, node_idx: usize) -> LayoutWritingMode {
1569        decode_writing_mode(self.tier1_enums[node_idx])
1570    }
1571
1572    #[inline]
1573    #[must_use] pub fn get_clear(&self, node_idx: usize) -> LayoutClear {
1574        decode_clear(self.tier1_enums[node_idx])
1575    }
1576
1577    #[inline]
1578    #[must_use] pub fn get_font_weight(&self, node_idx: usize) -> StyleFontWeight {
1579        decode_font_weight(self.tier1_enums[node_idx])
1580    }
1581
1582    #[inline]
1583    #[must_use] pub fn get_font_style(&self, node_idx: usize) -> StyleFontStyle {
1584        decode_font_style(self.tier1_enums[node_idx])
1585    }
1586
1587    #[inline]
1588    #[must_use] pub fn get_text_align(&self, node_idx: usize) -> StyleTextAlign {
1589        decode_text_align(self.tier1_enums[node_idx])
1590    }
1591
1592    #[inline]
1593    #[must_use] pub fn get_visibility(&self, node_idx: usize) -> StyleVisibility {
1594        decode_visibility(self.tier1_enums[node_idx])
1595    }
1596
1597    #[inline]
1598    #[must_use] pub fn get_white_space(&self, node_idx: usize) -> StyleWhiteSpace {
1599        decode_white_space(self.tier1_enums[node_idx])
1600    }
1601
1602    #[inline]
1603    #[must_use] pub fn get_direction(&self, node_idx: usize) -> StyleDirection {
1604        decode_direction(self.tier1_enums[node_idx])
1605    }
1606
1607    #[inline]
1608    #[must_use] pub fn get_vertical_align(&self, node_idx: usize) -> StyleVerticalAlign {
1609        decode_vertical_align(self.tier1_enums[node_idx])
1610    }
1611
1612    #[inline]
1613    #[must_use] pub fn get_border_collapse(&self, node_idx: usize) -> StyleBorderCollapse {
1614        decode_border_collapse(self.tier1_enums[node_idx])
1615    }
1616
1617    // -- Tier 2 getters (numeric dimensions) --
1618
1619    /// Get width as encoded u32 (use `decode_pixel_value_u32` or check sentinel).
1620    #[inline]
1621    #[must_use] pub fn get_width_raw(&self, node_idx: usize) -> u32 {
1622        self.tier2_dims[node_idx].width
1623    }
1624
1625    #[inline]
1626    #[must_use] pub fn get_height_raw(&self, node_idx: usize) -> u32 {
1627        self.tier2_dims[node_idx].height
1628    }
1629
1630    #[inline]
1631    #[must_use] pub fn get_min_width_raw(&self, node_idx: usize) -> u32 {
1632        self.tier2_dims[node_idx].min_width
1633    }
1634
1635    #[inline]
1636    #[must_use] pub fn get_max_width_raw(&self, node_idx: usize) -> u32 {
1637        self.tier2_dims[node_idx].max_width
1638    }
1639
1640    #[inline]
1641    #[must_use] pub fn get_min_height_raw(&self, node_idx: usize) -> u32 {
1642        self.tier2_dims[node_idx].min_height
1643    }
1644
1645    #[inline]
1646    #[must_use] pub fn get_max_height_raw(&self, node_idx: usize) -> u32 {
1647        self.tier2_dims[node_idx].max_height
1648    }
1649
1650    #[inline]
1651    #[must_use] pub fn get_font_size_raw(&self, node_idx: usize) -> u32 {
1652        self.tier2_dims[node_idx].font_size
1653    }
1654
1655    #[inline]
1656    #[must_use] pub fn get_flex_basis_raw(&self, node_idx: usize) -> u32 {
1657        self.tier2_dims[node_idx].flex_basis
1658    }
1659
1660    /// Get padding-top as resolved px. Returns None if sentinel (needs slow path).
1661    #[inline]
1662    #[must_use] pub fn get_padding_top(&self, node_idx: usize) -> Option<f32> {
1663        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_top)
1664    }
1665
1666    #[inline]
1667    #[must_use] pub fn get_padding_right(&self, node_idx: usize) -> Option<f32> {
1668        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_right)
1669    }
1670
1671    #[inline]
1672    #[must_use] pub fn get_padding_bottom(&self, node_idx: usize) -> Option<f32> {
1673        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_bottom)
1674    }
1675
1676    #[inline]
1677    #[must_use] pub fn get_padding_left(&self, node_idx: usize) -> Option<f32> {
1678        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_left)
1679    }
1680
1681    #[inline]
1682    #[must_use] pub fn get_margin_top(&self, node_idx: usize) -> Option<f32> {
1683        let v = self.tier2_dims[node_idx].margin_top;
1684        if v == I16_AUTO { return None; } // Auto for margin is special
1685        decode_resolved_px_i16(v)
1686    }
1687
1688    #[inline]
1689    #[must_use] pub fn get_margin_right(&self, node_idx: usize) -> Option<f32> {
1690        let v = self.tier2_dims[node_idx].margin_right;
1691        if v == I16_AUTO { return None; }
1692        decode_resolved_px_i16(v)
1693    }
1694
1695    #[inline]
1696    #[must_use] pub fn get_margin_bottom(&self, node_idx: usize) -> Option<f32> {
1697        let v = self.tier2_dims[node_idx].margin_bottom;
1698        if v == I16_AUTO { return None; }
1699        decode_resolved_px_i16(v)
1700    }
1701
1702    #[inline]
1703    #[must_use] pub fn get_margin_left(&self, node_idx: usize) -> Option<f32> {
1704        let v = self.tier2_dims[node_idx].margin_left;
1705        if v == I16_AUTO { return None; }
1706        decode_resolved_px_i16(v)
1707    }
1708
1709    /// Check if margin is Auto (important for centering logic).
1710    #[inline]
1711    #[must_use] pub fn is_margin_top_auto(&self, node_idx: usize) -> bool {
1712        self.tier2_dims[node_idx].margin_top == I16_AUTO
1713    }
1714
1715    #[inline]
1716    #[must_use] pub fn is_margin_right_auto(&self, node_idx: usize) -> bool {
1717        self.tier2_dims[node_idx].margin_right == I16_AUTO
1718    }
1719
1720    #[inline]
1721    #[must_use] pub fn is_margin_bottom_auto(&self, node_idx: usize) -> bool {
1722        self.tier2_dims[node_idx].margin_bottom == I16_AUTO
1723    }
1724
1725    #[inline]
1726    #[must_use] pub fn is_margin_left_auto(&self, node_idx: usize) -> bool {
1727        self.tier2_dims[node_idx].margin_left == I16_AUTO
1728    }
1729
1730    #[inline]
1731    #[must_use] pub fn get_border_top_width(&self, node_idx: usize) -> Option<f32> {
1732        decode_resolved_px_i16(self.tier2_dims[node_idx].border_top_width)
1733    }
1734
1735    #[inline]
1736    #[must_use] pub fn get_border_right_width(&self, node_idx: usize) -> Option<f32> {
1737        decode_resolved_px_i16(self.tier2_dims[node_idx].border_right_width)
1738    }
1739
1740    #[inline]
1741    #[must_use] pub fn get_border_bottom_width(&self, node_idx: usize) -> Option<f32> {
1742        decode_resolved_px_i16(self.tier2_dims[node_idx].border_bottom_width)
1743    }
1744
1745    #[inline]
1746    #[must_use] pub fn get_border_left_width(&self, node_idx: usize) -> Option<f32> {
1747        decode_resolved_px_i16(self.tier2_dims[node_idx].border_left_width)
1748    }
1749
1750    // -- Raw i16 getters for macro fast paths --
1751
1752    #[inline]
1753    #[must_use] pub fn get_padding_top_raw(&self, node_idx: usize) -> i16 {
1754        self.tier2_dims[node_idx].padding_top
1755    }
1756
1757    #[inline]
1758    #[must_use] pub fn get_padding_right_raw(&self, node_idx: usize) -> i16 {
1759        self.tier2_dims[node_idx].padding_right
1760    }
1761
1762    #[inline]
1763    #[must_use] pub fn get_padding_bottom_raw(&self, node_idx: usize) -> i16 {
1764        self.tier2_dims[node_idx].padding_bottom
1765    }
1766
1767    #[inline]
1768    #[must_use] pub fn get_padding_left_raw(&self, node_idx: usize) -> i16 {
1769        self.tier2_dims[node_idx].padding_left
1770    }
1771
1772    #[inline]
1773    #[must_use] pub fn get_margin_top_raw(&self, node_idx: usize) -> i16 {
1774        self.tier2_dims[node_idx].margin_top
1775    }
1776
1777    #[inline]
1778    #[must_use] pub fn get_margin_right_raw(&self, node_idx: usize) -> i16 {
1779        self.tier2_dims[node_idx].margin_right
1780    }
1781
1782    #[inline]
1783    #[must_use] pub fn get_margin_bottom_raw(&self, node_idx: usize) -> i16 {
1784        self.tier2_dims[node_idx].margin_bottom
1785    }
1786
1787    #[inline]
1788    #[must_use] pub fn get_margin_left_raw(&self, node_idx: usize) -> i16 {
1789        self.tier2_dims[node_idx].margin_left
1790    }
1791
1792    #[inline]
1793    #[must_use] pub fn get_border_top_width_raw(&self, node_idx: usize) -> i16 {
1794        self.tier2_dims[node_idx].border_top_width
1795    }
1796
1797    #[inline]
1798    #[must_use] pub fn get_border_right_width_raw(&self, node_idx: usize) -> i16 {
1799        self.tier2_dims[node_idx].border_right_width
1800    }
1801
1802    #[inline]
1803    #[must_use] pub fn get_border_bottom_width_raw(&self, node_idx: usize) -> i16 {
1804        self.tier2_dims[node_idx].border_bottom_width
1805    }
1806
1807    #[inline]
1808    #[must_use] pub fn get_border_left_width_raw(&self, node_idx: usize) -> i16 {
1809        self.tier2_dims[node_idx].border_left_width
1810    }
1811
1812    #[inline]
1813    #[must_use] pub fn get_top(&self, node_idx: usize) -> i16 {
1814        self.tier2_dims[node_idx].top
1815    }
1816
1817    #[inline]
1818    #[must_use] pub fn get_right(&self, node_idx: usize) -> i16 {
1819        self.tier2_dims[node_idx].right
1820    }
1821
1822    #[inline]
1823    #[must_use] pub fn get_bottom(&self, node_idx: usize) -> i16 {
1824        self.tier2_dims[node_idx].bottom
1825    }
1826
1827    #[inline]
1828    #[must_use] pub fn get_left(&self, node_idx: usize) -> i16 {
1829        self.tier2_dims[node_idx].left
1830    }
1831
1832    #[inline]
1833    #[must_use] pub fn get_flex_grow(&self, node_idx: usize) -> Option<f32> {
1834        decode_flex_u16(self.tier2_dims[node_idx].flex_grow)
1835    }
1836
1837    #[inline]
1838    #[must_use] pub fn get_flex_shrink(&self, node_idx: usize) -> Option<f32> {
1839        decode_flex_u16(self.tier2_dims[node_idx].flex_shrink)
1840    }
1841
1842    #[inline]
1843    #[must_use] pub fn get_z_index(&self, node_idx: usize) -> i16 {
1844        self.tier2_cold[node_idx].z_index
1845    }
1846
1847    // -- Border colors (u32 RGBA) — cold tier --
1848
1849    #[inline]
1850    #[must_use] pub fn get_border_top_color_raw(&self, node_idx: usize) -> u32 {
1851        self.tier2_cold[node_idx].border_top_color
1852    }
1853
1854    #[inline]
1855    #[must_use] pub fn get_border_right_color_raw(&self, node_idx: usize) -> u32 {
1856        self.tier2_cold[node_idx].border_right_color
1857    }
1858
1859    #[inline]
1860    #[must_use] pub fn get_border_bottom_color_raw(&self, node_idx: usize) -> u32 {
1861        self.tier2_cold[node_idx].border_bottom_color
1862    }
1863
1864    #[inline]
1865    #[must_use] pub fn get_border_left_color_raw(&self, node_idx: usize) -> u32 {
1866        self.tier2_cold[node_idx].border_left_color
1867    }
1868
1869    // -- Border styles (packed u16) — cold tier --
1870
1871    #[inline]
1872    #[must_use] pub fn get_border_styles_packed(&self, node_idx: usize) -> u16 {
1873        self.tier2_cold[node_idx].border_styles_packed
1874    }
1875
1876    #[inline]
1877    #[must_use] pub fn get_border_top_style(&self, node_idx: usize) -> BorderStyle {
1878        decode_border_top_style(self.tier2_cold[node_idx].border_styles_packed)
1879    }
1880
1881    #[inline]
1882    #[must_use] pub fn get_border_right_style(&self, node_idx: usize) -> BorderStyle {
1883        decode_border_right_style(self.tier2_cold[node_idx].border_styles_packed)
1884    }
1885
1886    #[inline]
1887    #[must_use] pub fn get_border_bottom_style(&self, node_idx: usize) -> BorderStyle {
1888        decode_border_bottom_style(self.tier2_cold[node_idx].border_styles_packed)
1889    }
1890
1891    #[inline]
1892    #[must_use] pub fn get_border_left_style(&self, node_idx: usize) -> BorderStyle {
1893        decode_border_left_style(self.tier2_cold[node_idx].border_styles_packed)
1894    }
1895
1896    // -- Border spacing — cold tier --
1897
1898    #[inline]
1899    #[must_use] pub fn get_border_spacing_h_raw(&self, node_idx: usize) -> i16 {
1900        self.tier2_cold[node_idx].border_spacing_h
1901    }
1902
1903    #[inline]
1904    #[must_use] pub fn get_border_spacing_v_raw(&self, node_idx: usize) -> i16 {
1905        self.tier2_cold[node_idx].border_spacing_v
1906    }
1907
1908    // -- Tab size — cold tier --
1909
1910    #[inline]
1911    #[must_use] pub fn get_tab_size_raw(&self, node_idx: usize) -> i16 {
1912        self.tier2_cold[node_idx].tab_size
1913    }
1914
1915    // -- Border radii — cold tier (i16 px × 10, I16_SENTINEL = unset = 0) --
1916
1917    #[inline]
1918    #[must_use] pub fn get_border_top_left_radius_raw(&self, node_idx: usize) -> i16 {
1919        self.tier2_cold[node_idx].border_top_left_radius
1920    }
1921
1922    #[inline]
1923    #[must_use] pub fn get_border_top_right_radius_raw(&self, node_idx: usize) -> i16 {
1924        self.tier2_cold[node_idx].border_top_right_radius
1925    }
1926
1927    #[inline]
1928    #[must_use] pub fn get_border_bottom_left_radius_raw(&self, node_idx: usize) -> i16 {
1929        self.tier2_cold[node_idx].border_bottom_left_radius
1930    }
1931
1932    #[inline]
1933    #[must_use] pub fn get_border_bottom_right_radius_raw(&self, node_idx: usize) -> i16 {
1934        self.tier2_cold[node_idx].border_bottom_right_radius
1935    }
1936
1937    // -- Opacity / transform / hot flags --
1938
1939    /// Raw opacity byte. `OPACITY_SENTINEL` (255) = unset (default = 1.0).
1940    /// Otherwise value / 254.0 yields the opacity in [0.0, 1.0].
1941    #[inline]
1942    #[must_use] pub fn get_opacity_raw(&self, node_idx: usize) -> u8 {
1943        self.tier2_cold[node_idx].opacity
1944    }
1945
1946    #[inline]
1947    #[must_use] pub fn get_hot_flags(&self, node_idx: usize) -> u8 {
1948        self.tier2_cold[node_idx].hot_flags
1949    }
1950
1951    #[inline]
1952    #[must_use] pub fn has_transform(&self, node_idx: usize) -> bool {
1953        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_TRANSFORM != 0
1954    }
1955
1956    #[inline]
1957    #[must_use] pub fn has_transform_origin(&self, node_idx: usize) -> bool {
1958        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_TRANSFORM_ORIGIN != 0
1959    }
1960
1961    #[inline]
1962    #[must_use] pub fn has_box_shadow(&self, node_idx: usize) -> bool {
1963        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_BOX_SHADOW != 0
1964    }
1965
1966    #[inline]
1967    #[must_use] pub fn has_text_decoration(&self, node_idx: usize) -> bool {
1968        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_TEXT_DECORATION != 0
1969    }
1970
1971    #[inline]
1972    #[must_use] pub fn has_background(&self, node_idx: usize) -> bool {
1973        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0
1974    }
1975
1976    #[inline]
1977    #[must_use] pub fn has_clip_path(&self, node_idx: usize) -> bool {
1978        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_CLIP_PATH != 0
1979    }
1980
1981    #[inline]
1982    #[must_use] pub fn has_scrollbar_css(&self, node_idx: usize) -> bool {
1983        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_SCROLLBAR_CSS != 0
1984    }
1985
1986    #[inline]
1987    #[must_use] pub fn has_counter(&self, node_idx: usize) -> bool {
1988        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_COUNTER != 0
1989    }
1990
1991    #[inline]
1992    #[must_use] pub fn has_break(&self, node_idx: usize) -> bool {
1993        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_BREAK != 0
1994    }
1995
1996    #[inline]
1997    #[must_use] pub fn has_text_orientation(&self, node_idx: usize) -> bool {
1998        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_TEXT_ORIENTATION != 0
1999    }
2000
2001    #[inline]
2002    #[must_use] pub fn has_text_shadow(&self, node_idx: usize) -> bool {
2003        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_TEXT_SHADOW != 0
2004    }
2005
2006    #[inline]
2007    #[must_use] pub fn has_backdrop_filter(&self, node_idx: usize) -> bool {
2008        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_BACKDROP_FILTER != 0
2009    }
2010
2011    #[inline]
2012    #[must_use] pub fn has_filter(&self, node_idx: usize) -> bool {
2013        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_FILTER != 0
2014    }
2015
2016    #[inline]
2017    #[must_use] pub fn has_mix_blend_mode(&self, node_idx: usize) -> bool {
2018        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_MIX_BLEND_MODE != 0
2019    }
2020
2021    /// DOM-level fast-path check: returns `true` if the given flag bit is set
2022    /// (some node in this DOM declared the corresponding property).
2023    #[inline]
2024    #[must_use] pub const fn dom_declared(&self, flag: u32) -> bool {
2025        self.dom_declared_flags & flag != 0
2026    }
2027
2028    /// Scrollbar-gutter: 0 = auto (default), 1 = stable, 2 = both-edges, 3 = mirror.
2029    #[inline]
2030    #[must_use] pub fn get_scrollbar_gutter_bits(&self, node_idx: usize) -> u8 {
2031        (self.tier2_cold[node_idx].hot_flags & HOT_FLAG_SCROLLBAR_GUTTER_MASK)
2032            >> HOT_FLAG_SCROLLBAR_GUTTER_SHIFT
2033    }
2034
2035    // -- Tier 2b getters (text props) --
2036
2037    #[inline]
2038    #[must_use] pub fn get_text_color_raw(&self, node_idx: usize) -> u32 {
2039        self.tier2b_text[node_idx].text_color
2040    }
2041
2042    #[inline]
2043    #[must_use] pub fn get_font_family_hash(&self, node_idx: usize) -> u64 {
2044        self.tier2b_text[node_idx].font_family_hash
2045    }
2046
2047    #[inline]
2048    #[must_use] pub fn get_line_height(&self, node_idx: usize) -> Option<f32> {
2049        decode_resolved_px_i16(self.tier2b_text[node_idx].line_height)
2050    }
2051
2052    #[inline]
2053    #[must_use] pub fn get_letter_spacing(&self, node_idx: usize) -> Option<f32> {
2054        decode_resolved_px_i16(self.tier2b_text[node_idx].letter_spacing)
2055    }
2056
2057    #[inline]
2058    #[must_use] pub fn get_word_spacing(&self, node_idx: usize) -> Option<f32> {
2059        decode_resolved_px_i16(self.tier2b_text[node_idx].word_spacing)
2060    }
2061
2062    #[inline]
2063    #[must_use] pub fn get_text_indent(&self, node_idx: usize) -> Option<f32> {
2064        decode_resolved_px_i16(self.tier2b_text[node_idx].text_indent)
2065    }
2066
2067}
2068
2069// =============================================================================
2070// Helper: encode a CssPropertyValue<PixelValue> into i16 resolved-px
2071// =============================================================================
2072
2073/// Resolve a `CssPropertyValue`<PixelValue> to an i16 ×10 encoding.
2074///
2075/// Only handles `Exact(px(...))` values. Everything else → sentinel.
2076/// For the compact cache builder, we only pre-resolve absolute pixel values.
2077/// Relative units (em, %, etc.) get sentinel and fall back to the slow path.
2078#[inline]
2079#[must_use] pub fn encode_css_pixel_as_i16(prop: &CssPropertyValue<PixelValue>) -> i16 {
2080    match prop {
2081        CssPropertyValue::Exact(pv) => {
2082            if pv.metric == SizeMetric::Px {
2083                encode_resolved_px_i16(pv.number.get())
2084            } else {
2085                I16_SENTINEL // non-px units need resolution context → slow path
2086            }
2087        }
2088        CssPropertyValue::Auto => I16_AUTO,
2089        CssPropertyValue::Initial => I16_INITIAL,
2090        CssPropertyValue::Inherit => I16_INHERIT,
2091        _ => I16_SENTINEL,
2092    }
2093}
2094
2095#[cfg(test)]
2096mod tests {
2097    use super::*;
2098
2099    #[test]
2100    fn test_tier1_roundtrip() {
2101        let t1 = encode_tier1(
2102            LayoutDisplay::Flex,
2103            LayoutPosition::Relative,
2104            LayoutFloat::Left,
2105            LayoutOverflow::Hidden,
2106            LayoutOverflow::Scroll,
2107            LayoutBoxSizing::BorderBox,
2108            LayoutFlexDirection::Column,
2109            LayoutFlexWrap::Wrap,
2110            LayoutJustifyContent::SpaceBetween,
2111            LayoutAlignItems::Center,
2112            LayoutAlignContent::End,
2113            LayoutWritingMode::VerticalRl,
2114            LayoutClear::Both,
2115            StyleFontWeight::Bold,
2116            StyleFontStyle::Italic,
2117            StyleTextAlign::Center,
2118            StyleVisibility::Hidden,
2119            StyleWhiteSpace::Pre,
2120            StyleDirection::Rtl,
2121            StyleVerticalAlign::Middle,
2122            StyleBorderCollapse::Collapse,
2123        );
2124
2125        assert!(tier1_is_populated(t1));
2126        assert_eq!(decode_display(t1), LayoutDisplay::Flex);
2127        assert_eq!(decode_position(t1), LayoutPosition::Relative);
2128        assert_eq!(decode_float(t1), LayoutFloat::Left);
2129        assert_eq!(decode_overflow_x(t1), LayoutOverflow::Hidden);
2130        assert_eq!(decode_overflow_y(t1), LayoutOverflow::Scroll);
2131        assert_eq!(decode_box_sizing(t1), LayoutBoxSizing::BorderBox);
2132        assert_eq!(decode_flex_direction(t1), LayoutFlexDirection::Column);
2133        assert_eq!(decode_flex_wrap(t1), LayoutFlexWrap::Wrap);
2134        assert_eq!(decode_justify_content(t1), LayoutJustifyContent::SpaceBetween);
2135        assert_eq!(decode_align_items(t1), LayoutAlignItems::Center);
2136        assert_eq!(decode_align_content(t1), LayoutAlignContent::End);
2137        assert_eq!(decode_writing_mode(t1), LayoutWritingMode::VerticalRl);
2138        assert_eq!(decode_clear(t1), LayoutClear::Both);
2139        assert_eq!(decode_font_weight(t1), StyleFontWeight::Bold);
2140        assert_eq!(decode_font_style(t1), StyleFontStyle::Italic);
2141        assert_eq!(decode_text_align(t1), StyleTextAlign::Center);
2142        assert_eq!(decode_visibility(t1), StyleVisibility::Hidden);
2143        assert_eq!(decode_white_space(t1), StyleWhiteSpace::Pre);
2144        assert_eq!(decode_direction(t1), StyleDirection::Rtl);
2145        assert_eq!(decode_vertical_align(t1), StyleVerticalAlign::Middle);
2146        assert_eq!(decode_border_collapse(t1), StyleBorderCollapse::Collapse);
2147    }
2148
2149    #[test]
2150    fn test_tier1_defaults() {
2151        let t1 = encode_tier1(
2152            LayoutDisplay::Block,
2153            LayoutPosition::Static,
2154            LayoutFloat::None,
2155            LayoutOverflow::Visible,
2156            LayoutOverflow::Visible,
2157            LayoutBoxSizing::ContentBox,
2158            LayoutFlexDirection::Row,
2159            LayoutFlexWrap::NoWrap,
2160            LayoutJustifyContent::FlexStart,
2161            LayoutAlignItems::Stretch,
2162            LayoutAlignContent::Stretch,
2163            LayoutWritingMode::HorizontalTb,
2164            LayoutClear::None,
2165            StyleFontWeight::Normal,
2166            StyleFontStyle::Normal,
2167            StyleTextAlign::Left,
2168            StyleVisibility::Visible,
2169            StyleWhiteSpace::Normal,
2170            StyleDirection::Ltr,
2171            StyleVerticalAlign::Baseline,
2172            StyleBorderCollapse::Separate,
2173        );
2174
2175        assert!(tier1_is_populated(t1));
2176        assert_eq!(decode_display(t1), LayoutDisplay::Block);
2177        assert_eq!(decode_position(t1), LayoutPosition::Static);
2178    }
2179
2180    #[test]
2181    fn test_pixel_value_u32_roundtrip() {
2182        let pv = PixelValue::px(123.456);
2183        let encoded = encode_pixel_value_u32(&pv);
2184        assert!(encoded < U32_SENTINEL_THRESHOLD);
2185        let decoded = decode_pixel_value_u32(encoded).unwrap();
2186        assert_eq!(decoded.metric, SizeMetric::Px);
2187        // Check within precision (×1000)
2188        assert!((decoded.number.get() - 123.456).abs() < 0.002);
2189    }
2190
2191    #[test]
2192    fn test_pixel_value_u32_percent() {
2193        let pv = PixelValue {
2194            metric: SizeMetric::Percent,
2195            number: FloatValue::new(50.0),
2196        };
2197        let encoded = encode_pixel_value_u32(&pv);
2198        let decoded = decode_pixel_value_u32(encoded).unwrap();
2199        assert_eq!(decoded.metric, SizeMetric::Percent);
2200        assert!((decoded.number.get() - 50.0).abs() < 0.002);
2201    }
2202
2203    #[test]
2204    fn test_sentinel_values() {
2205        assert_eq!(decode_pixel_value_u32(U32_SENTINEL), None);
2206        assert_eq!(decode_pixel_value_u32(U32_AUTO), None);
2207        assert_eq!(decode_pixel_value_u32(U32_MIN_CONTENT), None);
2208        assert_eq!(decode_resolved_px_i16(I16_SENTINEL), None);
2209        assert_eq!(decode_resolved_px_i16(I16_AUTO), None);
2210    }
2211
2212    #[test]
2213    fn test_resolved_px_i16_roundtrip() {
2214        let px = 123.4f32;
2215        let encoded = encode_resolved_px_i16(px);
2216        let decoded = decode_resolved_px_i16(encoded).unwrap();
2217        assert!((decoded - 123.4).abs() < 0.11);
2218
2219        // Negative values
2220        let px = -50.7f32;
2221        let encoded = encode_resolved_px_i16(px);
2222        let decoded = decode_resolved_px_i16(encoded).unwrap();
2223        assert!((decoded - (-50.7)).abs() < 0.11);
2224    }
2225
2226    #[test]
2227    fn test_flex_u16_roundtrip() {
2228        let v = 2.5f32;
2229        let encoded = encode_flex_u16(v);
2230        let decoded = decode_flex_u16(encoded).unwrap();
2231        assert!((decoded - 2.5).abs() < 0.011);
2232    }
2233
2234    #[test]
2235    fn test_compact_node_props_size() {
2236        // 72B hot props: 8×u32 dimensions (32B) + 16×i16 box model (32B)
2237        // + 2×u16 flex (4B) + 1×i16 order + align/pos tier1 bits (4B).
2238        assert_eq!(size_of::<CompactNodeProps>(), 72);
2239    }
2240
2241    #[test]
2242    fn test_compact_node_props_cold_size() {
2243        // 48B cold props: 4×u32 border colors (16B) + 4×i16 border radii (8B)
2244        // + 1×i16 z_index + 1×u16 border_styles_packed + 2×i16 border_spacing
2245        // + 1×i16 tab_size + 4×i16 grid placement (8B) + 3×u8 (opacity,
2246        // hot_flags, extra_flags) = 45B, padded to 48B for u32 alignment.
2247        assert_eq!(size_of::<CompactNodePropsCold>(), 48);
2248    }
2249
2250    #[test]
2251    fn test_compact_text_props_size() {
2252        assert_eq!(size_of::<CompactTextProps>(), 24);
2253    }
2254
2255    // ========================================================================
2256    // Tier1 enum 0-sentinel contract
2257    //
2258    // Tier1 packs 21 enums into a single u64. A bit run that is all zeros
2259    // must decode to the CSS initial value of that property, because an
2260    // unpopulated tier1 field is all zeros. If any encoder shifts so that
2261    // `0 -> something-other-than-initial`, every node that didn't explicitly
2262    // set the property silently gets the wrong default — which is exactly
2263    // how the calc.c grid stretch regression shipped (Start encoded as 0,
2264    // so every grid container reported justify-items: Start instead of
2265    // the CSS default Stretch-for-grid, collapsing the calc button grid).
2266    //
2267    // Test invariant: decoding a u8 of 0 for every enum yields the CSS
2268    // initial value of that property.
2269    // ========================================================================
2270
2271    #[test]
2272    fn test_justify_items_zero_is_stretch() {
2273        // CSS initial for justify-items is `normal`, which on a grid item
2274        // behaves as `stretch`. The tier1 bit pattern 0 must round-trip to
2275        // Stretch so unset grid containers don't collapse their items.
2276        assert_eq!(layout_justify_items_from_u8(0), LayoutJustifyItems::Stretch);
2277        assert_eq!(layout_justify_items_to_u8(LayoutJustifyItems::Stretch), 0);
2278    }
2279
2280    #[test]
2281    fn test_tier1_enum_zero_sentinel_is_css_initial() {
2282        assert_eq!(layout_display_from_u8(0), LayoutDisplay::Block);
2283        assert_eq!(layout_position_from_u8(0), LayoutPosition::Static);
2284        assert_eq!(layout_float_from_u8(0), LayoutFloat::None);
2285        assert_eq!(layout_overflow_from_u8(0), LayoutOverflow::Visible);
2286        assert_eq!(layout_box_sizing_from_u8(0), LayoutBoxSizing::ContentBox);
2287        assert_eq!(layout_flex_direction_from_u8(0), LayoutFlexDirection::Row);
2288        assert_eq!(layout_flex_wrap_from_u8(0), LayoutFlexWrap::NoWrap);
2289        assert_eq!(layout_justify_content_from_u8(0), LayoutJustifyContent::FlexStart);
2290        assert_eq!(layout_align_items_from_u8(0), LayoutAlignItems::Stretch);
2291        assert_eq!(layout_align_content_from_u8(0), LayoutAlignContent::Stretch);
2292        assert_eq!(layout_align_self_from_u8(0), LayoutAlignSelf::Auto);
2293        assert_eq!(layout_justify_self_from_u8(0), LayoutJustifySelf::Auto);
2294        assert_eq!(layout_justify_items_from_u8(0), LayoutJustifyItems::Stretch);
2295        assert_eq!(layout_grid_auto_flow_from_u8(0), LayoutGridAutoFlow::Row);
2296        assert_eq!(layout_writing_mode_from_u8(0), LayoutWritingMode::HorizontalTb);
2297        assert_eq!(layout_clear_from_u8(0), LayoutClear::None);
2298        assert_eq!(style_font_weight_from_u8(0), StyleFontWeight::Normal);
2299        assert_eq!(style_font_style_from_u8(0), StyleFontStyle::Normal);
2300        // text-align initial is `start`; we collapse `start` → `left` on
2301        // LTR runs at encode time, so the 0 slot decodes to Left.
2302        assert_eq!(style_text_align_from_u8(0), StyleTextAlign::Start);
2303        assert_eq!(style_visibility_from_u8(0), StyleVisibility::Visible);
2304        assert_eq!(style_white_space_from_u8(0), StyleWhiteSpace::Normal);
2305        assert_eq!(style_direction_from_u8(0), StyleDirection::Ltr);
2306        assert_eq!(style_vertical_align_from_u8(0), StyleVerticalAlign::Baseline);
2307        assert_eq!(border_collapse_from_u8(0), StyleBorderCollapse::Separate);
2308    }
2309
2310    #[test]
2311    fn test_tier1_enum_initial_encodes_to_zero() {
2312        // Mirror of the above — encoding the CSS initial value must
2313        // produce 0, otherwise an `all-zeros` tier1 bit run would encode
2314        // a non-initial value and nodes without explicit properties would
2315        // silently inherit the wrong default.
2316        assert_eq!(layout_display_to_u8(LayoutDisplay::Block), 0);
2317        assert_eq!(layout_position_to_u8(LayoutPosition::Static), 0);
2318        assert_eq!(layout_float_to_u8(LayoutFloat::None), 0);
2319        assert_eq!(layout_overflow_to_u8(LayoutOverflow::Visible), 0);
2320        assert_eq!(layout_box_sizing_to_u8(LayoutBoxSizing::ContentBox), 0);
2321        assert_eq!(layout_flex_direction_to_u8(LayoutFlexDirection::Row), 0);
2322        assert_eq!(layout_flex_wrap_to_u8(LayoutFlexWrap::NoWrap), 0);
2323        assert_eq!(layout_justify_content_to_u8(LayoutJustifyContent::FlexStart), 0);
2324        assert_eq!(layout_align_items_to_u8(LayoutAlignItems::Stretch), 0);
2325        assert_eq!(layout_align_content_to_u8(LayoutAlignContent::Stretch), 0);
2326        assert_eq!(layout_align_self_to_u8(LayoutAlignSelf::Auto), 0);
2327        assert_eq!(layout_justify_self_to_u8(LayoutJustifySelf::Auto), 0);
2328        assert_eq!(layout_justify_items_to_u8(LayoutJustifyItems::Stretch), 0);
2329        assert_eq!(layout_grid_auto_flow_to_u8(LayoutGridAutoFlow::Row), 0);
2330        assert_eq!(layout_writing_mode_to_u8(LayoutWritingMode::HorizontalTb), 0);
2331        assert_eq!(layout_clear_to_u8(LayoutClear::None), 0);
2332        assert_eq!(style_font_weight_to_u8(StyleFontWeight::Normal), 0);
2333        assert_eq!(style_font_style_to_u8(StyleFontStyle::Normal), 0);
2334        assert_eq!(style_text_align_to_u8(StyleTextAlign::Left), 4);
2335        assert_eq!(style_visibility_to_u8(StyleVisibility::Visible), 0);
2336        assert_eq!(style_white_space_to_u8(StyleWhiteSpace::Normal), 0);
2337        assert_eq!(style_direction_to_u8(StyleDirection::Ltr), 0);
2338        assert_eq!(style_vertical_align_to_u8(StyleVerticalAlign::Baseline), 0);
2339        assert_eq!(border_collapse_to_u8(StyleBorderCollapse::Separate), 0);
2340    }
2341
2342    // ========================================================================
2343    // Exhaustive round-trip: every variant of every enum must survive
2344    // encode → decode unchanged. Catches any reordering that maps two
2345    // different variants to the same u8, or any mask-width mismatch.
2346    // ========================================================================
2347
2348    macro_rules! roundtrip_all {
2349        ($name:ident, $to:ident, $from:ident, [$($variant:expr),+ $(,)?]) => {
2350            #[test]
2351            fn $name() {
2352                for v in [$($variant),+] {
2353                    let u = $to(v);
2354                    let decoded = $from(u);
2355                    assert_eq!(decoded, v, "{:?} != {:?} (via u8 = {})", decoded, v, u);
2356                }
2357            }
2358        };
2359    }
2360
2361    roundtrip_all!(rt_display, layout_display_to_u8, layout_display_from_u8, [
2362        LayoutDisplay::Block, LayoutDisplay::Inline, LayoutDisplay::InlineBlock,
2363        LayoutDisplay::Flex, LayoutDisplay::None, LayoutDisplay::InlineFlex,
2364        LayoutDisplay::Table, LayoutDisplay::InlineTable, LayoutDisplay::TableRowGroup,
2365        LayoutDisplay::TableHeaderGroup, LayoutDisplay::TableFooterGroup,
2366        LayoutDisplay::TableRow, LayoutDisplay::TableColumnGroup,
2367        LayoutDisplay::TableColumn, LayoutDisplay::TableCell,
2368        LayoutDisplay::TableCaption, LayoutDisplay::FlowRoot,
2369        LayoutDisplay::ListItem, LayoutDisplay::RunIn, LayoutDisplay::Marker,
2370        LayoutDisplay::Grid, LayoutDisplay::InlineGrid, LayoutDisplay::Contents,
2371    ]);
2372
2373    roundtrip_all!(rt_position, layout_position_to_u8, layout_position_from_u8, [
2374        LayoutPosition::Static, LayoutPosition::Relative, LayoutPosition::Absolute,
2375        LayoutPosition::Fixed, LayoutPosition::Sticky,
2376    ]);
2377
2378    roundtrip_all!(rt_float, layout_float_to_u8, layout_float_from_u8, [
2379        LayoutFloat::None, LayoutFloat::Left, LayoutFloat::Right,
2380    ]);
2381
2382    roundtrip_all!(rt_overflow, layout_overflow_to_u8, layout_overflow_from_u8, [
2383        LayoutOverflow::Visible, LayoutOverflow::Hidden, LayoutOverflow::Scroll,
2384        LayoutOverflow::Auto, LayoutOverflow::Clip,
2385    ]);
2386
2387    roundtrip_all!(rt_box_sizing, layout_box_sizing_to_u8, layout_box_sizing_from_u8, [
2388        LayoutBoxSizing::ContentBox, LayoutBoxSizing::BorderBox,
2389    ]);
2390
2391    roundtrip_all!(rt_flex_direction, layout_flex_direction_to_u8, layout_flex_direction_from_u8, [
2392        LayoutFlexDirection::Row, LayoutFlexDirection::RowReverse,
2393        LayoutFlexDirection::Column, LayoutFlexDirection::ColumnReverse,
2394    ]);
2395
2396    roundtrip_all!(rt_flex_wrap, layout_flex_wrap_to_u8, layout_flex_wrap_from_u8, [
2397        LayoutFlexWrap::NoWrap, LayoutFlexWrap::Wrap, LayoutFlexWrap::WrapReverse,
2398    ]);
2399
2400    roundtrip_all!(rt_justify_content, layout_justify_content_to_u8, layout_justify_content_from_u8, [
2401        LayoutJustifyContent::FlexStart, LayoutJustifyContent::FlexEnd,
2402        LayoutJustifyContent::Start, LayoutJustifyContent::End,
2403        LayoutJustifyContent::Center, LayoutJustifyContent::SpaceBetween,
2404        LayoutJustifyContent::SpaceAround, LayoutJustifyContent::SpaceEvenly,
2405    ]);
2406
2407    roundtrip_all!(rt_align_items, layout_align_items_to_u8, layout_align_items_from_u8, [
2408        LayoutAlignItems::Stretch, LayoutAlignItems::Center, LayoutAlignItems::Start,
2409        LayoutAlignItems::End, LayoutAlignItems::Baseline,
2410    ]);
2411
2412    roundtrip_all!(rt_align_self, layout_align_self_to_u8, layout_align_self_from_u8, [
2413        LayoutAlignSelf::Auto, LayoutAlignSelf::Stretch, LayoutAlignSelf::Center,
2414        LayoutAlignSelf::Start, LayoutAlignSelf::End, LayoutAlignSelf::Baseline,
2415    ]);
2416
2417    roundtrip_all!(rt_justify_self, layout_justify_self_to_u8, layout_justify_self_from_u8, [
2418        LayoutJustifySelf::Auto, LayoutJustifySelf::Start, LayoutJustifySelf::End,
2419        LayoutJustifySelf::Center, LayoutJustifySelf::Stretch,
2420    ]);
2421
2422    roundtrip_all!(rt_justify_items, layout_justify_items_to_u8, layout_justify_items_from_u8, [
2423        LayoutJustifyItems::Stretch, LayoutJustifyItems::Start,
2424        LayoutJustifyItems::End, LayoutJustifyItems::Center,
2425    ]);
2426
2427    roundtrip_all!(rt_grid_auto_flow, layout_grid_auto_flow_to_u8, layout_grid_auto_flow_from_u8, [
2428        LayoutGridAutoFlow::Row, LayoutGridAutoFlow::Column,
2429        LayoutGridAutoFlow::RowDense, LayoutGridAutoFlow::ColumnDense,
2430    ]);
2431
2432    roundtrip_all!(rt_align_content, layout_align_content_to_u8, layout_align_content_from_u8, [
2433        LayoutAlignContent::Stretch, LayoutAlignContent::Center,
2434        LayoutAlignContent::Start, LayoutAlignContent::End,
2435        LayoutAlignContent::SpaceBetween, LayoutAlignContent::SpaceAround,
2436    ]);
2437
2438    roundtrip_all!(rt_writing_mode, layout_writing_mode_to_u8, layout_writing_mode_from_u8, [
2439        LayoutWritingMode::HorizontalTb, LayoutWritingMode::VerticalRl,
2440        LayoutWritingMode::VerticalLr,
2441    ]);
2442
2443    roundtrip_all!(rt_clear, layout_clear_to_u8, layout_clear_from_u8, [
2444        LayoutClear::None, LayoutClear::Left, LayoutClear::Right, LayoutClear::Both,
2445    ]);
2446
2447    roundtrip_all!(rt_font_weight, style_font_weight_to_u8, style_font_weight_from_u8, [
2448        StyleFontWeight::Normal, StyleFontWeight::W100, StyleFontWeight::W200,
2449        StyleFontWeight::W300, StyleFontWeight::W500, StyleFontWeight::W600,
2450        StyleFontWeight::Bold, StyleFontWeight::W800, StyleFontWeight::W900,
2451        StyleFontWeight::Lighter, StyleFontWeight::Bolder,
2452    ]);
2453
2454    roundtrip_all!(rt_font_style, style_font_style_to_u8, style_font_style_from_u8, [
2455        StyleFontStyle::Normal, StyleFontStyle::Italic, StyleFontStyle::Oblique,
2456    ]);
2457
2458    roundtrip_all!(rt_text_align, style_text_align_to_u8, style_text_align_from_u8, [
2459        StyleTextAlign::Left, StyleTextAlign::Center, StyleTextAlign::Right,
2460        StyleTextAlign::Justify, StyleTextAlign::Start, StyleTextAlign::End,
2461    ]);
2462
2463    roundtrip_all!(rt_visibility, style_visibility_to_u8, style_visibility_from_u8, [
2464        StyleVisibility::Visible, StyleVisibility::Hidden, StyleVisibility::Collapse,
2465    ]);
2466
2467    roundtrip_all!(rt_white_space, style_white_space_to_u8, style_white_space_from_u8, [
2468        StyleWhiteSpace::Normal, StyleWhiteSpace::Pre, StyleWhiteSpace::Nowrap,
2469        StyleWhiteSpace::PreWrap, StyleWhiteSpace::PreLine, StyleWhiteSpace::BreakSpaces,
2470    ]);
2471
2472    roundtrip_all!(rt_direction, style_direction_to_u8, style_direction_from_u8, [
2473        StyleDirection::Ltr, StyleDirection::Rtl,
2474    ]);
2475
2476    roundtrip_all!(rt_vertical_align, style_vertical_align_to_u8, style_vertical_align_from_u8, [
2477        StyleVerticalAlign::Baseline, StyleVerticalAlign::Top, StyleVerticalAlign::Middle,
2478        StyleVerticalAlign::Bottom, StyleVerticalAlign::Sub, StyleVerticalAlign::Superscript,
2479        StyleVerticalAlign::TextTop, StyleVerticalAlign::TextBottom,
2480    ]);
2481
2482    roundtrip_all!(rt_border_collapse, border_collapse_to_u8, border_collapse_from_u8, [
2483        StyleBorderCollapse::Separate, StyleBorderCollapse::Collapse,
2484    ]);
2485
2486    // ========================================================================
2487    // Bit-layout safety: every encoder must produce a u8 whose bits all
2488    // fit inside the mask allocated for that property in the tier1 u64.
2489    // If an enum grows a new variant that overflows its mask, the encoded
2490    // bits would leak into the next property's slot and silently corrupt
2491    // unrelated state.
2492    // ========================================================================
2493
2494    #[test]
2495    fn test_encoded_u8_fits_in_tier1_mask() {
2496        fn assert_fits(name: &str, val: u8, mask: u64) {
2497            assert!(
2498                u64::from(val) & !mask == 0,
2499                "{name}: encoded u8 {val} overflows mask {mask:b}",
2500            );
2501        }
2502
2503        assert_fits("display", layout_display_to_u8(LayoutDisplay::Contents), DISPLAY_MASK);
2504        assert_fits("position", layout_position_to_u8(LayoutPosition::Sticky), POSITION_MASK);
2505        assert_fits("float", layout_float_to_u8(LayoutFloat::Right), FLOAT_MASK);
2506        assert_fits("overflow", layout_overflow_to_u8(LayoutOverflow::Clip), OVERFLOW_MASK);
2507        assert_fits("box_sizing", layout_box_sizing_to_u8(LayoutBoxSizing::BorderBox), BOX_SIZING_MASK);
2508        assert_fits("flex_direction", layout_flex_direction_to_u8(LayoutFlexDirection::ColumnReverse), FLEX_DIR_MASK);
2509        assert_fits("flex_wrap", layout_flex_wrap_to_u8(LayoutFlexWrap::WrapReverse), FLEX_WRAP_MASK);
2510        assert_fits("justify_content", layout_justify_content_to_u8(LayoutJustifyContent::SpaceEvenly), JUSTIFY_MASK);
2511        assert_fits("align_items", layout_align_items_to_u8(LayoutAlignItems::Baseline), ALIGN_MASK);
2512        assert_fits("align_self", layout_align_self_to_u8(LayoutAlignSelf::Baseline), ALIGN_SELF_MASK);
2513        assert_fits("justify_self", layout_justify_self_to_u8(LayoutJustifySelf::Stretch), JUSTIFY_SELF_MASK);
2514        assert_fits("justify_items", layout_justify_items_to_u8(LayoutJustifyItems::Center), JUSTIFY_ITEMS_MASK);
2515        assert_fits("grid_auto_flow", layout_grid_auto_flow_to_u8(LayoutGridAutoFlow::ColumnDense), GRID_AUTO_FLOW_MASK);
2516        assert_fits("align_content", layout_align_content_to_u8(LayoutAlignContent::SpaceAround), ALIGN_MASK);
2517        assert_fits("writing_mode", layout_writing_mode_to_u8(LayoutWritingMode::VerticalLr), WRITING_MODE_MASK);
2518        assert_fits("clear", layout_clear_to_u8(LayoutClear::Both), CLEAR_MASK);
2519        assert_fits("font_weight", style_font_weight_to_u8(StyleFontWeight::Bolder), FONT_WEIGHT_MASK);
2520        assert_fits("font_style", style_font_style_to_u8(StyleFontStyle::Oblique), FONT_STYLE_MASK);
2521        assert_fits("text_align", style_text_align_to_u8(StyleTextAlign::End), TEXT_ALIGN_MASK);
2522        assert_fits("visibility", style_visibility_to_u8(StyleVisibility::Collapse), VISIBILITY_MASK);
2523        assert_fits("white_space", style_white_space_to_u8(StyleWhiteSpace::BreakSpaces), WHITE_SPACE_MASK);
2524        assert_fits("direction", style_direction_to_u8(StyleDirection::Rtl), DIRECTION_MASK);
2525        assert_fits("vertical_align", style_vertical_align_to_u8(StyleVerticalAlign::TextBottom), VERTICAL_ALIGN_MASK);
2526        assert_fits("border_collapse", border_collapse_to_u8(StyleBorderCollapse::Collapse), BORDER_COLLAPSE_MASK);
2527    }
2528
2529    // ========================================================================
2530    // Empty tier1 decodes to all-initial — this is the core contract that
2531    // was violated by the pre-fix justify_items encoding. An empty u64 with
2532    // only TIER1_POPULATED_BIT set must decode every property to its CSS
2533    // initial value; this is how `build_compact_cache` can leave unspecified
2534    // properties at 0 and still produce the correct cascade result.
2535    // ========================================================================
2536
2537    #[test]
2538    fn test_empty_tier1_decodes_to_initial_values() {
2539        let t1 = TIER1_POPULATED_BIT; // populated, but zero content
2540        assert!(tier1_is_populated(t1));
2541        assert_eq!(decode_display(t1), LayoutDisplay::Block);
2542        assert_eq!(decode_position(t1), LayoutPosition::Static);
2543        assert_eq!(decode_float(t1), LayoutFloat::None);
2544        assert_eq!(decode_overflow_x(t1), LayoutOverflow::Visible);
2545        assert_eq!(decode_overflow_y(t1), LayoutOverflow::Visible);
2546        assert_eq!(decode_box_sizing(t1), LayoutBoxSizing::ContentBox);
2547        assert_eq!(decode_flex_direction(t1), LayoutFlexDirection::Row);
2548        assert_eq!(decode_flex_wrap(t1), LayoutFlexWrap::NoWrap);
2549        assert_eq!(decode_justify_content(t1), LayoutJustifyContent::FlexStart);
2550        assert_eq!(decode_align_items(t1), LayoutAlignItems::Stretch);
2551        assert_eq!(decode_align_content(t1), LayoutAlignContent::Stretch);
2552        assert_eq!(decode_writing_mode(t1), LayoutWritingMode::HorizontalTb);
2553        assert_eq!(decode_clear(t1), LayoutClear::None);
2554        assert_eq!(decode_font_weight(t1), StyleFontWeight::Normal);
2555        assert_eq!(decode_font_style(t1), StyleFontStyle::Normal);
2556        assert_eq!(decode_text_align(t1), StyleTextAlign::Start);
2557        assert_eq!(decode_visibility(t1), StyleVisibility::Visible);
2558        assert_eq!(decode_white_space(t1), StyleWhiteSpace::Normal);
2559        assert_eq!(decode_direction(t1), StyleDirection::Ltr);
2560        assert_eq!(decode_vertical_align(t1), StyleVerticalAlign::Baseline);
2561        assert_eq!(decode_border_collapse(t1), StyleBorderCollapse::Separate);
2562    }
2563}
2564
2565#[cfg(test)]
2566#[allow(
2567    clippy::float_cmp,
2568    clippy::unreadable_literal,
2569    clippy::too_many_lines,
2570    clippy::cast_precision_loss
2571)]
2572mod autotest_generated {
2573    use super::*;
2574    use crate::props::basic::length::PercentageValue;
2575
2576    // =========================================================================
2577    // Shared fixtures
2578    // =========================================================================
2579
2580    const ALL_DISPLAY: [LayoutDisplay; 23] = [
2581        LayoutDisplay::Block,
2582        LayoutDisplay::Inline,
2583        LayoutDisplay::InlineBlock,
2584        LayoutDisplay::Flex,
2585        LayoutDisplay::None,
2586        LayoutDisplay::InlineFlex,
2587        LayoutDisplay::Table,
2588        LayoutDisplay::InlineTable,
2589        LayoutDisplay::TableRowGroup,
2590        LayoutDisplay::TableHeaderGroup,
2591        LayoutDisplay::TableFooterGroup,
2592        LayoutDisplay::TableRow,
2593        LayoutDisplay::TableColumnGroup,
2594        LayoutDisplay::TableColumn,
2595        LayoutDisplay::TableCell,
2596        LayoutDisplay::TableCaption,
2597        LayoutDisplay::FlowRoot,
2598        LayoutDisplay::ListItem,
2599        LayoutDisplay::RunIn,
2600        LayoutDisplay::Marker,
2601        LayoutDisplay::Grid,
2602        LayoutDisplay::InlineGrid,
2603        LayoutDisplay::Contents,
2604    ];
2605
2606    const ALL_BORDER_STYLE: [BorderStyle; 10] = [
2607        BorderStyle::None,
2608        BorderStyle::Solid,
2609        BorderStyle::Double,
2610        BorderStyle::Dotted,
2611        BorderStyle::Dashed,
2612        BorderStyle::Hidden,
2613        BorderStyle::Groove,
2614        BorderStyle::Ridge,
2615        BorderStyle::Inset,
2616        BorderStyle::Outset,
2617    ];
2618
2619    const ALL_SIZE_METRIC: [SizeMetric; 12] = [
2620        SizeMetric::Px,
2621        SizeMetric::Pt,
2622        SizeMetric::Em,
2623        SizeMetric::Rem,
2624        SizeMetric::In,
2625        SizeMetric::Cm,
2626        SizeMetric::Mm,
2627        SizeMetric::Percent,
2628        SizeMetric::Vw,
2629        SizeMetric::Vh,
2630        SizeMetric::Vmin,
2631        SizeMetric::Vmax,
2632    ];
2633
2634    /// Build a `PixelValue` straight from the raw fixed-point isize (×1000),
2635    /// bypassing the f32 constructor so boundary rows are exact.
2636    fn pv_raw(metric: SizeMetric, raw: isize) -> PixelValue {
2637        PixelValue {
2638            metric,
2639            number: FloatValue { number: raw },
2640        }
2641    }
2642
2643    /// All 21 tier-1 enum properties as one struct, so a decode can be checked
2644    /// field-by-field against exactly what was encoded.
2645    #[derive(Debug, Copy, Clone, PartialEq, Eq)]
2646    struct T1 {
2647        display: LayoutDisplay,
2648        position: LayoutPosition,
2649        float: LayoutFloat,
2650        overflow_x: LayoutOverflow,
2651        overflow_y: LayoutOverflow,
2652        box_sizing: LayoutBoxSizing,
2653        flex_direction: LayoutFlexDirection,
2654        flex_wrap: LayoutFlexWrap,
2655        justify_content: LayoutJustifyContent,
2656        align_items: LayoutAlignItems,
2657        align_content: LayoutAlignContent,
2658        writing_mode: LayoutWritingMode,
2659        clear: LayoutClear,
2660        font_weight: StyleFontWeight,
2661        font_style: StyleFontStyle,
2662        text_align: StyleTextAlign,
2663        visibility: StyleVisibility,
2664        white_space: StyleWhiteSpace,
2665        direction: StyleDirection,
2666        vertical_align: StyleVerticalAlign,
2667        border_collapse: StyleBorderCollapse,
2668    }
2669
2670    impl T1 {
2671        /// Every field at its CSS initial value (the u8-0 slot).
2672        const fn initial() -> Self {
2673            Self {
2674                display: LayoutDisplay::Block,
2675                position: LayoutPosition::Static,
2676                float: LayoutFloat::None,
2677                overflow_x: LayoutOverflow::Visible,
2678                overflow_y: LayoutOverflow::Visible,
2679                box_sizing: LayoutBoxSizing::ContentBox,
2680                flex_direction: LayoutFlexDirection::Row,
2681                flex_wrap: LayoutFlexWrap::NoWrap,
2682                justify_content: LayoutJustifyContent::FlexStart,
2683                align_items: LayoutAlignItems::Stretch,
2684                align_content: LayoutAlignContent::Stretch,
2685                writing_mode: LayoutWritingMode::HorizontalTb,
2686                clear: LayoutClear::None,
2687                font_weight: StyleFontWeight::Normal,
2688                font_style: StyleFontStyle::Normal,
2689                text_align: StyleTextAlign::Start,
2690                visibility: StyleVisibility::Visible,
2691                white_space: StyleWhiteSpace::Normal,
2692                direction: StyleDirection::Ltr,
2693                vertical_align: StyleVerticalAlign::Baseline,
2694                border_collapse: StyleBorderCollapse::Separate,
2695            }
2696        }
2697
2698        /// Every field at its highest-numbered variant — the worst case for a
2699        /// mask that is one bit too narrow.
2700        const fn saturated() -> Self {
2701            Self {
2702                display: LayoutDisplay::Contents,
2703                position: LayoutPosition::Sticky,
2704                float: LayoutFloat::Right,
2705                overflow_x: LayoutOverflow::Clip,
2706                overflow_y: LayoutOverflow::Clip,
2707                box_sizing: LayoutBoxSizing::BorderBox,
2708                flex_direction: LayoutFlexDirection::ColumnReverse,
2709                flex_wrap: LayoutFlexWrap::WrapReverse,
2710                justify_content: LayoutJustifyContent::SpaceEvenly,
2711                align_items: LayoutAlignItems::Baseline,
2712                align_content: LayoutAlignContent::SpaceAround,
2713                writing_mode: LayoutWritingMode::VerticalLr,
2714                clear: LayoutClear::Both,
2715                font_weight: StyleFontWeight::Bolder,
2716                font_style: StyleFontStyle::Oblique,
2717                text_align: StyleTextAlign::End,
2718                visibility: StyleVisibility::Collapse,
2719                white_space: StyleWhiteSpace::BreakSpaces,
2720                direction: StyleDirection::Rtl,
2721                vertical_align: StyleVerticalAlign::TextBottom,
2722                border_collapse: StyleBorderCollapse::Collapse,
2723            }
2724        }
2725
2726        fn encode(self) -> u64 {
2727            encode_tier1(
2728                self.display,
2729                self.position,
2730                self.float,
2731                self.overflow_x,
2732                self.overflow_y,
2733                self.box_sizing,
2734                self.flex_direction,
2735                self.flex_wrap,
2736                self.justify_content,
2737                self.align_items,
2738                self.align_content,
2739                self.writing_mode,
2740                self.clear,
2741                self.font_weight,
2742                self.font_style,
2743                self.text_align,
2744                self.visibility,
2745                self.white_space,
2746                self.direction,
2747                self.vertical_align,
2748                self.border_collapse,
2749            )
2750        }
2751
2752        fn decode(t1: u64) -> Self {
2753            Self {
2754                display: decode_display(t1),
2755                position: decode_position(t1),
2756                float: decode_float(t1),
2757                overflow_x: decode_overflow_x(t1),
2758                overflow_y: decode_overflow_y(t1),
2759                box_sizing: decode_box_sizing(t1),
2760                flex_direction: decode_flex_direction(t1),
2761                flex_wrap: decode_flex_wrap(t1),
2762                justify_content: decode_justify_content(t1),
2763                align_items: decode_align_items(t1),
2764                align_content: decode_align_content(t1),
2765                writing_mode: decode_writing_mode(t1),
2766                clear: decode_clear(t1),
2767                font_weight: decode_font_weight(t1),
2768                font_style: decode_font_style(t1),
2769                text_align: decode_text_align(t1),
2770                visibility: decode_visibility(t1),
2771                white_space: decode_white_space(t1),
2772                direction: decode_direction(t1),
2773                vertical_align: decode_vertical_align(t1),
2774                border_collapse: decode_border_collapse(t1),
2775            }
2776        }
2777    }
2778
2779    // =========================================================================
2780    // u8 decoders — every byte outside the variant range must fall back to the
2781    // CSS initial value (the u8-0 slot), never panic, never alias a real variant
2782    // =========================================================================
2783
2784    /// `first_invalid` = the first u8 that has no variant. Every byte from there
2785    /// to `u8::MAX` must decode to `initial`.
2786    fn assert_u8_fallback<T: PartialEq + core::fmt::Debug + Copy>(
2787        name: &str,
2788        decode: fn(u8) -> T,
2789        first_invalid: u8,
2790        initial: T,
2791    ) {
2792        for v in first_invalid..=u8::MAX {
2793            assert_eq!(
2794                decode(v),
2795                initial,
2796                "{name}: out-of-range byte {v} must fall back to the CSS initial value",
2797            );
2798        }
2799    }
2800
2801    #[test]
2802    fn out_of_range_u8_falls_back_to_css_initial_for_every_enum() {
2803        assert_u8_fallback("display", layout_display_from_u8, 23, LayoutDisplay::Block);
2804        assert_u8_fallback("position", layout_position_from_u8, 5, LayoutPosition::Static);
2805        assert_u8_fallback("float", layout_float_from_u8, 3, LayoutFloat::None);
2806        assert_u8_fallback("overflow", layout_overflow_from_u8, 5, LayoutOverflow::Visible);
2807        assert_u8_fallback(
2808            "box_sizing",
2809            layout_box_sizing_from_u8,
2810            2,
2811            LayoutBoxSizing::ContentBox,
2812        );
2813        assert_u8_fallback(
2814            "flex_direction",
2815            layout_flex_direction_from_u8,
2816            4,
2817            LayoutFlexDirection::Row,
2818        );
2819        assert_u8_fallback("flex_wrap", layout_flex_wrap_from_u8, 3, LayoutFlexWrap::NoWrap);
2820        assert_u8_fallback(
2821            "justify_content",
2822            layout_justify_content_from_u8,
2823            8,
2824            LayoutJustifyContent::FlexStart,
2825        );
2826        assert_u8_fallback(
2827            "align_items",
2828            layout_align_items_from_u8,
2829            5,
2830            LayoutAlignItems::Stretch,
2831        );
2832        assert_u8_fallback("align_self", layout_align_self_from_u8, 6, LayoutAlignSelf::Auto);
2833        assert_u8_fallback(
2834            "justify_self",
2835            layout_justify_self_from_u8,
2836            5,
2837            LayoutJustifySelf::Auto,
2838        );
2839        assert_u8_fallback(
2840            "justify_items",
2841            layout_justify_items_from_u8,
2842            4,
2843            LayoutJustifyItems::Stretch,
2844        );
2845        assert_u8_fallback(
2846            "grid_auto_flow",
2847            layout_grid_auto_flow_from_u8,
2848            4,
2849            LayoutGridAutoFlow::Row,
2850        );
2851        assert_u8_fallback(
2852            "align_content",
2853            layout_align_content_from_u8,
2854            6,
2855            LayoutAlignContent::Stretch,
2856        );
2857        assert_u8_fallback(
2858            "writing_mode",
2859            layout_writing_mode_from_u8,
2860            3,
2861            LayoutWritingMode::HorizontalTb,
2862        );
2863        assert_u8_fallback("clear", layout_clear_from_u8, 4, LayoutClear::None);
2864        assert_u8_fallback(
2865            "font_weight",
2866            style_font_weight_from_u8,
2867            11,
2868            StyleFontWeight::Normal,
2869        );
2870        assert_u8_fallback("font_style", style_font_style_from_u8, 3, StyleFontStyle::Normal);
2871        assert_u8_fallback("text_align", style_text_align_from_u8, 6, StyleTextAlign::Start);
2872        assert_u8_fallback(
2873            "visibility",
2874            style_visibility_from_u8,
2875            3,
2876            StyleVisibility::Visible,
2877        );
2878        assert_u8_fallback(
2879            "white_space",
2880            style_white_space_from_u8,
2881            6,
2882            StyleWhiteSpace::Normal,
2883        );
2884        assert_u8_fallback("direction", style_direction_from_u8, 2, StyleDirection::Ltr);
2885        assert_u8_fallback(
2886            "vertical_align",
2887            style_vertical_align_from_u8,
2888            8,
2889            StyleVerticalAlign::Baseline,
2890        );
2891        assert_u8_fallback(
2892            "border_collapse",
2893            border_collapse_from_u8,
2894            2,
2895            StyleBorderCollapse::Separate,
2896        );
2897        assert_u8_fallback("border_style", border_style_from_u8, 10, BorderStyle::None);
2898        assert_u8_fallback("size_metric", size_metric_from_u8, 12, SizeMetric::Px);
2899    }
2900
2901    #[test]
2902    fn display_documented_sentinel_31_decodes_to_block() {
2903        // The module doc promises 0x1F is the "look it up in the slow path"
2904        // sentinel and that decoding it yields the default rather than a
2905        // garbage variant. 31 is also DISPLAY_MASK, so a saturated tier1 u64
2906        // lands exactly here.
2907        assert_eq!(layout_display_from_u8(31), LayoutDisplay::Block);
2908        assert_eq!(layout_display_from_u8(u8::MAX), LayoutDisplay::Block);
2909    }
2910
2911    #[test]
2912    fn every_variant_of_every_enum_fits_its_tier1_mask() {
2913        // The existing suite checks only the highest variant per enum. If a new
2914        // variant is inserted in the middle with a hand-written u8 above the
2915        // mask width, that check still passes while the encoding silently
2916        // corrupts the neighbouring bit run. Check the whole variant set.
2917        fn fits(name: &str, val: u8, mask: u64) {
2918            assert!(
2919                u64::from(val) & !mask == 0,
2920                "{name}: encoded u8 {val} does not fit mask {mask:#b}",
2921            );
2922        }
2923
2924        for v in ALL_DISPLAY {
2925            fits("display", layout_display_to_u8(v), DISPLAY_MASK);
2926        }
2927        for v in [
2928            LayoutPosition::Static,
2929            LayoutPosition::Relative,
2930            LayoutPosition::Absolute,
2931            LayoutPosition::Fixed,
2932            LayoutPosition::Sticky,
2933        ] {
2934            fits("position", layout_position_to_u8(v), POSITION_MASK);
2935        }
2936        for v in [LayoutFloat::None, LayoutFloat::Left, LayoutFloat::Right] {
2937            fits("float", layout_float_to_u8(v), FLOAT_MASK);
2938        }
2939        for v in [
2940            LayoutOverflow::Visible,
2941            LayoutOverflow::Hidden,
2942            LayoutOverflow::Scroll,
2943            LayoutOverflow::Auto,
2944            LayoutOverflow::Clip,
2945        ] {
2946            fits("overflow", layout_overflow_to_u8(v), OVERFLOW_MASK);
2947        }
2948        for v in [LayoutBoxSizing::ContentBox, LayoutBoxSizing::BorderBox] {
2949            fits("box_sizing", layout_box_sizing_to_u8(v), BOX_SIZING_MASK);
2950        }
2951        for v in [
2952            LayoutFlexDirection::Row,
2953            LayoutFlexDirection::RowReverse,
2954            LayoutFlexDirection::Column,
2955            LayoutFlexDirection::ColumnReverse,
2956        ] {
2957            fits("flex_direction", layout_flex_direction_to_u8(v), FLEX_DIR_MASK);
2958        }
2959        for v in [
2960            LayoutFlexWrap::NoWrap,
2961            LayoutFlexWrap::Wrap,
2962            LayoutFlexWrap::WrapReverse,
2963        ] {
2964            fits("flex_wrap", layout_flex_wrap_to_u8(v), FLEX_WRAP_MASK);
2965        }
2966        for v in [
2967            LayoutJustifyContent::FlexStart,
2968            LayoutJustifyContent::FlexEnd,
2969            LayoutJustifyContent::Start,
2970            LayoutJustifyContent::End,
2971            LayoutJustifyContent::Center,
2972            LayoutJustifyContent::SpaceBetween,
2973            LayoutJustifyContent::SpaceAround,
2974            LayoutJustifyContent::SpaceEvenly,
2975        ] {
2976            fits("justify_content", layout_justify_content_to_u8(v), JUSTIFY_MASK);
2977        }
2978        for v in [
2979            LayoutAlignItems::Stretch,
2980            LayoutAlignItems::Center,
2981            LayoutAlignItems::Start,
2982            LayoutAlignItems::End,
2983            LayoutAlignItems::Baseline,
2984        ] {
2985            fits("align_items", layout_align_items_to_u8(v), ALIGN_MASK);
2986        }
2987        for v in [
2988            LayoutAlignSelf::Auto,
2989            LayoutAlignSelf::Stretch,
2990            LayoutAlignSelf::Center,
2991            LayoutAlignSelf::Start,
2992            LayoutAlignSelf::End,
2993            LayoutAlignSelf::Baseline,
2994        ] {
2995            fits("align_self", layout_align_self_to_u8(v), ALIGN_SELF_MASK);
2996        }
2997        for v in [
2998            LayoutJustifySelf::Auto,
2999            LayoutJustifySelf::Start,
3000            LayoutJustifySelf::End,
3001            LayoutJustifySelf::Center,
3002            LayoutJustifySelf::Stretch,
3003        ] {
3004            fits("justify_self", layout_justify_self_to_u8(v), JUSTIFY_SELF_MASK);
3005        }
3006        for v in [
3007            LayoutJustifyItems::Stretch,
3008            LayoutJustifyItems::Start,
3009            LayoutJustifyItems::End,
3010            LayoutJustifyItems::Center,
3011        ] {
3012            fits("justify_items", layout_justify_items_to_u8(v), JUSTIFY_ITEMS_MASK);
3013        }
3014        for v in [
3015            LayoutGridAutoFlow::Row,
3016            LayoutGridAutoFlow::Column,
3017            LayoutGridAutoFlow::RowDense,
3018            LayoutGridAutoFlow::ColumnDense,
3019        ] {
3020            fits("grid_auto_flow", layout_grid_auto_flow_to_u8(v), GRID_AUTO_FLOW_MASK);
3021        }
3022        for v in [
3023            LayoutAlignContent::Stretch,
3024            LayoutAlignContent::Center,
3025            LayoutAlignContent::Start,
3026            LayoutAlignContent::End,
3027            LayoutAlignContent::SpaceBetween,
3028            LayoutAlignContent::SpaceAround,
3029        ] {
3030            fits("align_content", layout_align_content_to_u8(v), ALIGN_MASK);
3031        }
3032        for v in [
3033            LayoutWritingMode::HorizontalTb,
3034            LayoutWritingMode::VerticalRl,
3035            LayoutWritingMode::VerticalLr,
3036        ] {
3037            fits("writing_mode", layout_writing_mode_to_u8(v), WRITING_MODE_MASK);
3038        }
3039        for v in [
3040            LayoutClear::None,
3041            LayoutClear::Left,
3042            LayoutClear::Right,
3043            LayoutClear::Both,
3044        ] {
3045            fits("clear", layout_clear_to_u8(v), CLEAR_MASK);
3046        }
3047        for v in [
3048            StyleFontWeight::Normal,
3049            StyleFontWeight::W100,
3050            StyleFontWeight::W200,
3051            StyleFontWeight::W300,
3052            StyleFontWeight::W500,
3053            StyleFontWeight::W600,
3054            StyleFontWeight::Bold,
3055            StyleFontWeight::W800,
3056            StyleFontWeight::W900,
3057            StyleFontWeight::Lighter,
3058            StyleFontWeight::Bolder,
3059        ] {
3060            fits("font_weight", style_font_weight_to_u8(v), FONT_WEIGHT_MASK);
3061        }
3062        for v in [
3063            StyleFontStyle::Normal,
3064            StyleFontStyle::Italic,
3065            StyleFontStyle::Oblique,
3066        ] {
3067            fits("font_style", style_font_style_to_u8(v), FONT_STYLE_MASK);
3068        }
3069        for v in [
3070            StyleTextAlign::Left,
3071            StyleTextAlign::Center,
3072            StyleTextAlign::Right,
3073            StyleTextAlign::Justify,
3074            StyleTextAlign::Start,
3075            StyleTextAlign::End,
3076        ] {
3077            fits("text_align", style_text_align_to_u8(v), TEXT_ALIGN_MASK);
3078        }
3079        for v in [
3080            StyleVisibility::Visible,
3081            StyleVisibility::Hidden,
3082            StyleVisibility::Collapse,
3083        ] {
3084            fits("visibility", style_visibility_to_u8(v), VISIBILITY_MASK);
3085        }
3086        for v in [
3087            StyleWhiteSpace::Normal,
3088            StyleWhiteSpace::Pre,
3089            StyleWhiteSpace::Nowrap,
3090            StyleWhiteSpace::PreWrap,
3091            StyleWhiteSpace::PreLine,
3092            StyleWhiteSpace::BreakSpaces,
3093        ] {
3094            fits("white_space", style_white_space_to_u8(v), WHITE_SPACE_MASK);
3095        }
3096        for v in [StyleDirection::Ltr, StyleDirection::Rtl] {
3097            fits("direction", style_direction_to_u8(v), DIRECTION_MASK);
3098        }
3099        for v in [
3100            StyleVerticalAlign::Baseline,
3101            StyleVerticalAlign::Top,
3102            StyleVerticalAlign::Middle,
3103            StyleVerticalAlign::Bottom,
3104            StyleVerticalAlign::Sub,
3105            StyleVerticalAlign::Superscript,
3106            StyleVerticalAlign::TextTop,
3107            StyleVerticalAlign::TextBottom,
3108            StyleVerticalAlign::Percentage(PercentageValue::new(50.0)),
3109            StyleVerticalAlign::Length(PixelValue::px(4.0)),
3110        ] {
3111            fits("vertical_align", style_vertical_align_to_u8(v), VERTICAL_ALIGN_MASK);
3112        }
3113        for v in [StyleBorderCollapse::Separate, StyleBorderCollapse::Collapse] {
3114            fits("border_collapse", border_collapse_to_u8(v), BORDER_COLLAPSE_MASK);
3115        }
3116        // border-style gets a 4-bit nibble inside the packed u16, not a tier1 mask.
3117        for v in ALL_BORDER_STYLE {
3118            fits("border_style", border_style_to_u8(v), 0x0F);
3119        }
3120        // SizeMetric gets the low 4 bits of the pixel-value u32.
3121        for v in ALL_SIZE_METRIC {
3122            fits("size_metric", size_metric_to_u8(v), 0x0F);
3123        }
3124    }
3125
3126    #[test]
3127    fn vertical_align_percentage_and_length_collapse_to_baseline() {
3128        // Documented lossy fallback: the 3-bit tier1 slot cannot carry a
3129        // length/percentage payload, so these must encode as 0 (Baseline) and
3130        // the caller is expected to take the slow path. What must NOT happen is
3131        // an out-of-range u8 leaking into the border-collapse bit next door.
3132        for v in [
3133            StyleVerticalAlign::Percentage(PercentageValue::new(0.0)),
3134            StyleVerticalAlign::Percentage(PercentageValue::new(-100.0)),
3135            StyleVerticalAlign::Percentage(PercentageValue::new(1e9)),
3136            StyleVerticalAlign::Length(PixelValue::px(0.0)),
3137            StyleVerticalAlign::Length(PixelValue::px(-1e9)),
3138        ] {
3139            assert_eq!(style_vertical_align_to_u8(v), 0);
3140            assert_eq!(
3141                style_vertical_align_from_u8(style_vertical_align_to_u8(v)),
3142                StyleVerticalAlign::Baseline,
3143            );
3144        }
3145
3146        // …and the collapse must not disturb the neighbouring border-collapse bit.
3147        let mut t = T1::initial();
3148        t.vertical_align = StyleVerticalAlign::Percentage(PercentageValue::new(150.0));
3149        t.border_collapse = StyleBorderCollapse::Collapse;
3150        let encoded = t.encode();
3151        assert_eq!(decode_vertical_align(encoded), StyleVerticalAlign::Baseline);
3152        assert_eq!(decode_border_collapse(encoded), StyleBorderCollapse::Collapse);
3153    }
3154
3155    // =========================================================================
3156    // Tier1 u64 packing — field isolation, saturation, arbitrary input
3157    // =========================================================================
3158
3159    #[test]
3160    fn tier1_each_field_at_max_does_not_leak_into_any_other_field() {
3161        // One field at its highest variant, all others at CSS initial. If a
3162        // shift/mask pair is wrong, the extra bits land in a neighbour and that
3163        // neighbour decodes to something other than its initial value.
3164        let sat = T1::saturated();
3165        let mut cases: Vec<(&str, T1)> = Vec::new();
3166
3167        macro_rules! case {
3168            ($field:ident) => {{
3169                let mut t = T1::initial();
3170                t.$field = sat.$field;
3171                cases.push((stringify!($field), t));
3172            }};
3173        }
3174
3175        case!(display);
3176        case!(position);
3177        case!(float);
3178        case!(overflow_x);
3179        case!(overflow_y);
3180        case!(box_sizing);
3181        case!(flex_direction);
3182        case!(flex_wrap);
3183        case!(justify_content);
3184        case!(align_items);
3185        case!(align_content);
3186        case!(writing_mode);
3187        case!(clear);
3188        case!(font_weight);
3189        case!(font_style);
3190        case!(text_align);
3191        case!(visibility);
3192        case!(white_space);
3193        case!(direction);
3194        case!(vertical_align);
3195        case!(border_collapse);
3196
3197        assert_eq!(cases.len(), 21, "every tier1 field must be covered");
3198
3199        for (name, expected) in cases {
3200            let decoded = T1::decode(expected.encode());
3201            assert_eq!(
3202                decoded, expected,
3203                "{name} at its max variant leaked into another tier1 field",
3204            );
3205        }
3206    }
3207
3208    #[test]
3209    fn tier1_all_fields_saturated_roundtrips() {
3210        let sat = T1::saturated();
3211        assert_eq!(T1::decode(sat.encode()), sat);
3212        assert!(tier1_is_populated(sat.encode()));
3213    }
3214
3215    #[test]
3216    fn tier1_encode_never_touches_the_grid_bits_or_bits_above_63() {
3217        // encode_tier1 owns bits [52:0] plus the populated flag at bit 63.
3218        // Bits [62:53] belong to align-self / justify-self / grid-auto-flow /
3219        // justify-items, which the cache builder ORs in separately. If
3220        // encode_tier1 ever spills into that window it silently rewrites a grid
3221        // property that it never received as an argument.
3222        const GRID_WINDOW: u64 = 0x3FF << 53; // bits 53..=62
3223
3224        for t in [T1::initial(), T1::saturated()] {
3225            let encoded = t.encode();
3226            assert_eq!(
3227                encoded & GRID_WINDOW,
3228                0,
3229                "encode_tier1 wrote into the grid bit window [62:53]",
3230            );
3231            assert_eq!(encoded & TIER1_POPULATED_BIT, TIER1_POPULATED_BIT);
3232        }
3233
3234        // The grid fields must survive being ORed on top of a saturated tier1.
3235        let base = T1::saturated().encode();
3236        let with_grid = base
3237            | ((u64::from(layout_align_self_to_u8(LayoutAlignSelf::Baseline))) << ALIGN_SELF_SHIFT)
3238            | ((u64::from(layout_justify_self_to_u8(LayoutJustifySelf::Stretch)))
3239                << JUSTIFY_SELF_SHIFT)
3240            | ((u64::from(layout_grid_auto_flow_to_u8(LayoutGridAutoFlow::ColumnDense)))
3241                << GRID_AUTO_FLOW_SHIFT)
3242            | ((u64::from(layout_justify_items_to_u8(LayoutJustifyItems::Center)))
3243                << JUSTIFY_ITEMS_SHIFT);
3244
3245        assert_eq!(T1::decode(with_grid), T1::saturated());
3246        assert_eq!(
3247            layout_align_self_from_u8(((with_grid >> ALIGN_SELF_SHIFT) & ALIGN_SELF_MASK) as u8),
3248            LayoutAlignSelf::Baseline,
3249        );
3250        assert_eq!(
3251            layout_justify_self_from_u8(
3252                ((with_grid >> JUSTIFY_SELF_SHIFT) & JUSTIFY_SELF_MASK) as u8
3253            ),
3254            LayoutJustifySelf::Stretch,
3255        );
3256        assert_eq!(
3257            layout_grid_auto_flow_from_u8(
3258                ((with_grid >> GRID_AUTO_FLOW_SHIFT) & GRID_AUTO_FLOW_MASK) as u8
3259            ),
3260            LayoutGridAutoFlow::ColumnDense,
3261        );
3262        assert_eq!(
3263            layout_justify_items_from_u8(
3264                ((with_grid >> JUSTIFY_ITEMS_SHIFT) & JUSTIFY_ITEMS_MASK) as u8
3265            ),
3266            LayoutJustifyItems::Center,
3267        );
3268    }
3269
3270    #[test]
3271    fn tier1_zero_is_unpopulated_but_still_decodes_to_css_initial() {
3272        // A `with_capacity`-allocated cache holds 0 for every node until the
3273        // builder fills it in. Reading such a node must be safe and yield the
3274        // CSS initial value, not a garbage variant.
3275        assert!(!tier1_is_populated(0));
3276        assert_eq!(T1::decode(0), T1::initial());
3277    }
3278
3279    #[test]
3280    fn tier1_decodes_arbitrary_u64_deterministically_without_panic() {
3281        // u64::MAX means every mask reads all-ones. Each decoder must clamp to a
3282        // real variant (usually the initial value via the `_` arm) rather than
3283        // panic or produce an out-of-range discriminant.
3284        let m = u64::MAX;
3285        assert!(tier1_is_populated(m));
3286        assert_eq!(
3287            T1::decode(m),
3288            T1 {
3289                display: LayoutDisplay::Block,          // 31 → fallback
3290                position: LayoutPosition::Static,       // 7  → fallback
3291                float: LayoutFloat::None,               // 3  → fallback
3292                overflow_x: LayoutOverflow::Visible,    // 7  → fallback
3293                overflow_y: LayoutOverflow::Visible,    // 7  → fallback
3294                box_sizing: LayoutBoxSizing::BorderBox, // 1  → real variant
3295                flex_direction: LayoutFlexDirection::ColumnReverse, // 3 → real
3296                flex_wrap: LayoutFlexWrap::NoWrap,      // 3  → fallback
3297                justify_content: LayoutJustifyContent::SpaceEvenly, // 7 → real
3298                align_items: LayoutAlignItems::Stretch, // 7  → fallback
3299                align_content: LayoutAlignContent::Stretch, // 7 → fallback
3300                writing_mode: LayoutWritingMode::HorizontalTb, // 3 → fallback
3301                clear: LayoutClear::Both,               // 3  → real variant
3302                font_weight: StyleFontWeight::Normal,   // 15 → fallback
3303                font_style: StyleFontStyle::Normal,     // 3  → fallback
3304                text_align: StyleTextAlign::Start,      // 7  → fallback
3305                visibility: StyleVisibility::Visible,   // 3  → fallback
3306                white_space: StyleWhiteSpace::Normal,   // 7  → fallback
3307                direction: StyleDirection::Rtl,         // 1  → real variant
3308                vertical_align: StyleVerticalAlign::TextBottom, // 7 → real
3309                border_collapse: StyleBorderCollapse::Collapse, // 1 → real
3310            },
3311        );
3312
3313        // A deterministic sweep of adversarial bit patterns: none may panic.
3314        let mut x: u64 = 0x9E37_79B9_7F4A_7C15;
3315        for _ in 0..4096 {
3316            let _ = T1::decode(x);
3317            let _ = tier1_is_populated(x);
3318            x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
3319        }
3320        for x in [0u64, 1, u64::MAX, 0xAAAA_AAAA_AAAA_AAAA, 0x5555_5555_5555_5555] {
3321            let _ = T1::decode(x);
3322        }
3323    }
3324
3325    // =========================================================================
3326    // Packed border styles (u16, four nibbles)
3327    // =========================================================================
3328
3329    #[test]
3330    fn border_styles_packed_roundtrip_for_all_10000_combinations() {
3331        for top in ALL_BORDER_STYLE {
3332            for right in ALL_BORDER_STYLE {
3333                for bottom in ALL_BORDER_STYLE {
3334                    for left in ALL_BORDER_STYLE {
3335                        let packed = encode_border_styles_packed(top, right, bottom, left);
3336                        assert_eq!(decode_border_top_style(packed), top);
3337                        assert_eq!(decode_border_right_style(packed), right);
3338                        assert_eq!(decode_border_bottom_style(packed), bottom);
3339                        assert_eq!(decode_border_left_style(packed), left);
3340                    }
3341                }
3342            }
3343        }
3344    }
3345
3346    #[test]
3347    fn border_styles_packed_nibbles_do_not_alias() {
3348        // Only the top nibble set — the other three must read as None (0),
3349        // not pick up bits from their neighbours.
3350        let packed = encode_border_styles_packed(
3351            BorderStyle::Outset, // 9 — the widest valid nibble
3352            BorderStyle::None,
3353            BorderStyle::None,
3354            BorderStyle::None,
3355        );
3356        assert_eq!(packed, 0x0009);
3357        assert_eq!(decode_border_top_style(packed), BorderStyle::Outset);
3358        assert_eq!(decode_border_right_style(packed), BorderStyle::None);
3359        assert_eq!(decode_border_bottom_style(packed), BorderStyle::None);
3360        assert_eq!(decode_border_left_style(packed), BorderStyle::None);
3361
3362        let packed = encode_border_styles_packed(
3363            BorderStyle::None,
3364            BorderStyle::None,
3365            BorderStyle::None,
3366            BorderStyle::Outset,
3367        );
3368        assert_eq!(packed, 0x9000);
3369        assert_eq!(decode_border_left_style(packed), BorderStyle::Outset);
3370        assert_eq!(decode_border_top_style(packed), BorderStyle::None);
3371    }
3372
3373    #[test]
3374    fn border_styles_packed_decodes_garbage_u16_without_panic() {
3375        // Nibbles 10..=15 have no variant. `0xFFFF` (an all-ones cold-tier row,
3376        // e.g. from a misinitialised buffer) must decode to None everywhere.
3377        for packed in [0u16, 0xFFFF, 0xAAAA, 0x5555, u16::MAX / 2] {
3378            let _ = decode_border_top_style(packed);
3379            let _ = decode_border_right_style(packed);
3380            let _ = decode_border_bottom_style(packed);
3381            let _ = decode_border_left_style(packed);
3382        }
3383        assert_eq!(decode_border_top_style(0xFFFF), BorderStyle::None);
3384        assert_eq!(decode_border_right_style(0xFFFF), BorderStyle::None);
3385        assert_eq!(decode_border_bottom_style(0xFFFF), BorderStyle::None);
3386        assert_eq!(decode_border_left_style(0xFFFF), BorderStyle::None);
3387        // Exhaustive: no u16 may panic any of the four decoders.
3388        for packed in 0..=u16::MAX {
3389            let _ = decode_border_top_style(packed);
3390            let _ = decode_border_left_style(packed);
3391        }
3392    }
3393
3394    // =========================================================================
3395    // Colors (u32 0xRRGGBBAA)
3396    // =========================================================================
3397
3398    #[test]
3399    fn color_u32_channel_order_is_rrggbbaa() {
3400        let c = ColorU {
3401            r: 0x12,
3402            g: 0x34,
3403            b: 0x56,
3404            a: 0x78,
3405        };
3406        assert_eq!(encode_color_u32(&c), 0x1234_5678);
3407        assert_eq!(decode_color_u32(0x1234_5678), Some(c));
3408    }
3409
3410    #[test]
3411    fn color_u32_roundtrips_every_boundary_channel_combination() {
3412        for r in [0u8, 1, 127, 254, 255] {
3413            for g in [0u8, 1, 127, 254, 255] {
3414                for b in [0u8, 1, 127, 254, 255] {
3415                    for a in [0u8, 1, 127, 254, 255] {
3416                        let c = ColorU { r, g, b, a };
3417                        let encoded = encode_color_u32(&c);
3418                        if encoded == 0 {
3419                            // Only fully-transparent black hits the unset sentinel.
3420                            assert_eq!((r, g, b, a), (0, 0, 0, 0));
3421                            assert_eq!(decode_color_u32(encoded), None);
3422                        } else {
3423                            assert_eq!(decode_color_u32(encoded), Some(c));
3424                        }
3425                    }
3426                }
3427            }
3428        }
3429    }
3430
3431    #[test]
3432    fn color_u32_transparent_black_is_the_documented_unset_collision() {
3433        // Documented limitation, pinned so it cannot regress silently:
3434        // rgba(0,0,0,0) is indistinguishable from "property never set".
3435        let transparent_black = ColorU {
3436            r: 0,
3437            g: 0,
3438            b: 0,
3439            a: 0,
3440        };
3441        assert_eq!(encode_color_u32(&transparent_black), 0);
3442        assert_eq!(decode_color_u32(0), None);
3443
3444        // Every other alpha-0 color must still survive the round-trip.
3445        let transparent_red = ColorU {
3446            r: 255,
3447            g: 0,
3448            b: 0,
3449            a: 0,
3450        };
3451        assert_eq!(encode_color_u32(&transparent_red), 0xFF00_0000);
3452        assert_eq!(decode_color_u32(0xFF00_0000), Some(transparent_red));
3453
3454        // …including "black but only just" (alpha 1).
3455        let almost = ColorU {
3456            r: 0,
3457            g: 0,
3458            b: 0,
3459            a: 1,
3460        };
3461        assert_eq!(decode_color_u32(encode_color_u32(&almost)), Some(almost));
3462    }
3463
3464    #[test]
3465    fn color_u32_max_decodes_to_opaque_white() {
3466        assert_eq!(
3467            decode_color_u32(u32::MAX),
3468            Some(ColorU {
3469                r: 255,
3470                g: 255,
3471                b: 255,
3472                a: 255
3473            }),
3474        );
3475    }
3476
3477    // =========================================================================
3478    // PixelValue u32 (4-bit metric + 28-bit signed fixed-point ×1000)
3479    // =========================================================================
3480
3481    #[test]
3482    fn pixel_value_u32_sentinels_all_decode_to_none_and_are_distinct() {
3483        let sentinels = [
3484            U32_SENTINEL,
3485            U32_AUTO,
3486            U32_NONE,
3487            U32_INHERIT,
3488            U32_INITIAL,
3489            U32_MIN_CONTENT,
3490            U32_MAX_CONTENT,
3491        ];
3492        for (i, a) in sentinels.iter().enumerate() {
3493            assert!(
3494                *a >= U32_SENTINEL_THRESHOLD,
3495                "sentinel {a:#X} sits below the threshold and would decode as a value",
3496            );
3497            assert_eq!(decode_pixel_value_u32(*a), None);
3498            for b in &sentinels[i + 1..] {
3499                assert_ne!(a, b, "two u32 sentinels share a bit pattern");
3500            }
3501        }
3502        // The threshold itself is the lowest reserved value.
3503        assert_eq!(decode_pixel_value_u32(U32_SENTINEL_THRESHOLD), None);
3504        // One below the threshold is still a real (negative) value.
3505        assert!(decode_pixel_value_u32(U32_SENTINEL_THRESHOLD - 1).is_some());
3506    }
3507
3508    #[test]
3509    fn pixel_value_u32_roundtrips_at_the_28_bit_boundaries_for_every_metric() {
3510        // ±2^27 is the documented edge of the 28-bit signed fixed-point field.
3511        for metric in ALL_SIZE_METRIC {
3512            for raw in [0isize, 1, -1, 1000, -1000, 134_217_727, -134_217_728] {
3513                let pv = pv_raw(metric, raw);
3514                let encoded = encode_pixel_value_u32(&pv);
3515
3516                // Raw -1 with a high metric nibble collides with the sentinel
3517                // band; that is asserted separately in the bug test below.
3518                if encoded >= U32_SENTINEL_THRESHOLD {
3519                    continue;
3520                }
3521
3522                let decoded = decode_pixel_value_u32(encoded)
3523                    .unwrap_or_else(|| panic!("{metric:?} raw {raw} decoded as a sentinel"));
3524                assert_eq!(decoded.metric, metric, "metric nibble lost for raw {raw}");
3525                assert_eq!(
3526                    decoded.number.number(),
3527                    raw,
3528                    "{metric:?}: raw {raw} did not survive the round-trip",
3529                );
3530            }
3531        }
3532    }
3533
3534    #[test]
3535    fn pixel_value_u32_out_of_28_bit_range_returns_sentinel() {
3536        for metric in ALL_SIZE_METRIC {
3537            for raw in [
3538                134_217_728isize,
3539                -134_217_729,
3540                1_000_000_000,
3541                -1_000_000_000,
3542                isize::MAX,
3543                isize::MIN,
3544            ] {
3545                assert_eq!(
3546                    encode_pixel_value_u32(&pv_raw(metric, raw)),
3547                    U32_SENTINEL,
3548                    "{metric:?}: raw {raw} is outside 28 bits and must escape to tier 3",
3549                );
3550            }
3551        }
3552    }
3553
3554    #[test]
3555    fn pixel_value_u32_decodes_every_low_bit_pattern_without_panic() {
3556        // Metric nibbles 12..=15 have no SizeMetric — they must clamp to Px.
3557        for nibble in 12u32..16 {
3558            let encoded = (1u32 << 4) | nibble;
3559            let decoded = decode_pixel_value_u32(encoded).unwrap();
3560            assert_eq!(decoded.metric, SizeMetric::Px);
3561            assert_eq!(decoded.number.number(), 1);
3562        }
3563        // Sign extension: the top bit of the 28-bit field must arithmetic-shift.
3564        let neg = decode_pixel_value_u32(0x8000_0000).unwrap();
3565        assert_eq!(neg.metric, SizeMetric::Px);
3566        assert_eq!(neg.number.number(), -134_217_728);
3567    }
3568
3569    #[test]
3570    fn pixel_value_u32_negative_0_001_in_vh_vmin_vmax_collides_with_sentinels() {
3571        // BUG (encode_pixel_value_u32 / decode_pixel_value_u32):
3572        //
3573        // raw == -1 (i.e. -0.001 of a unit) sign-extends to 0xFFFF_FFFF, and
3574        // `<< 4` leaves 0xFFFF_FFF0. ORing in a metric nibble >= 9 pushes the
3575        // word into the reserved sentinel band (>= 0xFFFF_FFF9):
3576        //
3577        //   -0.001vh   → 0xFFFF_FFF9 == U32_MAX_CONTENT
3578        //   -0.001vmin → 0xFFFF_FFFA == U32_MIN_CONTENT
3579        //   -0.001vmax → 0xFFFF_FFFB == U32_INITIAL
3580        //
3581        // So a legal (if tiny) negative viewport-relative length is written into
3582        // the cache as `max-content` / `min-content` / `initial`, and the decoder
3583        // reports None (unset) instead of the value. The encoder's range check
3584        // guards the 28-bit magnitude but not the sentinel band it lands in.
3585        //
3586        // Fix would be to reject any encoding that lands >= U32_SENTINEL_THRESHOLD
3587        // and escape to tier 3 instead.
3588        // FIXED (as this test's own comment prescribed): a raw -1 with a high metric
3589        // nibble packs into the reserved sentinel band, so the encoder now ESCAPES it to
3590        // U32_SENTINEL (tier 3) rather than emitting a value that decodes as a wrong,
3591        // aliased sentinel. decode() therefore returns None ("not in the fast cache —
3592        // look in tier 3"), which is the safe, non-aliasing outcome.
3593        for metric in [SizeMetric::Vh, SizeMetric::Vmin, SizeMetric::Vmax] {
3594            let pv = pv_raw(metric, -1);
3595            let encoded = encode_pixel_value_u32(&pv);
3596            assert_eq!(encoded, U32_SENTINEL, "{metric:?}: raw -1 must escape to tier 3");
3597            assert_eq!(
3598                decode_pixel_value_u32(encoded),
3599                None,
3600                "{metric:?}: an escaped value decodes as None, never an aliased sentinel",
3601            );
3602        }
3603    }
3604
3605    // =========================================================================
3606    // Resolved px i16 (×10)
3607    // =========================================================================
3608
3609    #[test]
3610    fn resolved_px_i16_nan_and_infinity_are_defined_and_do_not_panic() {
3611        // `f32 as i32` saturates: NaN → 0, +inf → i32::MAX, -inf → i32::MIN.
3612        assert_eq!(encode_resolved_px_i16(f32::NAN), 0);
3613        assert_eq!(encode_resolved_px_i16(-f32::NAN), 0);
3614        assert_eq!(encode_resolved_px_i16(f32::INFINITY), I16_SENTINEL);
3615        assert_eq!(encode_resolved_px_i16(f32::NEG_INFINITY), I16_SENTINEL);
3616        assert_eq!(encode_resolved_px_i16(f32::MAX), I16_SENTINEL);
3617        assert_eq!(encode_resolved_px_i16(f32::MIN), I16_SENTINEL);
3618        // Subnormals round to zero rather than blowing up.
3619        assert_eq!(encode_resolved_px_i16(f32::MIN_POSITIVE), 0);
3620        assert_eq!(encode_resolved_px_i16(-0.0), 0);
3621    }
3622
3623    #[test]
3624    fn resolved_px_i16_saturates_exactly_at_the_documented_range() {
3625        // Doc: -3276.8 ..= +3276.3 px at 0.1px precision.
3626        assert_eq!(encode_resolved_px_i16(3276.3), 32763);
3627        assert_eq!(encode_resolved_px_i16(-3276.8), -32768);
3628
3629        // One tick outside in either direction escapes to tier 3.
3630        assert_eq!(encode_resolved_px_i16(3276.4), I16_SENTINEL);
3631        assert_eq!(encode_resolved_px_i16(-3276.9), I16_SENTINEL);
3632        assert_eq!(encode_resolved_px_i16(1e9), I16_SENTINEL);
3633        assert_eq!(encode_resolved_px_i16(-1e9), I16_SENTINEL);
3634    }
3635
3636    #[test]
3637    fn resolved_px_i16_sentinels_decode_to_none_and_are_distinct() {
3638        let sentinels = [I16_SENTINEL, I16_AUTO, I16_INHERIT, I16_INITIAL];
3639        for (i, a) in sentinels.iter().enumerate() {
3640            assert!(*a >= I16_SENTINEL_THRESHOLD);
3641            assert_eq!(decode_resolved_px_i16(*a), None);
3642            for b in &sentinels[i + 1..] {
3643                assert_ne!(a, b, "two i16 sentinels share a bit pattern");
3644            }
3645        }
3646        assert_eq!(decode_resolved_px_i16(I16_SENTINEL_THRESHOLD), None);
3647        assert_eq!(decode_resolved_px_i16(I16_SENTINEL_THRESHOLD - 1), Some(3276.3));
3648    }
3649
3650    #[test]
3651    fn resolved_px_i16_every_non_sentinel_value_roundtrips() {
3652        // Exhaustive over the whole non-sentinel i16 domain: decode → encode must
3653        // be the identity, or a value written by one frame reads back shifted on
3654        // the next.
3655        for v in i16::MIN..I16_SENTINEL_THRESHOLD {
3656            let px = decode_resolved_px_i16(v)
3657                .unwrap_or_else(|| panic!("{v} is below the threshold but decoded as a sentinel"));
3658            assert_eq!(
3659                encode_resolved_px_i16(px),
3660                v,
3661                "i16 {v} decoded to {px} px which re-encodes to a different i16",
3662            );
3663        }
3664    }
3665
3666    // =========================================================================
3667    // Flex u16 (×100)
3668    // =========================================================================
3669
3670    #[test]
3671    fn flex_u16_nan_infinity_and_negatives_are_defined_and_do_not_panic() {
3672        assert_eq!(encode_flex_u16(f32::NAN), 0);
3673        assert_eq!(encode_flex_u16(f32::INFINITY), U16_SENTINEL);
3674        assert_eq!(encode_flex_u16(f32::NEG_INFINITY), U16_SENTINEL);
3675        assert_eq!(encode_flex_u16(f32::MAX), U16_SENTINEL);
3676        // flex-grow/shrink are non-negative; a negative escapes to tier 3.
3677        assert_eq!(encode_flex_u16(-1.0), U16_SENTINEL);
3678        assert_eq!(encode_flex_u16(-0.01), U16_SENTINEL);
3679        // …but -0.0 and values that round to zero clamp to 0, not to a sentinel.
3680        assert_eq!(encode_flex_u16(-0.0), 0);
3681        assert_eq!(encode_flex_u16(0.0), 0);
3682    }
3683
3684    #[test]
3685    fn flex_u16_saturates_exactly_at_the_documented_range() {
3686        // Doc: 0.00 ..= 655.27 at 0.01 precision.
3687        assert_eq!(encode_flex_u16(655.27), 65527);
3688        assert_eq!(decode_flex_u16(65527), Some(655.27));
3689        // 65528 is representable but 65529 is the threshold.
3690        assert_eq!(encode_flex_u16(655.28), 65528);
3691        assert_eq!(encode_flex_u16(655.29), U16_SENTINEL);
3692        assert_eq!(encode_flex_u16(1e9), U16_SENTINEL);
3693    }
3694
3695    #[test]
3696    fn flex_u16_sentinel_band_decodes_to_none() {
3697        for v in U16_SENTINEL_THRESHOLD..=u16::MAX {
3698            assert_eq!(decode_flex_u16(v), None, "u16 {v} is reserved and must decode as None");
3699        }
3700        assert_eq!(U16_SENTINEL, u16::MAX);
3701        assert!(decode_flex_u16(U16_SENTINEL_THRESHOLD - 1).is_some());
3702    }
3703
3704    #[test]
3705    fn flex_u16_every_non_sentinel_value_roundtrips() {
3706        for v in 0..U16_SENTINEL_THRESHOLD {
3707            let f = decode_flex_u16(v).unwrap_or_else(|| panic!("{v} decoded as a sentinel"));
3708            assert_eq!(encode_flex_u16(f), v, "u16 {v} → {f} → re-encoded differently");
3709        }
3710    }
3711
3712    // =========================================================================
3713    // encode_css_pixel_as_i16 — CssPropertyValue → i16 keyword sentinels
3714    // =========================================================================
3715
3716    #[test]
3717    fn css_pixel_as_i16_maps_every_keyword_to_its_own_sentinel() {
3718        assert_eq!(
3719            encode_css_pixel_as_i16(&CssPropertyValue::Auto),
3720            I16_AUTO,
3721        );
3722        assert_eq!(
3723            encode_css_pixel_as_i16(&CssPropertyValue::Initial),
3724            I16_INITIAL,
3725        );
3726        assert_eq!(
3727            encode_css_pixel_as_i16(&CssPropertyValue::Inherit),
3728            I16_INHERIT,
3729        );
3730        // None / Revert / Unset have no dedicated slot → generic sentinel (slow path).
3731        assert_eq!(
3732            encode_css_pixel_as_i16(&CssPropertyValue::<PixelValue>::None),
3733            I16_SENTINEL,
3734        );
3735        assert_eq!(
3736            encode_css_pixel_as_i16(&CssPropertyValue::<PixelValue>::Revert),
3737            I16_SENTINEL,
3738        );
3739        assert_eq!(
3740            encode_css_pixel_as_i16(&CssPropertyValue::<PixelValue>::Unset),
3741            I16_SENTINEL,
3742        );
3743    }
3744
3745    #[test]
3746    fn css_pixel_as_i16_only_takes_the_fast_path_for_absolute_px() {
3747        // Only SizeMetric::Px can be pre-resolved without layout context;
3748        // every relative unit must escape to the cascade.
3749        assert_eq!(
3750            encode_css_pixel_as_i16(&CssPropertyValue::Exact(PixelValue::px(12.5))),
3751            125,
3752        );
3753        for metric in ALL_SIZE_METRIC {
3754            if metric == SizeMetric::Px {
3755                continue;
3756            }
3757            assert_eq!(
3758                encode_css_pixel_as_i16(&CssPropertyValue::Exact(pv_raw(metric, 12_500))),
3759                I16_SENTINEL,
3760                "{metric:?} needs resolution context and must not be pre-resolved",
3761            );
3762        }
3763    }
3764
3765    #[test]
3766    fn css_pixel_as_i16_out_of_range_px_escapes_to_the_sentinel() {
3767        assert_eq!(
3768            encode_css_pixel_as_i16(&CssPropertyValue::Exact(PixelValue::px(1e6))),
3769            I16_SENTINEL,
3770        );
3771        assert_eq!(
3772            encode_css_pixel_as_i16(&CssPropertyValue::Exact(PixelValue::px(-1e6))),
3773            I16_SENTINEL,
3774        );
3775        // A px value that happens to land on a keyword sentinel would be
3776        // misread as `auto`/`inherit`; the range check must exclude the band.
3777        for raw in [I16_SENTINEL, I16_AUTO, I16_INHERIT, I16_INITIAL] {
3778            let px = f32::from(raw) / 10.0;
3779            assert_eq!(
3780                encode_css_pixel_as_i16(&CssPropertyValue::Exact(PixelValue::px(px))),
3781                I16_SENTINEL,
3782                "{px} px must not silently encode as the keyword sentinel {raw}",
3783            );
3784        }
3785    }
3786
3787    // =========================================================================
3788    // CompactLayoutCache — constructor invariants, bounds, predicates
3789    // =========================================================================
3790
3791    #[test]
3792    fn empty_cache_is_the_neutral_element() {
3793        let c = CompactLayoutCache::empty();
3794        assert_eq!(c.node_count(), 0);
3795        assert!(c.tier1_enums.is_empty());
3796        assert!(c.tier2_dims.is_empty());
3797        assert!(c.tier2_cold.is_empty());
3798        assert!(c.tier2b_text.is_empty());
3799        assert!(c.font_dirty_nodes.is_empty());
3800        assert!(c.prev_font_hashes.is_empty());
3801        assert!(c.font_hash_to_families.is_empty());
3802        assert_eq!(c.dom_declared_flags, 0);
3803        // with_capacity(0) must produce exactly the same thing.
3804        assert_eq!(CompactLayoutCache::with_capacity(0), c);
3805    }
3806
3807    #[test]
3808    fn with_capacity_keeps_every_tier_the_same_length() {
3809        for n in [0usize, 1, 2, 3, 17, 1024] {
3810            let c = CompactLayoutCache::with_capacity(n);
3811            assert_eq!(c.node_count(), n);
3812            assert_eq!(c.tier1_enums.len(), n);
3813            assert_eq!(c.tier2_dims.len(), n);
3814            assert_eq!(c.tier2_cold.len(), n);
3815            assert_eq!(c.tier2b_text.len(), n);
3816            assert_eq!(c.prev_font_hashes.len(), n);
3817            // Dirty list starts empty regardless of node count.
3818            assert!(c.font_dirty_nodes.is_empty());
3819            assert_eq!(c.dom_declared_flags, 0);
3820        }
3821    }
3822
3823    #[test]
3824    fn freshly_allocated_node_reads_back_every_css_initial_value() {
3825        // `with_capacity` zeroes tier1 and default-fills tier2. A node the
3826        // builder never touched must still answer every getter with the CSS
3827        // initial value — this is what lets the builder skip unset properties.
3828        let c = CompactLayoutCache::with_capacity(3);
3829        for i in 0..3 {
3830            assert_eq!(T1::decode(c.tier1_enums[i]), T1::initial());
3831            assert_eq!(c.get_display(i), LayoutDisplay::Block);
3832            assert_eq!(c.get_position(i), LayoutPosition::Static);
3833            assert_eq!(c.get_float(i), LayoutFloat::None);
3834            assert_eq!(c.get_overflow_x(i), LayoutOverflow::Visible);
3835            assert_eq!(c.get_overflow_y(i), LayoutOverflow::Visible);
3836            assert_eq!(c.get_box_sizing(i), LayoutBoxSizing::ContentBox);
3837            assert_eq!(c.get_flex_direction(i), LayoutFlexDirection::Row);
3838            assert_eq!(c.get_flex_wrap(i), LayoutFlexWrap::NoWrap);
3839            assert_eq!(c.get_justify_content(i), LayoutJustifyContent::FlexStart);
3840            assert_eq!(c.get_align_items(i), LayoutAlignItems::Stretch);
3841            assert_eq!(c.get_align_content(i), LayoutAlignContent::Stretch);
3842            assert_eq!(c.get_writing_mode(i), LayoutWritingMode::HorizontalTb);
3843            assert_eq!(c.get_clear(i), LayoutClear::None);
3844            assert_eq!(c.get_font_weight(i), StyleFontWeight::Normal);
3845            assert_eq!(c.get_font_style(i), StyleFontStyle::Normal);
3846            assert_eq!(c.get_text_align(i), StyleTextAlign::Start);
3847            assert_eq!(c.get_visibility(i), StyleVisibility::Visible);
3848            assert_eq!(c.get_white_space(i), StyleWhiteSpace::Normal);
3849            assert_eq!(c.get_direction(i), StyleDirection::Ltr);
3850            assert_eq!(c.get_vertical_align(i), StyleVerticalAlign::Baseline);
3851            assert_eq!(c.get_border_collapse(i), StyleBorderCollapse::Separate);
3852
3853            // Dimensions: auto / none, i.e. no decodable pixel value.
3854            assert_eq!(c.get_width_raw(i), U32_AUTO);
3855            assert_eq!(c.get_height_raw(i), U32_AUTO);
3856            assert_eq!(c.get_min_width_raw(i), U32_AUTO);
3857            assert_eq!(c.get_min_height_raw(i), U32_AUTO);
3858            assert_eq!(c.get_max_width_raw(i), U32_NONE);
3859            assert_eq!(c.get_max_height_raw(i), U32_NONE);
3860            assert_eq!(c.get_flex_basis_raw(i), U32_AUTO);
3861            assert_eq!(c.get_font_size_raw(i), U32_INITIAL);
3862            assert_eq!(decode_pixel_value_u32(c.get_width_raw(i)), None);
3863            assert_eq!(decode_pixel_value_u32(c.get_font_size_raw(i)), None);
3864
3865            // Box model: zeros are real values (Some), offsets are auto (raw sentinel).
3866            assert_eq!(c.get_padding_top(i), Some(0.0));
3867            assert_eq!(c.get_padding_right(i), Some(0.0));
3868            assert_eq!(c.get_padding_bottom(i), Some(0.0));
3869            assert_eq!(c.get_padding_left(i), Some(0.0));
3870            assert_eq!(c.get_border_top_width(i), Some(0.0));
3871            assert_eq!(c.get_border_left_width(i), Some(0.0));
3872            // margin defaults to 0, NOT auto — centering must not kick in for free.
3873            assert_eq!(c.get_margin_top(i), Some(0.0));
3874            assert_eq!(c.get_margin_left(i), Some(0.0));
3875            assert!(!c.is_margin_top_auto(i));
3876            assert!(!c.is_margin_right_auto(i));
3877            assert!(!c.is_margin_bottom_auto(i));
3878            assert!(!c.is_margin_left_auto(i));
3879            // …but the inset properties DO default to auto.
3880            assert_eq!(c.get_top(i), I16_AUTO);
3881            assert_eq!(c.get_right(i), I16_AUTO);
3882            assert_eq!(c.get_bottom(i), I16_AUTO);
3883            assert_eq!(c.get_left(i), I16_AUTO);
3884
3885            // Flex: grow 0, shrink 1 (the CSS defaults).
3886            assert_eq!(c.get_flex_grow(i), Some(0.0));
3887            assert_eq!(c.get_flex_shrink(i), Some(1.0));
3888
3889            // Cold tier.
3890            assert_eq!(c.get_z_index(i), I16_AUTO);
3891            assert_eq!(c.get_border_styles_packed(i), 0);
3892            assert_eq!(c.get_border_top_style(i), BorderStyle::None);
3893            assert_eq!(c.get_border_right_style(i), BorderStyle::None);
3894            assert_eq!(c.get_border_bottom_style(i), BorderStyle::None);
3895            assert_eq!(c.get_border_left_style(i), BorderStyle::None);
3896            assert_eq!(c.get_border_top_color_raw(i), 0);
3897            assert_eq!(decode_color_u32(c.get_border_top_color_raw(i)), None);
3898            assert_eq!(c.get_border_top_left_radius_raw(i), I16_SENTINEL);
3899            assert_eq!(c.get_tab_size_raw(i), I16_SENTINEL);
3900            assert_eq!(c.get_border_spacing_h_raw(i), 0);
3901            assert_eq!(c.get_border_spacing_v_raw(i), 0);
3902            assert_eq!(c.get_opacity_raw(i), OPACITY_SENTINEL);
3903            assert_eq!(c.get_hot_flags(i), 0);
3904            assert_eq!(c.get_scrollbar_gutter_bits(i), SCROLLBAR_GUTTER_AUTO);
3905
3906            // Every "has this rare prop" predicate must be false on a fresh node,
3907            // otherwise the fast path would take a cascade walk for every node.
3908            assert!(!c.has_transform(i));
3909            assert!(!c.has_transform_origin(i));
3910            assert!(!c.has_box_shadow(i));
3911            assert!(!c.has_text_decoration(i));
3912            assert!(!c.has_background(i));
3913            assert!(!c.has_clip_path(i));
3914            assert!(!c.has_scrollbar_css(i));
3915            assert!(!c.has_counter(i));
3916            assert!(!c.has_break(i));
3917            assert!(!c.has_text_orientation(i));
3918            assert!(!c.has_text_shadow(i));
3919            assert!(!c.has_backdrop_filter(i));
3920            assert!(!c.has_filter(i));
3921            assert!(!c.has_mix_blend_mode(i));
3922
3923            // Text tier.
3924            assert_eq!(c.get_text_color_raw(i), 0);
3925            assert_eq!(c.get_font_family_hash(i), 0);
3926            assert_eq!(c.get_line_height(i), None); // "normal" → slow path
3927            assert_eq!(c.get_letter_spacing(i), Some(0.0));
3928            assert_eq!(c.get_word_spacing(i), Some(0.0));
3929            assert_eq!(c.get_text_indent(i), Some(0.0));
3930        }
3931    }
3932
3933    #[test]
3934    fn hot_flag_predicates_read_only_their_own_bit() {
3935        let flags = [
3936            ("transform", HOT_FLAG_HAS_TRANSFORM),
3937            ("transform_origin", HOT_FLAG_HAS_TRANSFORM_ORIGIN),
3938            ("box_shadow", HOT_FLAG_HAS_BOX_SHADOW),
3939            ("text_decoration", HOT_FLAG_HAS_TEXT_DECORATION),
3940            ("background", HOT_FLAG_HAS_BACKGROUND),
3941            ("clip_path", HOT_FLAG_HAS_CLIP_PATH),
3942        ];
3943
3944        for (name, bit) in flags {
3945            let mut c = CompactLayoutCache::with_capacity(1);
3946            c.tier2_cold[0].hot_flags = bit;
3947
3948            let observed = [
3949                ("transform", c.has_transform(0)),
3950                ("transform_origin", c.has_transform_origin(0)),
3951                ("box_shadow", c.has_box_shadow(0)),
3952                ("text_decoration", c.has_text_decoration(0)),
3953                ("background", c.has_background(0)),
3954                ("clip_path", c.has_clip_path(0)),
3955            ];
3956            for (other, is_set) in observed {
3957                assert_eq!(
3958                    is_set,
3959                    other == name,
3960                    "hot_flags = {bit:#010b}: has_{other}() should be {}",
3961                    other == name,
3962                );
3963            }
3964            // The gutter field lives in bits 4-5 and must be unaffected.
3965            assert_eq!(c.get_scrollbar_gutter_bits(0), SCROLLBAR_GUTTER_AUTO);
3966        }
3967
3968        // No two hot flags may share a bit, and none may overlap the gutter field.
3969        let mut seen = 0u8;
3970        for (_, bit) in flags {
3971            assert_eq!(seen & bit, 0, "two hot flags share bit {bit:#010b}");
3972            assert_eq!(
3973                bit & HOT_FLAG_SCROLLBAR_GUTTER_MASK,
3974                0,
3975                "hot flag {bit:#010b} overlaps the scrollbar-gutter field",
3976            );
3977            seen |= bit;
3978        }
3979    }
3980
3981    #[test]
3982    fn scrollbar_gutter_bits_survive_a_fully_set_hot_flags_byte() {
3983        for gutter in [
3984            SCROLLBAR_GUTTER_AUTO,
3985            SCROLLBAR_GUTTER_STABLE,
3986            SCROLLBAR_GUTTER_BOTH_EDGES,
3987            SCROLLBAR_GUTTER_MIRROR,
3988        ] {
3989            let mut c = CompactLayoutCache::with_capacity(1);
3990            // Every boolean flag set *and* a gutter value: the gutter must still
3991            // read back cleanly out of the middle of the byte.
3992            c.tier2_cold[0].hot_flags = HOT_FLAG_HAS_TRANSFORM
3993                | HOT_FLAG_HAS_TRANSFORM_ORIGIN
3994                | HOT_FLAG_HAS_BOX_SHADOW
3995                | HOT_FLAG_HAS_TEXT_DECORATION
3996                | HOT_FLAG_HAS_BACKGROUND
3997                | HOT_FLAG_HAS_CLIP_PATH
3998                | (gutter << HOT_FLAG_SCROLLBAR_GUTTER_SHIFT);
3999
4000            assert_eq!(c.get_scrollbar_gutter_bits(0), gutter);
4001            assert!(c.has_transform(0));
4002            assert!(c.has_clip_path(0));
4003        }
4004
4005        // An all-ones byte reads the max gutter value, never something out of range.
4006        let mut c = CompactLayoutCache::with_capacity(1);
4007        c.tier2_cold[0].hot_flags = u8::MAX;
4008        assert_eq!(c.get_scrollbar_gutter_bits(0), SCROLLBAR_GUTTER_MIRROR);
4009        assert!(c.get_scrollbar_gutter_bits(0) <= 3);
4010    }
4011
4012    #[test]
4013    fn extra_flag_predicates_read_only_their_own_bit() {
4014        let flags = [
4015            ("scrollbar_css", EXTRA_FLAG_HAS_SCROLLBAR_CSS),
4016            ("counter", EXTRA_FLAG_HAS_COUNTER),
4017            ("break", EXTRA_FLAG_HAS_BREAK),
4018            ("text_orientation", EXTRA_FLAG_HAS_TEXT_ORIENTATION),
4019            ("text_shadow", EXTRA_FLAG_HAS_TEXT_SHADOW),
4020            ("backdrop_filter", EXTRA_FLAG_HAS_BACKDROP_FILTER),
4021            ("filter", EXTRA_FLAG_HAS_FILTER),
4022            ("mix_blend_mode", EXTRA_FLAG_HAS_MIX_BLEND_MODE),
4023        ];
4024
4025        // All 8 bits must be distinct and together cover the whole byte.
4026        let mut seen = 0u8;
4027        for (_, bit) in flags {
4028            assert_eq!(seen & bit, 0, "two extra flags share bit {bit:#010b}");
4029            seen |= bit;
4030        }
4031        assert_eq!(seen, u8::MAX);
4032
4033        for (name, bit) in flags {
4034            let mut c = CompactLayoutCache::with_capacity(1);
4035            c.tier2_cold[0].extra_flags = bit;
4036            let observed = [
4037                ("scrollbar_css", c.has_scrollbar_css(0)),
4038                ("counter", c.has_counter(0)),
4039                ("break", c.has_break(0)),
4040                ("text_orientation", c.has_text_orientation(0)),
4041                ("text_shadow", c.has_text_shadow(0)),
4042                ("backdrop_filter", c.has_backdrop_filter(0)),
4043                ("filter", c.has_filter(0)),
4044                ("mix_blend_mode", c.has_mix_blend_mode(0)),
4045            ];
4046            for (other, is_set) in observed {
4047                assert_eq!(
4048                    is_set,
4049                    other == name,
4050                    "extra_flags = {bit:#010b}: has_{other}() should be {}",
4051                    other == name,
4052                );
4053            }
4054            // Setting an extra flag must not make any hot-flag predicate fire.
4055            assert!(!c.has_transform(0));
4056            assert!(!c.has_background(0));
4057        }
4058    }
4059
4060    #[test]
4061    fn dom_declared_flags_are_distinct_and_queryable() {
4062        let flags = [
4063            DOM_HAS_SHAPE_INSIDE,
4064            DOM_HAS_SHAPE_OUTSIDE,
4065            DOM_HAS_TEXT_JUSTIFY,
4066            DOM_HAS_TEXT_INDENT,
4067            DOM_HAS_COLUMN_COUNT,
4068            DOM_HAS_COLUMN_GAP,
4069            DOM_HAS_INITIAL_LETTER,
4070            DOM_HAS_INITIAL_LETTER_ALIGN,
4071            DOM_HAS_LINE_CLAMP,
4072            DOM_HAS_HANGING_PUNCTUATION,
4073            DOM_HAS_TEXT_COMBINE_UPRIGHT,
4074            DOM_HAS_EXCLUSION_MARGIN,
4075            DOM_HAS_HYPHENATION_LANGUAGE,
4076            DOM_HAS_UNICODE_BIDI,
4077            DOM_HAS_TEXT_BOX_TRIM,
4078            DOM_HAS_HYPHENS,
4079            DOM_HAS_WORD_BREAK,
4080            DOM_HAS_OVERFLOW_WRAP,
4081            DOM_HAS_LINE_BREAK,
4082            DOM_HAS_TEXT_ALIGN_LAST,
4083            DOM_HAS_LINE_HEIGHT,
4084            DOM_HAS_COLUMN_WIDTH,
4085            DOM_HAS_SHAPE_MARGIN,
4086        ];
4087
4088        let mut seen = 0u32;
4089        for f in flags {
4090            assert_eq!(f.count_ones(), 1, "{f:#X} is not a single-bit flag");
4091            assert_eq!(seen & f, 0, "two DOM_HAS_* flags share bit {f:#X}");
4092            seen |= f;
4093        }
4094
4095        let mut c = CompactLayoutCache::empty();
4096        // Nothing declared: every query is false, including the degenerate ones.
4097        for f in flags {
4098            assert!(!c.dom_declared(f));
4099        }
4100        assert!(!c.dom_declared(0));
4101        assert!(!c.dom_declared(u32::MAX));
4102
4103        // One flag declared: only that query is true.
4104        c.dom_declared_flags = DOM_HAS_LINE_HEIGHT;
4105        for f in flags {
4106            assert_eq!(c.dom_declared(f), f == DOM_HAS_LINE_HEIGHT);
4107        }
4108        assert!(!c.dom_declared(0), "an empty flag query must never report declared");
4109        assert!(c.dom_declared(u32::MAX));
4110
4111        // Everything declared: every query is true.
4112        c.dom_declared_flags = u32::MAX;
4113        for f in flags {
4114            assert!(c.dom_declared(f));
4115        }
4116    }
4117
4118    #[test]
4119    fn getters_at_the_last_valid_index_do_not_panic() {
4120        let c = CompactLayoutCache::with_capacity(4);
4121        let last = c.node_count() - 1;
4122        let _ = c.get_display(last);
4123        let _ = c.get_width_raw(last);
4124        let _ = c.get_padding_top(last);
4125        let _ = c.get_z_index(last);
4126        let _ = c.get_border_top_style(last);
4127        let _ = c.get_line_height(last);
4128        let _ = c.has_transform(last);
4129    }
4130
4131    #[test]
4132    #[should_panic(expected = "index out of bounds")]
4133    fn tier1_getter_on_an_empty_cache_panics_rather_than_reading_oob() {
4134        let c = CompactLayoutCache::empty();
4135        let _ = c.get_display(0);
4136    }
4137
4138    #[test]
4139    #[should_panic(expected = "index out of bounds")]
4140    fn tier2_getter_past_the_end_panics_rather_than_reading_oob() {
4141        let c = CompactLayoutCache::with_capacity(2);
4142        let _ = c.get_padding_top(2);
4143    }
4144
4145    #[test]
4146    #[should_panic(expected = "index out of bounds")]
4147    fn cold_tier_getter_at_usize_max_panics_rather_than_wrapping() {
4148        // usize::MAX would wrap to a valid offset if the index were ever used in
4149        // pointer arithmetic without a bounds check.
4150        let c = CompactLayoutCache::with_capacity(1);
4151        let _ = c.get_z_index(usize::MAX);
4152    }
4153
4154    #[test]
4155    #[should_panic(expected = "index out of bounds")]
4156    fn text_tier_getter_past_the_end_panics_rather_than_reading_oob() {
4157        let c = CompactLayoutCache::with_capacity(1);
4158        let _ = c.get_font_family_hash(1);
4159    }
4160
4161    // =========================================================================
4162    // Struct layout — the compact cache's whole point is its byte budget
4163    // =========================================================================
4164
4165    #[test]
4166    fn compact_structs_stay_within_their_documented_byte_budget() {
4167        // These sizes are load-bearing: the cache is sized as N × these, and the
4168        // module header quotes them. A field added without updating the header
4169        // silently doubles the per-node memory cost.
4170        assert_eq!(size_of::<CompactNodeProps>(), 72);
4171        assert_eq!(size_of::<CompactNodePropsCold>(), 48);
4172        assert_eq!(size_of::<CompactTextProps>(), 24);
4173        // Tier 1 is exactly one u64 per node.
4174        assert_eq!(size_of::<u64>(), 8);
4175        // No padding surprises from #[repr(C)] reordering.
4176        assert_eq!(align_of::<CompactNodeProps>(), 4);
4177        assert_eq!(align_of::<CompactNodePropsCold>(), 4);
4178        assert_eq!(align_of::<CompactTextProps>(), 8);
4179    }
4180}