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    /// Split scale by SIGN (parser convention: negative normalized =
1396    /// absolute px): negative = -px x 10 (line-height: 40px -> -400),
1397    /// positive = unitless multiple x 1000 (1.2 / 120% -> 1200).
1398    /// `I16_SENTINEL` = unset ("normal").
1399    pub line_height: i16,
1400    pub letter_spacing: i16,   // px × 10
1401    pub word_spacing: i16,     // px × 10
1402    pub text_indent: i16,      // px × 10
1403}
1404
1405impl Default for CompactTextProps {
1406    fn default() -> Self {
1407        Self {
1408            text_color: 0,
1409            font_family_hash: 0,
1410            line_height: I16_SENTINEL, // "normal" → sentinel
1411            letter_spacing: 0,
1412            word_spacing: 0,
1413            text_indent: 0,
1414        }
1415    }
1416}
1417
1418// =============================================================================
1419// Tier 3: Overflow map — rare/complex properties
1420// =============================================================================
1421
1422// Overflow properties that couldn't fit in Tier 1/2 encoding.
1423// Contains the original `CssProperty` values for properties that:
1424// - Have `calc()` expressions
1425// - Exceed the numeric range of compact encoding
1426// - Are rare CSS properties (grid, transforms, etc.)
1427// =============================================================================
1428// CompactLayoutCache — the top-level container
1429// =============================================================================
1430
1431/// Three-tier compact layout property cache.
1432///
1433/// Allocated once per restyle, indexed by node index (same as `NodeId`).
1434/// Provides O(1) array-indexed access to all layout properties.
1435///
1436/// Non-compact properties (background, box-shadow, transform, etc.) are
1437/// resolved via the slow cascade path in `CssPropertyCache::get_property_slow()`.
1438#[derive(Debug, Clone, PartialEq, Eq)]
1439pub struct CompactLayoutCache {
1440    /// Whether ANY layout-relevant pixel value in this document uses a
1441    /// viewport-relative unit (vw/vh/vmin/vmax), detected while the build
1442    /// loop extracts those very values (text tier + box dimensions — the set
1443    /// that feeds inline collection and measurement).
1444    ///
1445    /// Consumers use it to SKIP viewport-based cache invalidation for the
1446    /// overwhelmingly common document that never mentions a viewport unit:
1447    /// solver3's inline-collection fingerprint folded `ctx.viewport_size`
1448    /// into EVERY IFC's key "because vw/vh", so every resize invalidated
1449    /// every collection (552 re-collections ≈ 19.6 ms per resize on big.md)
1450    /// to protect a feature the document did not use. False negatives are
1451    /// impossible for the covered set (the flag is set from the same values
1452    /// the cache encodes); a property OUTSIDE the covered set cannot reach
1453    /// inline collection, which is the only consumer.
1454    pub uses_viewport_units: bool,
1455    /// Tier 1: ALL enum properties bitpacked into u64 per node (8 B/node)
1456    pub tier1_enums: Vec<u64>,
1457    /// Tier 2 hot: Layout-critical numeric dimensions per node (68 B/node)
1458    pub tier2_dims: Vec<CompactNodeProps>,
1459    /// Tier 2 cold: Paint-only properties per node (28 B/node)
1460    pub tier2_cold: Vec<CompactNodePropsCold>,
1461    /// Tier 2b: Text/IFC properties per node (24 B/node)
1462    pub tier2b_text: Vec<CompactTextProps>,
1463    /// Indices of nodes whose `font_family_hash` changed since the last frame.
1464    ///
1465    /// Enables **per-node** font dirty tracking instead of the global all-or-nothing
1466    /// `font_stacks_hash` XOR approach. When this list is non-empty, only the
1467    /// font chains for these specific nodes need to be re-resolved, avoiding O(N)
1468    /// re-resolution when a single node's `font-family` changes.
1469    ///
1470    /// Populated during `build_compact_cache()` by comparing each node's
1471    /// `font_family_hash` against `prev_font_hashes`.
1472    pub font_dirty_nodes: Vec<usize>,
1473    /// Previous frame's per-node `font_family_hash` values.
1474    ///
1475    /// Stored after each compact cache build so that the next build can detect
1476    /// which specific nodes' font-family changed (rather than relying on a
1477    /// collision-prone global XOR hash).
1478    pub prev_font_hashes: Vec<u64>,
1479    /// Reverse map: `font_family_hash` (u64) → actual `StyleFontFamilyVec`.
1480    ///
1481    /// Populated during `build_compact_cache()` as a byproduct of hash computation.
1482    /// Consumers use this to look up font family names from the compact cache hash
1483    /// without going through `get_property_slow()` (which fails for inherited values
1484    /// on text nodes).
1485    pub font_hash_to_families: alloc::collections::BTreeMap<u64, crate::props::basic::font::StyleFontFamilyVec>,
1486    /// True when any node's inline style carries a NON-pseudo dynamic
1487    /// condition (viewport/@media, theme, OS, container...). The window uses
1488    /// this to decide whether a `DynamicSelectorContext` change (a resize
1489    /// crossing a breakpoint, a theme flip) requires rebuilding this cache:
1490    /// for the overwhelmingly common condition-free DOM the answer is a
1491    /// single bool read.
1492    pub has_dynamic_conditions: bool,
1493    /// Bitfield tracking which rare text props are declared *anywhere* in the DOM.
1494    /// Built once during `build_compact_cache_with_inheritance`. When a bit is
1495    /// clear, callers (e.g. `translate_to_text3_constraints`) can skip the
1496    /// cascade walk for that property — its slow path would always return
1497    /// `None` and fall back to the default. See `DOM_HAS_*` constants.
1498    pub dom_declared_flags: u32,
1499    /// Viewport WIDTH thresholds harvested from every inline conditional
1500    /// property in the DOM — where a `ViewportWidth` condition can flip.
1501    /// Stored as `f32::to_bits` (this struct derives `Eq`); sorted +
1502    /// deduped. Together with the author stylesheet's
1503    /// `Css::viewport_breakpoints`, these replace the old hardcoded
1504    /// breakpoint guess list in the engine's resize decision (a widget
1505    /// breakpoint like the ribbon's 720px was invisible to that list, so
1506    /// shrinking onto the mobile layout never regenerated).
1507    pub inline_viewport_w: Vec<u32>,
1508    /// Viewport HEIGHT thresholds, same contract as `inline_viewport_w`.
1509    pub inline_viewport_h: Vec<u32>,
1510}
1511
1512impl CompactLayoutCache {
1513    /// Create an empty cache (no nodes).
1514    #[must_use] pub const fn empty() -> Self {
1515        Self {
1516            uses_viewport_units: false,
1517            tier1_enums: Vec::new(),
1518            tier2_dims: Vec::new(),
1519            tier2_cold: Vec::new(),
1520            tier2b_text: Vec::new(),
1521            font_dirty_nodes: Vec::new(),
1522            prev_font_hashes: Vec::new(),
1523            font_hash_to_families: alloc::collections::BTreeMap::new(),
1524            dom_declared_flags: 0,
1525            has_dynamic_conditions: false,
1526            inline_viewport_w: Vec::new(),
1527            inline_viewport_h: Vec::new(),
1528        }
1529    }
1530
1531    /// Create a cache pre-allocated for `node_count` nodes, filled with defaults.
1532    #[must_use] pub fn with_capacity(node_count: usize) -> Self {
1533        Self {
1534            uses_viewport_units: false,
1535            tier1_enums: vec![0u64; node_count],
1536            tier2_dims: vec![CompactNodeProps::default(); node_count],
1537            tier2_cold: vec![CompactNodePropsCold::default(); node_count],
1538            tier2b_text: vec![CompactTextProps::default(); node_count],
1539            font_dirty_nodes: Vec::new(),
1540            prev_font_hashes: vec![0u64; node_count],
1541            font_hash_to_families: alloc::collections::BTreeMap::new(),
1542            dom_declared_flags: 0,
1543            has_dynamic_conditions: false,
1544            inline_viewport_w: Vec::new(),
1545            inline_viewport_h: Vec::new(),
1546        }
1547    }
1548
1549    /// Number of nodes in this cache.
1550    #[inline]
1551    #[must_use] pub const fn node_count(&self) -> usize {
1552        self.tier1_enums.len()
1553    }
1554
1555    // -- Tier 1 getters (enum properties) --
1556
1557    #[inline]
1558    #[must_use] pub fn get_display(&self, node_idx: usize) -> LayoutDisplay {
1559        decode_display(self.tier1_enums[node_idx])
1560    }
1561
1562    #[inline]
1563    #[must_use] pub fn get_position(&self, node_idx: usize) -> LayoutPosition {
1564        decode_position(self.tier1_enums[node_idx])
1565    }
1566
1567    #[inline]
1568    #[must_use] pub fn get_float(&self, node_idx: usize) -> LayoutFloat {
1569        decode_float(self.tier1_enums[node_idx])
1570    }
1571
1572    #[inline]
1573    #[must_use] pub fn get_overflow_x(&self, node_idx: usize) -> LayoutOverflow {
1574        decode_overflow_x(self.tier1_enums[node_idx])
1575    }
1576
1577    #[inline]
1578    #[must_use] pub fn get_overflow_y(&self, node_idx: usize) -> LayoutOverflow {
1579        decode_overflow_y(self.tier1_enums[node_idx])
1580    }
1581
1582    #[inline]
1583    #[must_use] pub fn get_box_sizing(&self, node_idx: usize) -> LayoutBoxSizing {
1584        decode_box_sizing(self.tier1_enums[node_idx])
1585    }
1586
1587    #[inline]
1588    #[must_use] pub fn get_flex_direction(&self, node_idx: usize) -> LayoutFlexDirection {
1589        decode_flex_direction(self.tier1_enums[node_idx])
1590    }
1591
1592    #[inline]
1593    #[must_use] pub fn get_flex_wrap(&self, node_idx: usize) -> LayoutFlexWrap {
1594        decode_flex_wrap(self.tier1_enums[node_idx])
1595    }
1596
1597    #[inline]
1598    #[must_use] pub fn get_justify_content(&self, node_idx: usize) -> LayoutJustifyContent {
1599        decode_justify_content(self.tier1_enums[node_idx])
1600    }
1601
1602    #[inline]
1603    #[must_use] pub fn get_align_items(&self, node_idx: usize) -> LayoutAlignItems {
1604        decode_align_items(self.tier1_enums[node_idx])
1605    }
1606
1607    #[inline]
1608    #[must_use] pub fn get_align_content(&self, node_idx: usize) -> LayoutAlignContent {
1609        decode_align_content(self.tier1_enums[node_idx])
1610    }
1611
1612    #[inline]
1613    #[must_use] pub fn get_writing_mode(&self, node_idx: usize) -> LayoutWritingMode {
1614        decode_writing_mode(self.tier1_enums[node_idx])
1615    }
1616
1617    #[inline]
1618    #[must_use] pub fn get_clear(&self, node_idx: usize) -> LayoutClear {
1619        decode_clear(self.tier1_enums[node_idx])
1620    }
1621
1622    #[inline]
1623    #[must_use] pub fn get_font_weight(&self, node_idx: usize) -> StyleFontWeight {
1624        decode_font_weight(self.tier1_enums[node_idx])
1625    }
1626
1627    #[inline]
1628    #[must_use] pub fn get_font_style(&self, node_idx: usize) -> StyleFontStyle {
1629        decode_font_style(self.tier1_enums[node_idx])
1630    }
1631
1632    #[inline]
1633    #[must_use] pub fn get_text_align(&self, node_idx: usize) -> StyleTextAlign {
1634        decode_text_align(self.tier1_enums[node_idx])
1635    }
1636
1637    #[inline]
1638    #[must_use] pub fn get_visibility(&self, node_idx: usize) -> StyleVisibility {
1639        decode_visibility(self.tier1_enums[node_idx])
1640    }
1641
1642    #[inline]
1643    #[must_use] pub fn get_white_space(&self, node_idx: usize) -> StyleWhiteSpace {
1644        decode_white_space(self.tier1_enums[node_idx])
1645    }
1646
1647    #[inline]
1648    #[must_use] pub fn get_direction(&self, node_idx: usize) -> StyleDirection {
1649        decode_direction(self.tier1_enums[node_idx])
1650    }
1651
1652    #[inline]
1653    #[must_use] pub fn get_vertical_align(&self, node_idx: usize) -> StyleVerticalAlign {
1654        decode_vertical_align(self.tier1_enums[node_idx])
1655    }
1656
1657    #[inline]
1658    #[must_use] pub fn get_border_collapse(&self, node_idx: usize) -> StyleBorderCollapse {
1659        decode_border_collapse(self.tier1_enums[node_idx])
1660    }
1661
1662    // -- Tier 2 getters (numeric dimensions) --
1663
1664    /// Get width as encoded u32 (use `decode_pixel_value_u32` or check sentinel).
1665    #[inline]
1666    #[must_use] pub fn get_width_raw(&self, node_idx: usize) -> u32 {
1667        self.tier2_dims[node_idx].width
1668    }
1669
1670    #[inline]
1671    #[must_use] pub fn get_height_raw(&self, node_idx: usize) -> u32 {
1672        self.tier2_dims[node_idx].height
1673    }
1674
1675    #[inline]
1676    #[must_use] pub fn get_min_width_raw(&self, node_idx: usize) -> u32 {
1677        self.tier2_dims[node_idx].min_width
1678    }
1679
1680    #[inline]
1681    #[must_use] pub fn get_max_width_raw(&self, node_idx: usize) -> u32 {
1682        self.tier2_dims[node_idx].max_width
1683    }
1684
1685    #[inline]
1686    #[must_use] pub fn get_min_height_raw(&self, node_idx: usize) -> u32 {
1687        self.tier2_dims[node_idx].min_height
1688    }
1689
1690    #[inline]
1691    #[must_use] pub fn get_max_height_raw(&self, node_idx: usize) -> u32 {
1692        self.tier2_dims[node_idx].max_height
1693    }
1694
1695    #[inline]
1696    #[must_use] pub fn get_font_size_raw(&self, node_idx: usize) -> u32 {
1697        self.tier2_dims[node_idx].font_size
1698    }
1699
1700    #[inline]
1701    #[must_use] pub fn get_flex_basis_raw(&self, node_idx: usize) -> u32 {
1702        self.tier2_dims[node_idx].flex_basis
1703    }
1704
1705    /// Get padding-top as resolved px. Returns None if sentinel (needs slow path).
1706    #[inline]
1707    #[must_use] pub fn get_padding_top(&self, node_idx: usize) -> Option<f32> {
1708        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_top)
1709    }
1710
1711    #[inline]
1712    #[must_use] pub fn get_padding_right(&self, node_idx: usize) -> Option<f32> {
1713        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_right)
1714    }
1715
1716    #[inline]
1717    #[must_use] pub fn get_padding_bottom(&self, node_idx: usize) -> Option<f32> {
1718        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_bottom)
1719    }
1720
1721    #[inline]
1722    #[must_use] pub fn get_padding_left(&self, node_idx: usize) -> Option<f32> {
1723        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_left)
1724    }
1725
1726    #[inline]
1727    #[must_use] pub fn get_margin_top(&self, node_idx: usize) -> Option<f32> {
1728        let v = self.tier2_dims[node_idx].margin_top;
1729        if v == I16_AUTO { return None; } // Auto for margin is special
1730        decode_resolved_px_i16(v)
1731    }
1732
1733    #[inline]
1734    #[must_use] pub fn get_margin_right(&self, node_idx: usize) -> Option<f32> {
1735        let v = self.tier2_dims[node_idx].margin_right;
1736        if v == I16_AUTO { return None; }
1737        decode_resolved_px_i16(v)
1738    }
1739
1740    #[inline]
1741    #[must_use] pub fn get_margin_bottom(&self, node_idx: usize) -> Option<f32> {
1742        let v = self.tier2_dims[node_idx].margin_bottom;
1743        if v == I16_AUTO { return None; }
1744        decode_resolved_px_i16(v)
1745    }
1746
1747    #[inline]
1748    #[must_use] pub fn get_margin_left(&self, node_idx: usize) -> Option<f32> {
1749        let v = self.tier2_dims[node_idx].margin_left;
1750        if v == I16_AUTO { return None; }
1751        decode_resolved_px_i16(v)
1752    }
1753
1754    /// Check if margin is Auto (important for centering logic).
1755    #[inline]
1756    #[must_use] pub fn is_margin_top_auto(&self, node_idx: usize) -> bool {
1757        self.tier2_dims[node_idx].margin_top == I16_AUTO
1758    }
1759
1760    #[inline]
1761    #[must_use] pub fn is_margin_right_auto(&self, node_idx: usize) -> bool {
1762        self.tier2_dims[node_idx].margin_right == I16_AUTO
1763    }
1764
1765    #[inline]
1766    #[must_use] pub fn is_margin_bottom_auto(&self, node_idx: usize) -> bool {
1767        self.tier2_dims[node_idx].margin_bottom == I16_AUTO
1768    }
1769
1770    #[inline]
1771    #[must_use] pub fn is_margin_left_auto(&self, node_idx: usize) -> bool {
1772        self.tier2_dims[node_idx].margin_left == I16_AUTO
1773    }
1774
1775    #[inline]
1776    #[must_use] pub fn get_border_top_width(&self, node_idx: usize) -> Option<f32> {
1777        decode_resolved_px_i16(self.tier2_dims[node_idx].border_top_width)
1778    }
1779
1780    #[inline]
1781    #[must_use] pub fn get_border_right_width(&self, node_idx: usize) -> Option<f32> {
1782        decode_resolved_px_i16(self.tier2_dims[node_idx].border_right_width)
1783    }
1784
1785    #[inline]
1786    #[must_use] pub fn get_border_bottom_width(&self, node_idx: usize) -> Option<f32> {
1787        decode_resolved_px_i16(self.tier2_dims[node_idx].border_bottom_width)
1788    }
1789
1790    #[inline]
1791    #[must_use] pub fn get_border_left_width(&self, node_idx: usize) -> Option<f32> {
1792        decode_resolved_px_i16(self.tier2_dims[node_idx].border_left_width)
1793    }
1794
1795    // -- Raw i16 getters for macro fast paths --
1796
1797    #[inline]
1798    #[must_use] pub fn get_padding_top_raw(&self, node_idx: usize) -> i16 {
1799        self.tier2_dims[node_idx].padding_top
1800    }
1801
1802    #[inline]
1803    #[must_use] pub fn get_padding_right_raw(&self, node_idx: usize) -> i16 {
1804        self.tier2_dims[node_idx].padding_right
1805    }
1806
1807    #[inline]
1808    #[must_use] pub fn get_padding_bottom_raw(&self, node_idx: usize) -> i16 {
1809        self.tier2_dims[node_idx].padding_bottom
1810    }
1811
1812    #[inline]
1813    #[must_use] pub fn get_padding_left_raw(&self, node_idx: usize) -> i16 {
1814        self.tier2_dims[node_idx].padding_left
1815    }
1816
1817    #[inline]
1818    #[must_use] pub fn get_margin_top_raw(&self, node_idx: usize) -> i16 {
1819        self.tier2_dims[node_idx].margin_top
1820    }
1821
1822    #[inline]
1823    #[must_use] pub fn get_margin_right_raw(&self, node_idx: usize) -> i16 {
1824        self.tier2_dims[node_idx].margin_right
1825    }
1826
1827    #[inline]
1828    #[must_use] pub fn get_margin_bottom_raw(&self, node_idx: usize) -> i16 {
1829        self.tier2_dims[node_idx].margin_bottom
1830    }
1831
1832    #[inline]
1833    #[must_use] pub fn get_margin_left_raw(&self, node_idx: usize) -> i16 {
1834        self.tier2_dims[node_idx].margin_left
1835    }
1836
1837    #[inline]
1838    #[must_use] pub fn get_border_top_width_raw(&self, node_idx: usize) -> i16 {
1839        self.tier2_dims[node_idx].border_top_width
1840    }
1841
1842    #[inline]
1843    #[must_use] pub fn get_border_right_width_raw(&self, node_idx: usize) -> i16 {
1844        self.tier2_dims[node_idx].border_right_width
1845    }
1846
1847    #[inline]
1848    #[must_use] pub fn get_border_bottom_width_raw(&self, node_idx: usize) -> i16 {
1849        self.tier2_dims[node_idx].border_bottom_width
1850    }
1851
1852    #[inline]
1853    #[must_use] pub fn get_border_left_width_raw(&self, node_idx: usize) -> i16 {
1854        self.tier2_dims[node_idx].border_left_width
1855    }
1856
1857    #[inline]
1858    #[must_use] pub fn get_top(&self, node_idx: usize) -> i16 {
1859        self.tier2_dims[node_idx].top
1860    }
1861
1862    #[inline]
1863    #[must_use] pub fn get_right(&self, node_idx: usize) -> i16 {
1864        self.tier2_dims[node_idx].right
1865    }
1866
1867    #[inline]
1868    #[must_use] pub fn get_bottom(&self, node_idx: usize) -> i16 {
1869        self.tier2_dims[node_idx].bottom
1870    }
1871
1872    #[inline]
1873    #[must_use] pub fn get_left(&self, node_idx: usize) -> i16 {
1874        self.tier2_dims[node_idx].left
1875    }
1876
1877    #[inline]
1878    #[must_use] pub fn get_flex_grow(&self, node_idx: usize) -> Option<f32> {
1879        decode_flex_u16(self.tier2_dims[node_idx].flex_grow)
1880    }
1881
1882    #[inline]
1883    #[must_use] pub fn get_flex_shrink(&self, node_idx: usize) -> Option<f32> {
1884        decode_flex_u16(self.tier2_dims[node_idx].flex_shrink)
1885    }
1886
1887    #[inline]
1888    #[must_use] pub fn get_z_index(&self, node_idx: usize) -> i16 {
1889        self.tier2_cold[node_idx].z_index
1890    }
1891
1892    // -- Border colors (u32 RGBA) — cold tier --
1893
1894    #[inline]
1895    #[must_use] pub fn get_border_top_color_raw(&self, node_idx: usize) -> u32 {
1896        self.tier2_cold[node_idx].border_top_color
1897    }
1898
1899    #[inline]
1900    #[must_use] pub fn get_border_right_color_raw(&self, node_idx: usize) -> u32 {
1901        self.tier2_cold[node_idx].border_right_color
1902    }
1903
1904    #[inline]
1905    #[must_use] pub fn get_border_bottom_color_raw(&self, node_idx: usize) -> u32 {
1906        self.tier2_cold[node_idx].border_bottom_color
1907    }
1908
1909    #[inline]
1910    #[must_use] pub fn get_border_left_color_raw(&self, node_idx: usize) -> u32 {
1911        self.tier2_cold[node_idx].border_left_color
1912    }
1913
1914    // -- Border styles (packed u16) — cold tier --
1915
1916    #[inline]
1917    #[must_use] pub fn get_border_styles_packed(&self, node_idx: usize) -> u16 {
1918        self.tier2_cold[node_idx].border_styles_packed
1919    }
1920
1921    #[inline]
1922    #[must_use] pub fn get_border_top_style(&self, node_idx: usize) -> BorderStyle {
1923        decode_border_top_style(self.tier2_cold[node_idx].border_styles_packed)
1924    }
1925
1926    #[inline]
1927    #[must_use] pub fn get_border_right_style(&self, node_idx: usize) -> BorderStyle {
1928        decode_border_right_style(self.tier2_cold[node_idx].border_styles_packed)
1929    }
1930
1931    #[inline]
1932    #[must_use] pub fn get_border_bottom_style(&self, node_idx: usize) -> BorderStyle {
1933        decode_border_bottom_style(self.tier2_cold[node_idx].border_styles_packed)
1934    }
1935
1936    #[inline]
1937    #[must_use] pub fn get_border_left_style(&self, node_idx: usize) -> BorderStyle {
1938        decode_border_left_style(self.tier2_cold[node_idx].border_styles_packed)
1939    }
1940
1941    // -- Border spacing — cold tier --
1942
1943    #[inline]
1944    #[must_use] pub fn get_border_spacing_h_raw(&self, node_idx: usize) -> i16 {
1945        self.tier2_cold[node_idx].border_spacing_h
1946    }
1947
1948    #[inline]
1949    #[must_use] pub fn get_border_spacing_v_raw(&self, node_idx: usize) -> i16 {
1950        self.tier2_cold[node_idx].border_spacing_v
1951    }
1952
1953    // -- Tab size — cold tier --
1954
1955    #[inline]
1956    #[must_use] pub fn get_tab_size_raw(&self, node_idx: usize) -> i16 {
1957        self.tier2_cold[node_idx].tab_size
1958    }
1959
1960    // -- Border radii — cold tier (i16 px × 10, I16_SENTINEL = unset = 0) --
1961
1962    #[inline]
1963    #[must_use] pub fn get_border_top_left_radius_raw(&self, node_idx: usize) -> i16 {
1964        self.tier2_cold[node_idx].border_top_left_radius
1965    }
1966
1967    #[inline]
1968    #[must_use] pub fn get_border_top_right_radius_raw(&self, node_idx: usize) -> i16 {
1969        self.tier2_cold[node_idx].border_top_right_radius
1970    }
1971
1972    #[inline]
1973    #[must_use] pub fn get_border_bottom_left_radius_raw(&self, node_idx: usize) -> i16 {
1974        self.tier2_cold[node_idx].border_bottom_left_radius
1975    }
1976
1977    #[inline]
1978    #[must_use] pub fn get_border_bottom_right_radius_raw(&self, node_idx: usize) -> i16 {
1979        self.tier2_cold[node_idx].border_bottom_right_radius
1980    }
1981
1982    // -- Opacity / transform / hot flags --
1983
1984    /// Raw opacity byte. `OPACITY_SENTINEL` (255) = unset (default = 1.0).
1985    /// Otherwise value / 254.0 yields the opacity in [0.0, 1.0].
1986    #[inline]
1987    #[must_use] pub fn get_opacity_raw(&self, node_idx: usize) -> u8 {
1988        self.tier2_cold[node_idx].opacity
1989    }
1990
1991    #[inline]
1992    #[must_use] pub fn get_hot_flags(&self, node_idx: usize) -> u8 {
1993        self.tier2_cold[node_idx].hot_flags
1994    }
1995
1996    #[inline]
1997    #[must_use] pub fn has_transform(&self, node_idx: usize) -> bool {
1998        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_TRANSFORM != 0
1999    }
2000
2001    #[inline]
2002    #[must_use] pub fn has_transform_origin(&self, node_idx: usize) -> bool {
2003        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_TRANSFORM_ORIGIN != 0
2004    }
2005
2006    #[inline]
2007    #[must_use] pub fn has_box_shadow(&self, node_idx: usize) -> bool {
2008        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_BOX_SHADOW != 0
2009    }
2010
2011    #[inline]
2012    #[must_use] pub fn has_text_decoration(&self, node_idx: usize) -> bool {
2013        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_TEXT_DECORATION != 0
2014    }
2015
2016    #[inline]
2017    #[must_use] pub fn has_background(&self, node_idx: usize) -> bool {
2018        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0
2019    }
2020
2021    #[inline]
2022    #[must_use] pub fn has_clip_path(&self, node_idx: usize) -> bool {
2023        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_CLIP_PATH != 0
2024    }
2025
2026    #[inline]
2027    #[must_use] pub fn has_scrollbar_css(&self, node_idx: usize) -> bool {
2028        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_SCROLLBAR_CSS != 0
2029    }
2030
2031    #[inline]
2032    #[must_use] pub fn has_counter(&self, node_idx: usize) -> bool {
2033        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_COUNTER != 0
2034    }
2035
2036    #[inline]
2037    #[must_use] pub fn has_break(&self, node_idx: usize) -> bool {
2038        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_BREAK != 0
2039    }
2040
2041    #[inline]
2042    #[must_use] pub fn has_text_orientation(&self, node_idx: usize) -> bool {
2043        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_TEXT_ORIENTATION != 0
2044    }
2045
2046    #[inline]
2047    #[must_use] pub fn has_text_shadow(&self, node_idx: usize) -> bool {
2048        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_TEXT_SHADOW != 0
2049    }
2050
2051    #[inline]
2052    #[must_use] pub fn has_backdrop_filter(&self, node_idx: usize) -> bool {
2053        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_BACKDROP_FILTER != 0
2054    }
2055
2056    #[inline]
2057    #[must_use] pub fn has_filter(&self, node_idx: usize) -> bool {
2058        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_FILTER != 0
2059    }
2060
2061    #[inline]
2062    #[must_use] pub fn has_mix_blend_mode(&self, node_idx: usize) -> bool {
2063        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_MIX_BLEND_MODE != 0
2064    }
2065
2066    /// DOM-level fast-path check: returns `true` if the given flag bit is set
2067    /// (some node in this DOM declared the corresponding property).
2068    #[inline]
2069    #[must_use] pub const fn dom_declared(&self, flag: u32) -> bool {
2070        self.dom_declared_flags & flag != 0
2071    }
2072
2073    /// Scrollbar-gutter: 0 = auto (default), 1 = stable, 2 = both-edges, 3 = mirror.
2074    #[inline]
2075    #[must_use] pub fn get_scrollbar_gutter_bits(&self, node_idx: usize) -> u8 {
2076        (self.tier2_cold[node_idx].hot_flags & HOT_FLAG_SCROLLBAR_GUTTER_MASK)
2077            >> HOT_FLAG_SCROLLBAR_GUTTER_SHIFT
2078    }
2079
2080    // -- Tier 2b getters (text props) --
2081
2082    #[inline]
2083    #[must_use] pub fn get_text_color_raw(&self, node_idx: usize) -> u32 {
2084        self.tier2b_text[node_idx].text_color
2085    }
2086
2087    #[inline]
2088    #[must_use] pub fn get_font_family_hash(&self, node_idx: usize) -> u64 {
2089        self.tier2b_text[node_idx].font_family_hash
2090    }
2091
2092    #[inline]
2093    #[must_use] pub fn get_line_height(&self, node_idx: usize) -> Option<f32> {
2094        decode_resolved_px_i16(self.tier2b_text[node_idx].line_height)
2095    }
2096
2097    #[inline]
2098    #[must_use] pub fn get_letter_spacing(&self, node_idx: usize) -> Option<f32> {
2099        decode_resolved_px_i16(self.tier2b_text[node_idx].letter_spacing)
2100    }
2101
2102    #[inline]
2103    #[must_use] pub fn get_word_spacing(&self, node_idx: usize) -> Option<f32> {
2104        decode_resolved_px_i16(self.tier2b_text[node_idx].word_spacing)
2105    }
2106
2107    #[inline]
2108    #[must_use] pub fn get_text_indent(&self, node_idx: usize) -> Option<f32> {
2109        decode_resolved_px_i16(self.tier2b_text[node_idx].text_indent)
2110    }
2111
2112}
2113
2114// =============================================================================
2115// Helper: encode a CssPropertyValue<PixelValue> into i16 resolved-px
2116// =============================================================================
2117
2118/// Resolve a `CssPropertyValue`<PixelValue> to an i16 ×10 encoding.
2119///
2120/// Only handles `Exact(px(...))` values. Everything else → sentinel.
2121/// For the compact cache builder, we only pre-resolve absolute pixel values.
2122/// Relative units (em, %, etc.) get sentinel and fall back to the slow path.
2123#[inline]
2124#[must_use] pub fn encode_css_pixel_as_i16(prop: &CssPropertyValue<PixelValue>) -> i16 {
2125    match prop {
2126        CssPropertyValue::Exact(pv) => {
2127            if pv.metric == SizeMetric::Px {
2128                encode_resolved_px_i16(pv.number.get())
2129            } else {
2130                I16_SENTINEL // non-px units need resolution context → slow path
2131            }
2132        }
2133        CssPropertyValue::Auto => I16_AUTO,
2134        CssPropertyValue::Initial => I16_INITIAL,
2135        CssPropertyValue::Inherit => I16_INHERIT,
2136        _ => I16_SENTINEL,
2137    }
2138}
2139
2140#[cfg(test)]
2141mod tests {
2142    use super::*;
2143
2144    #[test]
2145    fn test_tier1_roundtrip() {
2146        let t1 = encode_tier1(
2147            LayoutDisplay::Flex,
2148            LayoutPosition::Relative,
2149            LayoutFloat::Left,
2150            LayoutOverflow::Hidden,
2151            LayoutOverflow::Scroll,
2152            LayoutBoxSizing::BorderBox,
2153            LayoutFlexDirection::Column,
2154            LayoutFlexWrap::Wrap,
2155            LayoutJustifyContent::SpaceBetween,
2156            LayoutAlignItems::Center,
2157            LayoutAlignContent::End,
2158            LayoutWritingMode::VerticalRl,
2159            LayoutClear::Both,
2160            StyleFontWeight::Bold,
2161            StyleFontStyle::Italic,
2162            StyleTextAlign::Center,
2163            StyleVisibility::Hidden,
2164            StyleWhiteSpace::Pre,
2165            StyleDirection::Rtl,
2166            StyleVerticalAlign::Middle,
2167            StyleBorderCollapse::Collapse,
2168        );
2169
2170        assert!(tier1_is_populated(t1));
2171        assert_eq!(decode_display(t1), LayoutDisplay::Flex);
2172        assert_eq!(decode_position(t1), LayoutPosition::Relative);
2173        assert_eq!(decode_float(t1), LayoutFloat::Left);
2174        assert_eq!(decode_overflow_x(t1), LayoutOverflow::Hidden);
2175        assert_eq!(decode_overflow_y(t1), LayoutOverflow::Scroll);
2176        assert_eq!(decode_box_sizing(t1), LayoutBoxSizing::BorderBox);
2177        assert_eq!(decode_flex_direction(t1), LayoutFlexDirection::Column);
2178        assert_eq!(decode_flex_wrap(t1), LayoutFlexWrap::Wrap);
2179        assert_eq!(decode_justify_content(t1), LayoutJustifyContent::SpaceBetween);
2180        assert_eq!(decode_align_items(t1), LayoutAlignItems::Center);
2181        assert_eq!(decode_align_content(t1), LayoutAlignContent::End);
2182        assert_eq!(decode_writing_mode(t1), LayoutWritingMode::VerticalRl);
2183        assert_eq!(decode_clear(t1), LayoutClear::Both);
2184        assert_eq!(decode_font_weight(t1), StyleFontWeight::Bold);
2185        assert_eq!(decode_font_style(t1), StyleFontStyle::Italic);
2186        assert_eq!(decode_text_align(t1), StyleTextAlign::Center);
2187        assert_eq!(decode_visibility(t1), StyleVisibility::Hidden);
2188        assert_eq!(decode_white_space(t1), StyleWhiteSpace::Pre);
2189        assert_eq!(decode_direction(t1), StyleDirection::Rtl);
2190        assert_eq!(decode_vertical_align(t1), StyleVerticalAlign::Middle);
2191        assert_eq!(decode_border_collapse(t1), StyleBorderCollapse::Collapse);
2192    }
2193
2194    #[test]
2195    fn test_tier1_defaults() {
2196        let t1 = encode_tier1(
2197            LayoutDisplay::Block,
2198            LayoutPosition::Static,
2199            LayoutFloat::None,
2200            LayoutOverflow::Visible,
2201            LayoutOverflow::Visible,
2202            LayoutBoxSizing::ContentBox,
2203            LayoutFlexDirection::Row,
2204            LayoutFlexWrap::NoWrap,
2205            LayoutJustifyContent::FlexStart,
2206            LayoutAlignItems::Stretch,
2207            LayoutAlignContent::Stretch,
2208            LayoutWritingMode::HorizontalTb,
2209            LayoutClear::None,
2210            StyleFontWeight::Normal,
2211            StyleFontStyle::Normal,
2212            StyleTextAlign::Left,
2213            StyleVisibility::Visible,
2214            StyleWhiteSpace::Normal,
2215            StyleDirection::Ltr,
2216            StyleVerticalAlign::Baseline,
2217            StyleBorderCollapse::Separate,
2218        );
2219
2220        assert!(tier1_is_populated(t1));
2221        assert_eq!(decode_display(t1), LayoutDisplay::Block);
2222        assert_eq!(decode_position(t1), LayoutPosition::Static);
2223    }
2224
2225    #[test]
2226    fn test_pixel_value_u32_roundtrip() {
2227        let pv = PixelValue::px(123.456);
2228        let encoded = encode_pixel_value_u32(&pv);
2229        assert!(encoded < U32_SENTINEL_THRESHOLD);
2230        let decoded = decode_pixel_value_u32(encoded).unwrap();
2231        assert_eq!(decoded.metric, SizeMetric::Px);
2232        // Check within precision (×1000)
2233        assert!((decoded.number.get() - 123.456).abs() < 0.002);
2234    }
2235
2236    #[test]
2237    fn test_pixel_value_u32_percent() {
2238        let pv = PixelValue {
2239            metric: SizeMetric::Percent,
2240            number: FloatValue::new(50.0),
2241        };
2242        let encoded = encode_pixel_value_u32(&pv);
2243        let decoded = decode_pixel_value_u32(encoded).unwrap();
2244        assert_eq!(decoded.metric, SizeMetric::Percent);
2245        assert!((decoded.number.get() - 50.0).abs() < 0.002);
2246    }
2247
2248    #[test]
2249    fn test_sentinel_values() {
2250        assert_eq!(decode_pixel_value_u32(U32_SENTINEL), None);
2251        assert_eq!(decode_pixel_value_u32(U32_AUTO), None);
2252        assert_eq!(decode_pixel_value_u32(U32_MIN_CONTENT), None);
2253        assert_eq!(decode_resolved_px_i16(I16_SENTINEL), None);
2254        assert_eq!(decode_resolved_px_i16(I16_AUTO), None);
2255    }
2256
2257    #[test]
2258    fn test_resolved_px_i16_roundtrip() {
2259        let px = 123.4f32;
2260        let encoded = encode_resolved_px_i16(px);
2261        let decoded = decode_resolved_px_i16(encoded).unwrap();
2262        assert!((decoded - 123.4).abs() < 0.11);
2263
2264        // Negative values
2265        let px = -50.7f32;
2266        let encoded = encode_resolved_px_i16(px);
2267        let decoded = decode_resolved_px_i16(encoded).unwrap();
2268        assert!((decoded - (-50.7)).abs() < 0.11);
2269    }
2270
2271    #[test]
2272    fn test_flex_u16_roundtrip() {
2273        let v = 2.5f32;
2274        let encoded = encode_flex_u16(v);
2275        let decoded = decode_flex_u16(encoded).unwrap();
2276        assert!((decoded - 2.5).abs() < 0.011);
2277    }
2278
2279    #[test]
2280    fn test_compact_node_props_size() {
2281        // 72B hot props: 8×u32 dimensions (32B) + 16×i16 box model (32B)
2282        // + 2×u16 flex (4B) + 1×i16 order + align/pos tier1 bits (4B).
2283        assert_eq!(size_of::<CompactNodeProps>(), 72);
2284    }
2285
2286    #[test]
2287    fn test_compact_node_props_cold_size() {
2288        // 48B cold props: 4×u32 border colors (16B) + 4×i16 border radii (8B)
2289        // + 1×i16 z_index + 1×u16 border_styles_packed + 2×i16 border_spacing
2290        // + 1×i16 tab_size + 4×i16 grid placement (8B) + 3×u8 (opacity,
2291        // hot_flags, extra_flags) = 45B, padded to 48B for u32 alignment.
2292        assert_eq!(size_of::<CompactNodePropsCold>(), 48);
2293    }
2294
2295    #[test]
2296    fn test_compact_text_props_size() {
2297        assert_eq!(size_of::<CompactTextProps>(), 24);
2298    }
2299
2300    // ========================================================================
2301    // Tier1 enum 0-sentinel contract
2302    //
2303    // Tier1 packs 21 enums into a single u64. A bit run that is all zeros
2304    // must decode to the CSS initial value of that property, because an
2305    // unpopulated tier1 field is all zeros. If any encoder shifts so that
2306    // `0 -> something-other-than-initial`, every node that didn't explicitly
2307    // set the property silently gets the wrong default — which is exactly
2308    // how the calc.c grid stretch regression shipped (Start encoded as 0,
2309    // so every grid container reported justify-items: Start instead of
2310    // the CSS default Stretch-for-grid, collapsing the calc button grid).
2311    //
2312    // Test invariant: decoding a u8 of 0 for every enum yields the CSS
2313    // initial value of that property.
2314    // ========================================================================
2315
2316    #[test]
2317    fn test_justify_items_zero_is_stretch() {
2318        // CSS initial for justify-items is `normal`, which on a grid item
2319        // behaves as `stretch`. The tier1 bit pattern 0 must round-trip to
2320        // Stretch so unset grid containers don't collapse their items.
2321        assert_eq!(layout_justify_items_from_u8(0), LayoutJustifyItems::Stretch);
2322        assert_eq!(layout_justify_items_to_u8(LayoutJustifyItems::Stretch), 0);
2323    }
2324
2325    #[test]
2326    fn test_tier1_enum_zero_sentinel_is_css_initial() {
2327        assert_eq!(layout_display_from_u8(0), LayoutDisplay::Block);
2328        assert_eq!(layout_position_from_u8(0), LayoutPosition::Static);
2329        assert_eq!(layout_float_from_u8(0), LayoutFloat::None);
2330        assert_eq!(layout_overflow_from_u8(0), LayoutOverflow::Visible);
2331        assert_eq!(layout_box_sizing_from_u8(0), LayoutBoxSizing::ContentBox);
2332        assert_eq!(layout_flex_direction_from_u8(0), LayoutFlexDirection::Row);
2333        assert_eq!(layout_flex_wrap_from_u8(0), LayoutFlexWrap::NoWrap);
2334        assert_eq!(layout_justify_content_from_u8(0), LayoutJustifyContent::FlexStart);
2335        assert_eq!(layout_align_items_from_u8(0), LayoutAlignItems::Stretch);
2336        assert_eq!(layout_align_content_from_u8(0), LayoutAlignContent::Stretch);
2337        assert_eq!(layout_align_self_from_u8(0), LayoutAlignSelf::Auto);
2338        assert_eq!(layout_justify_self_from_u8(0), LayoutJustifySelf::Auto);
2339        assert_eq!(layout_justify_items_from_u8(0), LayoutJustifyItems::Stretch);
2340        assert_eq!(layout_grid_auto_flow_from_u8(0), LayoutGridAutoFlow::Row);
2341        assert_eq!(layout_writing_mode_from_u8(0), LayoutWritingMode::HorizontalTb);
2342        assert_eq!(layout_clear_from_u8(0), LayoutClear::None);
2343        assert_eq!(style_font_weight_from_u8(0), StyleFontWeight::Normal);
2344        assert_eq!(style_font_style_from_u8(0), StyleFontStyle::Normal);
2345        // text-align initial is `start`; we collapse `start` → `left` on
2346        // LTR runs at encode time, so the 0 slot decodes to Left.
2347        assert_eq!(style_text_align_from_u8(0), StyleTextAlign::Start);
2348        assert_eq!(style_visibility_from_u8(0), StyleVisibility::Visible);
2349        assert_eq!(style_white_space_from_u8(0), StyleWhiteSpace::Normal);
2350        assert_eq!(style_direction_from_u8(0), StyleDirection::Ltr);
2351        assert_eq!(style_vertical_align_from_u8(0), StyleVerticalAlign::Baseline);
2352        assert_eq!(border_collapse_from_u8(0), StyleBorderCollapse::Separate);
2353    }
2354
2355    #[test]
2356    fn test_tier1_enum_initial_encodes_to_zero() {
2357        // Mirror of the above — encoding the CSS initial value must
2358        // produce 0, otherwise an `all-zeros` tier1 bit run would encode
2359        // a non-initial value and nodes without explicit properties would
2360        // silently inherit the wrong default.
2361        assert_eq!(layout_display_to_u8(LayoutDisplay::Block), 0);
2362        assert_eq!(layout_position_to_u8(LayoutPosition::Static), 0);
2363        assert_eq!(layout_float_to_u8(LayoutFloat::None), 0);
2364        assert_eq!(layout_overflow_to_u8(LayoutOverflow::Visible), 0);
2365        assert_eq!(layout_box_sizing_to_u8(LayoutBoxSizing::ContentBox), 0);
2366        assert_eq!(layout_flex_direction_to_u8(LayoutFlexDirection::Row), 0);
2367        assert_eq!(layout_flex_wrap_to_u8(LayoutFlexWrap::NoWrap), 0);
2368        assert_eq!(layout_justify_content_to_u8(LayoutJustifyContent::FlexStart), 0);
2369        assert_eq!(layout_align_items_to_u8(LayoutAlignItems::Stretch), 0);
2370        assert_eq!(layout_align_content_to_u8(LayoutAlignContent::Stretch), 0);
2371        assert_eq!(layout_align_self_to_u8(LayoutAlignSelf::Auto), 0);
2372        assert_eq!(layout_justify_self_to_u8(LayoutJustifySelf::Auto), 0);
2373        assert_eq!(layout_justify_items_to_u8(LayoutJustifyItems::Stretch), 0);
2374        assert_eq!(layout_grid_auto_flow_to_u8(LayoutGridAutoFlow::Row), 0);
2375        assert_eq!(layout_writing_mode_to_u8(LayoutWritingMode::HorizontalTb), 0);
2376        assert_eq!(layout_clear_to_u8(LayoutClear::None), 0);
2377        assert_eq!(style_font_weight_to_u8(StyleFontWeight::Normal), 0);
2378        assert_eq!(style_font_style_to_u8(StyleFontStyle::Normal), 0);
2379        assert_eq!(style_text_align_to_u8(StyleTextAlign::Left), 4);
2380        assert_eq!(style_visibility_to_u8(StyleVisibility::Visible), 0);
2381        assert_eq!(style_white_space_to_u8(StyleWhiteSpace::Normal), 0);
2382        assert_eq!(style_direction_to_u8(StyleDirection::Ltr), 0);
2383        assert_eq!(style_vertical_align_to_u8(StyleVerticalAlign::Baseline), 0);
2384        assert_eq!(border_collapse_to_u8(StyleBorderCollapse::Separate), 0);
2385    }
2386
2387    // ========================================================================
2388    // Exhaustive round-trip: every variant of every enum must survive
2389    // encode → decode unchanged. Catches any reordering that maps two
2390    // different variants to the same u8, or any mask-width mismatch.
2391    // ========================================================================
2392
2393    macro_rules! roundtrip_all {
2394        ($name:ident, $to:ident, $from:ident, [$($variant:expr),+ $(,)?]) => {
2395            #[test]
2396            fn $name() {
2397                for v in [$($variant),+] {
2398                    let u = $to(v);
2399                    let decoded = $from(u);
2400                    assert_eq!(decoded, v, "{:?} != {:?} (via u8 = {})", decoded, v, u);
2401                }
2402            }
2403        };
2404    }
2405
2406    roundtrip_all!(rt_display, layout_display_to_u8, layout_display_from_u8, [
2407        LayoutDisplay::Block, LayoutDisplay::Inline, LayoutDisplay::InlineBlock,
2408        LayoutDisplay::Flex, LayoutDisplay::None, LayoutDisplay::InlineFlex,
2409        LayoutDisplay::Table, LayoutDisplay::InlineTable, LayoutDisplay::TableRowGroup,
2410        LayoutDisplay::TableHeaderGroup, LayoutDisplay::TableFooterGroup,
2411        LayoutDisplay::TableRow, LayoutDisplay::TableColumnGroup,
2412        LayoutDisplay::TableColumn, LayoutDisplay::TableCell,
2413        LayoutDisplay::TableCaption, LayoutDisplay::FlowRoot,
2414        LayoutDisplay::ListItem, LayoutDisplay::RunIn, LayoutDisplay::Marker,
2415        LayoutDisplay::Grid, LayoutDisplay::InlineGrid, LayoutDisplay::Contents,
2416    ]);
2417
2418    roundtrip_all!(rt_position, layout_position_to_u8, layout_position_from_u8, [
2419        LayoutPosition::Static, LayoutPosition::Relative, LayoutPosition::Absolute,
2420        LayoutPosition::Fixed, LayoutPosition::Sticky,
2421    ]);
2422
2423    roundtrip_all!(rt_float, layout_float_to_u8, layout_float_from_u8, [
2424        LayoutFloat::None, LayoutFloat::Left, LayoutFloat::Right,
2425    ]);
2426
2427    roundtrip_all!(rt_overflow, layout_overflow_to_u8, layout_overflow_from_u8, [
2428        LayoutOverflow::Visible, LayoutOverflow::Hidden, LayoutOverflow::Scroll,
2429        LayoutOverflow::Auto, LayoutOverflow::Clip,
2430    ]);
2431
2432    roundtrip_all!(rt_box_sizing, layout_box_sizing_to_u8, layout_box_sizing_from_u8, [
2433        LayoutBoxSizing::ContentBox, LayoutBoxSizing::BorderBox,
2434    ]);
2435
2436    roundtrip_all!(rt_flex_direction, layout_flex_direction_to_u8, layout_flex_direction_from_u8, [
2437        LayoutFlexDirection::Row, LayoutFlexDirection::RowReverse,
2438        LayoutFlexDirection::Column, LayoutFlexDirection::ColumnReverse,
2439    ]);
2440
2441    roundtrip_all!(rt_flex_wrap, layout_flex_wrap_to_u8, layout_flex_wrap_from_u8, [
2442        LayoutFlexWrap::NoWrap, LayoutFlexWrap::Wrap, LayoutFlexWrap::WrapReverse,
2443    ]);
2444
2445    roundtrip_all!(rt_justify_content, layout_justify_content_to_u8, layout_justify_content_from_u8, [
2446        LayoutJustifyContent::FlexStart, LayoutJustifyContent::FlexEnd,
2447        LayoutJustifyContent::Start, LayoutJustifyContent::End,
2448        LayoutJustifyContent::Center, LayoutJustifyContent::SpaceBetween,
2449        LayoutJustifyContent::SpaceAround, LayoutJustifyContent::SpaceEvenly,
2450    ]);
2451
2452    roundtrip_all!(rt_align_items, layout_align_items_to_u8, layout_align_items_from_u8, [
2453        LayoutAlignItems::Stretch, LayoutAlignItems::Center, LayoutAlignItems::Start,
2454        LayoutAlignItems::End, LayoutAlignItems::Baseline,
2455    ]);
2456
2457    roundtrip_all!(rt_align_self, layout_align_self_to_u8, layout_align_self_from_u8, [
2458        LayoutAlignSelf::Auto, LayoutAlignSelf::Stretch, LayoutAlignSelf::Center,
2459        LayoutAlignSelf::Start, LayoutAlignSelf::End, LayoutAlignSelf::Baseline,
2460    ]);
2461
2462    roundtrip_all!(rt_justify_self, layout_justify_self_to_u8, layout_justify_self_from_u8, [
2463        LayoutJustifySelf::Auto, LayoutJustifySelf::Start, LayoutJustifySelf::End,
2464        LayoutJustifySelf::Center, LayoutJustifySelf::Stretch,
2465    ]);
2466
2467    roundtrip_all!(rt_justify_items, layout_justify_items_to_u8, layout_justify_items_from_u8, [
2468        LayoutJustifyItems::Stretch, LayoutJustifyItems::Start,
2469        LayoutJustifyItems::End, LayoutJustifyItems::Center,
2470    ]);
2471
2472    roundtrip_all!(rt_grid_auto_flow, layout_grid_auto_flow_to_u8, layout_grid_auto_flow_from_u8, [
2473        LayoutGridAutoFlow::Row, LayoutGridAutoFlow::Column,
2474        LayoutGridAutoFlow::RowDense, LayoutGridAutoFlow::ColumnDense,
2475    ]);
2476
2477    roundtrip_all!(rt_align_content, layout_align_content_to_u8, layout_align_content_from_u8, [
2478        LayoutAlignContent::Stretch, LayoutAlignContent::Center,
2479        LayoutAlignContent::Start, LayoutAlignContent::End,
2480        LayoutAlignContent::SpaceBetween, LayoutAlignContent::SpaceAround,
2481    ]);
2482
2483    roundtrip_all!(rt_writing_mode, layout_writing_mode_to_u8, layout_writing_mode_from_u8, [
2484        LayoutWritingMode::HorizontalTb, LayoutWritingMode::VerticalRl,
2485        LayoutWritingMode::VerticalLr,
2486    ]);
2487
2488    roundtrip_all!(rt_clear, layout_clear_to_u8, layout_clear_from_u8, [
2489        LayoutClear::None, LayoutClear::Left, LayoutClear::Right, LayoutClear::Both,
2490    ]);
2491
2492    roundtrip_all!(rt_font_weight, style_font_weight_to_u8, style_font_weight_from_u8, [
2493        StyleFontWeight::Normal, StyleFontWeight::W100, StyleFontWeight::W200,
2494        StyleFontWeight::W300, StyleFontWeight::W500, StyleFontWeight::W600,
2495        StyleFontWeight::Bold, StyleFontWeight::W800, StyleFontWeight::W900,
2496        StyleFontWeight::Lighter, StyleFontWeight::Bolder,
2497    ]);
2498
2499    roundtrip_all!(rt_font_style, style_font_style_to_u8, style_font_style_from_u8, [
2500        StyleFontStyle::Normal, StyleFontStyle::Italic, StyleFontStyle::Oblique,
2501    ]);
2502
2503    roundtrip_all!(rt_text_align, style_text_align_to_u8, style_text_align_from_u8, [
2504        StyleTextAlign::Left, StyleTextAlign::Center, StyleTextAlign::Right,
2505        StyleTextAlign::Justify, StyleTextAlign::Start, StyleTextAlign::End,
2506    ]);
2507
2508    roundtrip_all!(rt_visibility, style_visibility_to_u8, style_visibility_from_u8, [
2509        StyleVisibility::Visible, StyleVisibility::Hidden, StyleVisibility::Collapse,
2510    ]);
2511
2512    roundtrip_all!(rt_white_space, style_white_space_to_u8, style_white_space_from_u8, [
2513        StyleWhiteSpace::Normal, StyleWhiteSpace::Pre, StyleWhiteSpace::Nowrap,
2514        StyleWhiteSpace::PreWrap, StyleWhiteSpace::PreLine, StyleWhiteSpace::BreakSpaces,
2515    ]);
2516
2517    roundtrip_all!(rt_direction, style_direction_to_u8, style_direction_from_u8, [
2518        StyleDirection::Ltr, StyleDirection::Rtl,
2519    ]);
2520
2521    roundtrip_all!(rt_vertical_align, style_vertical_align_to_u8, style_vertical_align_from_u8, [
2522        StyleVerticalAlign::Baseline, StyleVerticalAlign::Top, StyleVerticalAlign::Middle,
2523        StyleVerticalAlign::Bottom, StyleVerticalAlign::Sub, StyleVerticalAlign::Superscript,
2524        StyleVerticalAlign::TextTop, StyleVerticalAlign::TextBottom,
2525    ]);
2526
2527    roundtrip_all!(rt_border_collapse, border_collapse_to_u8, border_collapse_from_u8, [
2528        StyleBorderCollapse::Separate, StyleBorderCollapse::Collapse,
2529    ]);
2530
2531    // ========================================================================
2532    // Bit-layout safety: every encoder must produce a u8 whose bits all
2533    // fit inside the mask allocated for that property in the tier1 u64.
2534    // If an enum grows a new variant that overflows its mask, the encoded
2535    // bits would leak into the next property's slot and silently corrupt
2536    // unrelated state.
2537    // ========================================================================
2538
2539    #[test]
2540    fn test_encoded_u8_fits_in_tier1_mask() {
2541        fn assert_fits(name: &str, val: u8, mask: u64) {
2542            assert!(
2543                u64::from(val) & !mask == 0,
2544                "{name}: encoded u8 {val} overflows mask {mask:b}",
2545            );
2546        }
2547
2548        assert_fits("display", layout_display_to_u8(LayoutDisplay::Contents), DISPLAY_MASK);
2549        assert_fits("position", layout_position_to_u8(LayoutPosition::Sticky), POSITION_MASK);
2550        assert_fits("float", layout_float_to_u8(LayoutFloat::Right), FLOAT_MASK);
2551        assert_fits("overflow", layout_overflow_to_u8(LayoutOverflow::Clip), OVERFLOW_MASK);
2552        assert_fits("box_sizing", layout_box_sizing_to_u8(LayoutBoxSizing::BorderBox), BOX_SIZING_MASK);
2553        assert_fits("flex_direction", layout_flex_direction_to_u8(LayoutFlexDirection::ColumnReverse), FLEX_DIR_MASK);
2554        assert_fits("flex_wrap", layout_flex_wrap_to_u8(LayoutFlexWrap::WrapReverse), FLEX_WRAP_MASK);
2555        assert_fits("justify_content", layout_justify_content_to_u8(LayoutJustifyContent::SpaceEvenly), JUSTIFY_MASK);
2556        assert_fits("align_items", layout_align_items_to_u8(LayoutAlignItems::Baseline), ALIGN_MASK);
2557        assert_fits("align_self", layout_align_self_to_u8(LayoutAlignSelf::Baseline), ALIGN_SELF_MASK);
2558        assert_fits("justify_self", layout_justify_self_to_u8(LayoutJustifySelf::Stretch), JUSTIFY_SELF_MASK);
2559        assert_fits("justify_items", layout_justify_items_to_u8(LayoutJustifyItems::Center), JUSTIFY_ITEMS_MASK);
2560        assert_fits("grid_auto_flow", layout_grid_auto_flow_to_u8(LayoutGridAutoFlow::ColumnDense), GRID_AUTO_FLOW_MASK);
2561        assert_fits("align_content", layout_align_content_to_u8(LayoutAlignContent::SpaceAround), ALIGN_MASK);
2562        assert_fits("writing_mode", layout_writing_mode_to_u8(LayoutWritingMode::VerticalLr), WRITING_MODE_MASK);
2563        assert_fits("clear", layout_clear_to_u8(LayoutClear::Both), CLEAR_MASK);
2564        assert_fits("font_weight", style_font_weight_to_u8(StyleFontWeight::Bolder), FONT_WEIGHT_MASK);
2565        assert_fits("font_style", style_font_style_to_u8(StyleFontStyle::Oblique), FONT_STYLE_MASK);
2566        assert_fits("text_align", style_text_align_to_u8(StyleTextAlign::End), TEXT_ALIGN_MASK);
2567        assert_fits("visibility", style_visibility_to_u8(StyleVisibility::Collapse), VISIBILITY_MASK);
2568        assert_fits("white_space", style_white_space_to_u8(StyleWhiteSpace::BreakSpaces), WHITE_SPACE_MASK);
2569        assert_fits("direction", style_direction_to_u8(StyleDirection::Rtl), DIRECTION_MASK);
2570        assert_fits("vertical_align", style_vertical_align_to_u8(StyleVerticalAlign::TextBottom), VERTICAL_ALIGN_MASK);
2571        assert_fits("border_collapse", border_collapse_to_u8(StyleBorderCollapse::Collapse), BORDER_COLLAPSE_MASK);
2572    }
2573
2574    // ========================================================================
2575    // Empty tier1 decodes to all-initial — this is the core contract that
2576    // was violated by the pre-fix justify_items encoding. An empty u64 with
2577    // only TIER1_POPULATED_BIT set must decode every property to its CSS
2578    // initial value; this is how `build_compact_cache` can leave unspecified
2579    // properties at 0 and still produce the correct cascade result.
2580    // ========================================================================
2581
2582    #[test]
2583    fn test_empty_tier1_decodes_to_initial_values() {
2584        let t1 = TIER1_POPULATED_BIT; // populated, but zero content
2585        assert!(tier1_is_populated(t1));
2586        assert_eq!(decode_display(t1), LayoutDisplay::Block);
2587        assert_eq!(decode_position(t1), LayoutPosition::Static);
2588        assert_eq!(decode_float(t1), LayoutFloat::None);
2589        assert_eq!(decode_overflow_x(t1), LayoutOverflow::Visible);
2590        assert_eq!(decode_overflow_y(t1), LayoutOverflow::Visible);
2591        assert_eq!(decode_box_sizing(t1), LayoutBoxSizing::ContentBox);
2592        assert_eq!(decode_flex_direction(t1), LayoutFlexDirection::Row);
2593        assert_eq!(decode_flex_wrap(t1), LayoutFlexWrap::NoWrap);
2594        assert_eq!(decode_justify_content(t1), LayoutJustifyContent::FlexStart);
2595        assert_eq!(decode_align_items(t1), LayoutAlignItems::Stretch);
2596        assert_eq!(decode_align_content(t1), LayoutAlignContent::Stretch);
2597        assert_eq!(decode_writing_mode(t1), LayoutWritingMode::HorizontalTb);
2598        assert_eq!(decode_clear(t1), LayoutClear::None);
2599        assert_eq!(decode_font_weight(t1), StyleFontWeight::Normal);
2600        assert_eq!(decode_font_style(t1), StyleFontStyle::Normal);
2601        assert_eq!(decode_text_align(t1), StyleTextAlign::Start);
2602        assert_eq!(decode_visibility(t1), StyleVisibility::Visible);
2603        assert_eq!(decode_white_space(t1), StyleWhiteSpace::Normal);
2604        assert_eq!(decode_direction(t1), StyleDirection::Ltr);
2605        assert_eq!(decode_vertical_align(t1), StyleVerticalAlign::Baseline);
2606        assert_eq!(decode_border_collapse(t1), StyleBorderCollapse::Separate);
2607    }
2608}
2609
2610#[cfg(test)]
2611#[allow(
2612    clippy::float_cmp,
2613    clippy::unreadable_literal,
2614    clippy::too_many_lines,
2615    clippy::cast_precision_loss
2616)]
2617mod autotest_generated {
2618    use super::*;
2619    use crate::props::basic::length::PercentageValue;
2620
2621    // =========================================================================
2622    // Shared fixtures
2623    // =========================================================================
2624
2625    const ALL_DISPLAY: [LayoutDisplay; 23] = [
2626        LayoutDisplay::Block,
2627        LayoutDisplay::Inline,
2628        LayoutDisplay::InlineBlock,
2629        LayoutDisplay::Flex,
2630        LayoutDisplay::None,
2631        LayoutDisplay::InlineFlex,
2632        LayoutDisplay::Table,
2633        LayoutDisplay::InlineTable,
2634        LayoutDisplay::TableRowGroup,
2635        LayoutDisplay::TableHeaderGroup,
2636        LayoutDisplay::TableFooterGroup,
2637        LayoutDisplay::TableRow,
2638        LayoutDisplay::TableColumnGroup,
2639        LayoutDisplay::TableColumn,
2640        LayoutDisplay::TableCell,
2641        LayoutDisplay::TableCaption,
2642        LayoutDisplay::FlowRoot,
2643        LayoutDisplay::ListItem,
2644        LayoutDisplay::RunIn,
2645        LayoutDisplay::Marker,
2646        LayoutDisplay::Grid,
2647        LayoutDisplay::InlineGrid,
2648        LayoutDisplay::Contents,
2649    ];
2650
2651    const ALL_BORDER_STYLE: [BorderStyle; 10] = [
2652        BorderStyle::None,
2653        BorderStyle::Solid,
2654        BorderStyle::Double,
2655        BorderStyle::Dotted,
2656        BorderStyle::Dashed,
2657        BorderStyle::Hidden,
2658        BorderStyle::Groove,
2659        BorderStyle::Ridge,
2660        BorderStyle::Inset,
2661        BorderStyle::Outset,
2662    ];
2663
2664    const ALL_SIZE_METRIC: [SizeMetric; 12] = [
2665        SizeMetric::Px,
2666        SizeMetric::Pt,
2667        SizeMetric::Em,
2668        SizeMetric::Rem,
2669        SizeMetric::In,
2670        SizeMetric::Cm,
2671        SizeMetric::Mm,
2672        SizeMetric::Percent,
2673        SizeMetric::Vw,
2674        SizeMetric::Vh,
2675        SizeMetric::Vmin,
2676        SizeMetric::Vmax,
2677    ];
2678
2679    /// Build a `PixelValue` straight from the raw fixed-point isize (×1000),
2680    /// bypassing the f32 constructor so boundary rows are exact.
2681    fn pv_raw(metric: SizeMetric, raw: isize) -> PixelValue {
2682        PixelValue {
2683            metric,
2684            number: FloatValue { number: raw },
2685        }
2686    }
2687
2688    /// All 21 tier-1 enum properties as one struct, so a decode can be checked
2689    /// field-by-field against exactly what was encoded.
2690    #[derive(Debug, Copy, Clone, PartialEq, Eq)]
2691    struct T1 {
2692        display: LayoutDisplay,
2693        position: LayoutPosition,
2694        float: LayoutFloat,
2695        overflow_x: LayoutOverflow,
2696        overflow_y: LayoutOverflow,
2697        box_sizing: LayoutBoxSizing,
2698        flex_direction: LayoutFlexDirection,
2699        flex_wrap: LayoutFlexWrap,
2700        justify_content: LayoutJustifyContent,
2701        align_items: LayoutAlignItems,
2702        align_content: LayoutAlignContent,
2703        writing_mode: LayoutWritingMode,
2704        clear: LayoutClear,
2705        font_weight: StyleFontWeight,
2706        font_style: StyleFontStyle,
2707        text_align: StyleTextAlign,
2708        visibility: StyleVisibility,
2709        white_space: StyleWhiteSpace,
2710        direction: StyleDirection,
2711        vertical_align: StyleVerticalAlign,
2712        border_collapse: StyleBorderCollapse,
2713    }
2714
2715    impl T1 {
2716        /// Every field at its CSS initial value (the u8-0 slot).
2717        const fn initial() -> Self {
2718            Self {
2719                display: LayoutDisplay::Block,
2720                position: LayoutPosition::Static,
2721                float: LayoutFloat::None,
2722                overflow_x: LayoutOverflow::Visible,
2723                overflow_y: LayoutOverflow::Visible,
2724                box_sizing: LayoutBoxSizing::ContentBox,
2725                flex_direction: LayoutFlexDirection::Row,
2726                flex_wrap: LayoutFlexWrap::NoWrap,
2727                justify_content: LayoutJustifyContent::FlexStart,
2728                align_items: LayoutAlignItems::Stretch,
2729                align_content: LayoutAlignContent::Stretch,
2730                writing_mode: LayoutWritingMode::HorizontalTb,
2731                clear: LayoutClear::None,
2732                font_weight: StyleFontWeight::Normal,
2733                font_style: StyleFontStyle::Normal,
2734                text_align: StyleTextAlign::Start,
2735                visibility: StyleVisibility::Visible,
2736                white_space: StyleWhiteSpace::Normal,
2737                direction: StyleDirection::Ltr,
2738                vertical_align: StyleVerticalAlign::Baseline,
2739                border_collapse: StyleBorderCollapse::Separate,
2740            }
2741        }
2742
2743        /// Every field at its highest-numbered variant — the worst case for a
2744        /// mask that is one bit too narrow.
2745        const fn saturated() -> Self {
2746            Self {
2747                display: LayoutDisplay::Contents,
2748                position: LayoutPosition::Sticky,
2749                float: LayoutFloat::Right,
2750                overflow_x: LayoutOverflow::Clip,
2751                overflow_y: LayoutOverflow::Clip,
2752                box_sizing: LayoutBoxSizing::BorderBox,
2753                flex_direction: LayoutFlexDirection::ColumnReverse,
2754                flex_wrap: LayoutFlexWrap::WrapReverse,
2755                justify_content: LayoutJustifyContent::SpaceEvenly,
2756                align_items: LayoutAlignItems::Baseline,
2757                align_content: LayoutAlignContent::SpaceAround,
2758                writing_mode: LayoutWritingMode::VerticalLr,
2759                clear: LayoutClear::Both,
2760                font_weight: StyleFontWeight::Bolder,
2761                font_style: StyleFontStyle::Oblique,
2762                text_align: StyleTextAlign::End,
2763                visibility: StyleVisibility::Collapse,
2764                white_space: StyleWhiteSpace::BreakSpaces,
2765                direction: StyleDirection::Rtl,
2766                vertical_align: StyleVerticalAlign::TextBottom,
2767                border_collapse: StyleBorderCollapse::Collapse,
2768            }
2769        }
2770
2771        fn encode(self) -> u64 {
2772            encode_tier1(
2773                self.display,
2774                self.position,
2775                self.float,
2776                self.overflow_x,
2777                self.overflow_y,
2778                self.box_sizing,
2779                self.flex_direction,
2780                self.flex_wrap,
2781                self.justify_content,
2782                self.align_items,
2783                self.align_content,
2784                self.writing_mode,
2785                self.clear,
2786                self.font_weight,
2787                self.font_style,
2788                self.text_align,
2789                self.visibility,
2790                self.white_space,
2791                self.direction,
2792                self.vertical_align,
2793                self.border_collapse,
2794            )
2795        }
2796
2797        fn decode(t1: u64) -> Self {
2798            Self {
2799                display: decode_display(t1),
2800                position: decode_position(t1),
2801                float: decode_float(t1),
2802                overflow_x: decode_overflow_x(t1),
2803                overflow_y: decode_overflow_y(t1),
2804                box_sizing: decode_box_sizing(t1),
2805                flex_direction: decode_flex_direction(t1),
2806                flex_wrap: decode_flex_wrap(t1),
2807                justify_content: decode_justify_content(t1),
2808                align_items: decode_align_items(t1),
2809                align_content: decode_align_content(t1),
2810                writing_mode: decode_writing_mode(t1),
2811                clear: decode_clear(t1),
2812                font_weight: decode_font_weight(t1),
2813                font_style: decode_font_style(t1),
2814                text_align: decode_text_align(t1),
2815                visibility: decode_visibility(t1),
2816                white_space: decode_white_space(t1),
2817                direction: decode_direction(t1),
2818                vertical_align: decode_vertical_align(t1),
2819                border_collapse: decode_border_collapse(t1),
2820            }
2821        }
2822    }
2823
2824    // =========================================================================
2825    // u8 decoders — every byte outside the variant range must fall back to the
2826    // CSS initial value (the u8-0 slot), never panic, never alias a real variant
2827    // =========================================================================
2828
2829    /// `first_invalid` = the first u8 that has no variant. Every byte from there
2830    /// to `u8::MAX` must decode to `initial`.
2831    fn assert_u8_fallback<T: PartialEq + core::fmt::Debug + Copy>(
2832        name: &str,
2833        decode: fn(u8) -> T,
2834        first_invalid: u8,
2835        initial: T,
2836    ) {
2837        for v in first_invalid..=u8::MAX {
2838            assert_eq!(
2839                decode(v),
2840                initial,
2841                "{name}: out-of-range byte {v} must fall back to the CSS initial value",
2842            );
2843        }
2844    }
2845
2846    #[test]
2847    fn out_of_range_u8_falls_back_to_css_initial_for_every_enum() {
2848        assert_u8_fallback("display", layout_display_from_u8, 23, LayoutDisplay::Block);
2849        assert_u8_fallback("position", layout_position_from_u8, 5, LayoutPosition::Static);
2850        assert_u8_fallback("float", layout_float_from_u8, 3, LayoutFloat::None);
2851        assert_u8_fallback("overflow", layout_overflow_from_u8, 5, LayoutOverflow::Visible);
2852        assert_u8_fallback(
2853            "box_sizing",
2854            layout_box_sizing_from_u8,
2855            2,
2856            LayoutBoxSizing::ContentBox,
2857        );
2858        assert_u8_fallback(
2859            "flex_direction",
2860            layout_flex_direction_from_u8,
2861            4,
2862            LayoutFlexDirection::Row,
2863        );
2864        assert_u8_fallback("flex_wrap", layout_flex_wrap_from_u8, 3, LayoutFlexWrap::NoWrap);
2865        assert_u8_fallback(
2866            "justify_content",
2867            layout_justify_content_from_u8,
2868            8,
2869            LayoutJustifyContent::FlexStart,
2870        );
2871        assert_u8_fallback(
2872            "align_items",
2873            layout_align_items_from_u8,
2874            5,
2875            LayoutAlignItems::Stretch,
2876        );
2877        assert_u8_fallback("align_self", layout_align_self_from_u8, 6, LayoutAlignSelf::Auto);
2878        assert_u8_fallback(
2879            "justify_self",
2880            layout_justify_self_from_u8,
2881            5,
2882            LayoutJustifySelf::Auto,
2883        );
2884        assert_u8_fallback(
2885            "justify_items",
2886            layout_justify_items_from_u8,
2887            4,
2888            LayoutJustifyItems::Stretch,
2889        );
2890        assert_u8_fallback(
2891            "grid_auto_flow",
2892            layout_grid_auto_flow_from_u8,
2893            4,
2894            LayoutGridAutoFlow::Row,
2895        );
2896        assert_u8_fallback(
2897            "align_content",
2898            layout_align_content_from_u8,
2899            6,
2900            LayoutAlignContent::Stretch,
2901        );
2902        assert_u8_fallback(
2903            "writing_mode",
2904            layout_writing_mode_from_u8,
2905            3,
2906            LayoutWritingMode::HorizontalTb,
2907        );
2908        assert_u8_fallback("clear", layout_clear_from_u8, 4, LayoutClear::None);
2909        assert_u8_fallback(
2910            "font_weight",
2911            style_font_weight_from_u8,
2912            11,
2913            StyleFontWeight::Normal,
2914        );
2915        assert_u8_fallback("font_style", style_font_style_from_u8, 3, StyleFontStyle::Normal);
2916        assert_u8_fallback("text_align", style_text_align_from_u8, 6, StyleTextAlign::Start);
2917        assert_u8_fallback(
2918            "visibility",
2919            style_visibility_from_u8,
2920            3,
2921            StyleVisibility::Visible,
2922        );
2923        assert_u8_fallback(
2924            "white_space",
2925            style_white_space_from_u8,
2926            6,
2927            StyleWhiteSpace::Normal,
2928        );
2929        assert_u8_fallback("direction", style_direction_from_u8, 2, StyleDirection::Ltr);
2930        assert_u8_fallback(
2931            "vertical_align",
2932            style_vertical_align_from_u8,
2933            8,
2934            StyleVerticalAlign::Baseline,
2935        );
2936        assert_u8_fallback(
2937            "border_collapse",
2938            border_collapse_from_u8,
2939            2,
2940            StyleBorderCollapse::Separate,
2941        );
2942        assert_u8_fallback("border_style", border_style_from_u8, 10, BorderStyle::None);
2943        assert_u8_fallback("size_metric", size_metric_from_u8, 12, SizeMetric::Px);
2944    }
2945
2946    #[test]
2947    fn display_documented_sentinel_31_decodes_to_block() {
2948        // The module doc promises 0x1F is the "look it up in the slow path"
2949        // sentinel and that decoding it yields the default rather than a
2950        // garbage variant. 31 is also DISPLAY_MASK, so a saturated tier1 u64
2951        // lands exactly here.
2952        assert_eq!(layout_display_from_u8(31), LayoutDisplay::Block);
2953        assert_eq!(layout_display_from_u8(u8::MAX), LayoutDisplay::Block);
2954    }
2955
2956    #[test]
2957    fn every_variant_of_every_enum_fits_its_tier1_mask() {
2958        // The existing suite checks only the highest variant per enum. If a new
2959        // variant is inserted in the middle with a hand-written u8 above the
2960        // mask width, that check still passes while the encoding silently
2961        // corrupts the neighbouring bit run. Check the whole variant set.
2962        fn fits(name: &str, val: u8, mask: u64) {
2963            assert!(
2964                u64::from(val) & !mask == 0,
2965                "{name}: encoded u8 {val} does not fit mask {mask:#b}",
2966            );
2967        }
2968
2969        for v in ALL_DISPLAY {
2970            fits("display", layout_display_to_u8(v), DISPLAY_MASK);
2971        }
2972        for v in [
2973            LayoutPosition::Static,
2974            LayoutPosition::Relative,
2975            LayoutPosition::Absolute,
2976            LayoutPosition::Fixed,
2977            LayoutPosition::Sticky,
2978        ] {
2979            fits("position", layout_position_to_u8(v), POSITION_MASK);
2980        }
2981        for v in [LayoutFloat::None, LayoutFloat::Left, LayoutFloat::Right] {
2982            fits("float", layout_float_to_u8(v), FLOAT_MASK);
2983        }
2984        for v in [
2985            LayoutOverflow::Visible,
2986            LayoutOverflow::Hidden,
2987            LayoutOverflow::Scroll,
2988            LayoutOverflow::Auto,
2989            LayoutOverflow::Clip,
2990        ] {
2991            fits("overflow", layout_overflow_to_u8(v), OVERFLOW_MASK);
2992        }
2993        for v in [LayoutBoxSizing::ContentBox, LayoutBoxSizing::BorderBox] {
2994            fits("box_sizing", layout_box_sizing_to_u8(v), BOX_SIZING_MASK);
2995        }
2996        for v in [
2997            LayoutFlexDirection::Row,
2998            LayoutFlexDirection::RowReverse,
2999            LayoutFlexDirection::Column,
3000            LayoutFlexDirection::ColumnReverse,
3001        ] {
3002            fits("flex_direction", layout_flex_direction_to_u8(v), FLEX_DIR_MASK);
3003        }
3004        for v in [
3005            LayoutFlexWrap::NoWrap,
3006            LayoutFlexWrap::Wrap,
3007            LayoutFlexWrap::WrapReverse,
3008        ] {
3009            fits("flex_wrap", layout_flex_wrap_to_u8(v), FLEX_WRAP_MASK);
3010        }
3011        for v in [
3012            LayoutJustifyContent::FlexStart,
3013            LayoutJustifyContent::FlexEnd,
3014            LayoutJustifyContent::Start,
3015            LayoutJustifyContent::End,
3016            LayoutJustifyContent::Center,
3017            LayoutJustifyContent::SpaceBetween,
3018            LayoutJustifyContent::SpaceAround,
3019            LayoutJustifyContent::SpaceEvenly,
3020        ] {
3021            fits("justify_content", layout_justify_content_to_u8(v), JUSTIFY_MASK);
3022        }
3023        for v in [
3024            LayoutAlignItems::Stretch,
3025            LayoutAlignItems::Center,
3026            LayoutAlignItems::Start,
3027            LayoutAlignItems::End,
3028            LayoutAlignItems::Baseline,
3029        ] {
3030            fits("align_items", layout_align_items_to_u8(v), ALIGN_MASK);
3031        }
3032        for v in [
3033            LayoutAlignSelf::Auto,
3034            LayoutAlignSelf::Stretch,
3035            LayoutAlignSelf::Center,
3036            LayoutAlignSelf::Start,
3037            LayoutAlignSelf::End,
3038            LayoutAlignSelf::Baseline,
3039        ] {
3040            fits("align_self", layout_align_self_to_u8(v), ALIGN_SELF_MASK);
3041        }
3042        for v in [
3043            LayoutJustifySelf::Auto,
3044            LayoutJustifySelf::Start,
3045            LayoutJustifySelf::End,
3046            LayoutJustifySelf::Center,
3047            LayoutJustifySelf::Stretch,
3048        ] {
3049            fits("justify_self", layout_justify_self_to_u8(v), JUSTIFY_SELF_MASK);
3050        }
3051        for v in [
3052            LayoutJustifyItems::Stretch,
3053            LayoutJustifyItems::Start,
3054            LayoutJustifyItems::End,
3055            LayoutJustifyItems::Center,
3056        ] {
3057            fits("justify_items", layout_justify_items_to_u8(v), JUSTIFY_ITEMS_MASK);
3058        }
3059        for v in [
3060            LayoutGridAutoFlow::Row,
3061            LayoutGridAutoFlow::Column,
3062            LayoutGridAutoFlow::RowDense,
3063            LayoutGridAutoFlow::ColumnDense,
3064        ] {
3065            fits("grid_auto_flow", layout_grid_auto_flow_to_u8(v), GRID_AUTO_FLOW_MASK);
3066        }
3067        for v in [
3068            LayoutAlignContent::Stretch,
3069            LayoutAlignContent::Center,
3070            LayoutAlignContent::Start,
3071            LayoutAlignContent::End,
3072            LayoutAlignContent::SpaceBetween,
3073            LayoutAlignContent::SpaceAround,
3074        ] {
3075            fits("align_content", layout_align_content_to_u8(v), ALIGN_MASK);
3076        }
3077        for v in [
3078            LayoutWritingMode::HorizontalTb,
3079            LayoutWritingMode::VerticalRl,
3080            LayoutWritingMode::VerticalLr,
3081        ] {
3082            fits("writing_mode", layout_writing_mode_to_u8(v), WRITING_MODE_MASK);
3083        }
3084        for v in [
3085            LayoutClear::None,
3086            LayoutClear::Left,
3087            LayoutClear::Right,
3088            LayoutClear::Both,
3089        ] {
3090            fits("clear", layout_clear_to_u8(v), CLEAR_MASK);
3091        }
3092        for v in [
3093            StyleFontWeight::Normal,
3094            StyleFontWeight::W100,
3095            StyleFontWeight::W200,
3096            StyleFontWeight::W300,
3097            StyleFontWeight::W500,
3098            StyleFontWeight::W600,
3099            StyleFontWeight::Bold,
3100            StyleFontWeight::W800,
3101            StyleFontWeight::W900,
3102            StyleFontWeight::Lighter,
3103            StyleFontWeight::Bolder,
3104        ] {
3105            fits("font_weight", style_font_weight_to_u8(v), FONT_WEIGHT_MASK);
3106        }
3107        for v in [
3108            StyleFontStyle::Normal,
3109            StyleFontStyle::Italic,
3110            StyleFontStyle::Oblique,
3111        ] {
3112            fits("font_style", style_font_style_to_u8(v), FONT_STYLE_MASK);
3113        }
3114        for v in [
3115            StyleTextAlign::Left,
3116            StyleTextAlign::Center,
3117            StyleTextAlign::Right,
3118            StyleTextAlign::Justify,
3119            StyleTextAlign::Start,
3120            StyleTextAlign::End,
3121        ] {
3122            fits("text_align", style_text_align_to_u8(v), TEXT_ALIGN_MASK);
3123        }
3124        for v in [
3125            StyleVisibility::Visible,
3126            StyleVisibility::Hidden,
3127            StyleVisibility::Collapse,
3128        ] {
3129            fits("visibility", style_visibility_to_u8(v), VISIBILITY_MASK);
3130        }
3131        for v in [
3132            StyleWhiteSpace::Normal,
3133            StyleWhiteSpace::Pre,
3134            StyleWhiteSpace::Nowrap,
3135            StyleWhiteSpace::PreWrap,
3136            StyleWhiteSpace::PreLine,
3137            StyleWhiteSpace::BreakSpaces,
3138        ] {
3139            fits("white_space", style_white_space_to_u8(v), WHITE_SPACE_MASK);
3140        }
3141        for v in [StyleDirection::Ltr, StyleDirection::Rtl] {
3142            fits("direction", style_direction_to_u8(v), DIRECTION_MASK);
3143        }
3144        for v in [
3145            StyleVerticalAlign::Baseline,
3146            StyleVerticalAlign::Top,
3147            StyleVerticalAlign::Middle,
3148            StyleVerticalAlign::Bottom,
3149            StyleVerticalAlign::Sub,
3150            StyleVerticalAlign::Superscript,
3151            StyleVerticalAlign::TextTop,
3152            StyleVerticalAlign::TextBottom,
3153            StyleVerticalAlign::Percentage(PercentageValue::new(50.0)),
3154            StyleVerticalAlign::Length(PixelValue::px(4.0)),
3155        ] {
3156            fits("vertical_align", style_vertical_align_to_u8(v), VERTICAL_ALIGN_MASK);
3157        }
3158        for v in [StyleBorderCollapse::Separate, StyleBorderCollapse::Collapse] {
3159            fits("border_collapse", border_collapse_to_u8(v), BORDER_COLLAPSE_MASK);
3160        }
3161        // border-style gets a 4-bit nibble inside the packed u16, not a tier1 mask.
3162        for v in ALL_BORDER_STYLE {
3163            fits("border_style", border_style_to_u8(v), 0x0F);
3164        }
3165        // SizeMetric gets the low 4 bits of the pixel-value u32.
3166        for v in ALL_SIZE_METRIC {
3167            fits("size_metric", size_metric_to_u8(v), 0x0F);
3168        }
3169    }
3170
3171    #[test]
3172    fn vertical_align_percentage_and_length_collapse_to_baseline() {
3173        // Documented lossy fallback: the 3-bit tier1 slot cannot carry a
3174        // length/percentage payload, so these must encode as 0 (Baseline) and
3175        // the caller is expected to take the slow path. What must NOT happen is
3176        // an out-of-range u8 leaking into the border-collapse bit next door.
3177        for v in [
3178            StyleVerticalAlign::Percentage(PercentageValue::new(0.0)),
3179            StyleVerticalAlign::Percentage(PercentageValue::new(-100.0)),
3180            StyleVerticalAlign::Percentage(PercentageValue::new(1e9)),
3181            StyleVerticalAlign::Length(PixelValue::px(0.0)),
3182            StyleVerticalAlign::Length(PixelValue::px(-1e9)),
3183        ] {
3184            assert_eq!(style_vertical_align_to_u8(v), 0);
3185            assert_eq!(
3186                style_vertical_align_from_u8(style_vertical_align_to_u8(v)),
3187                StyleVerticalAlign::Baseline,
3188            );
3189        }
3190
3191        // …and the collapse must not disturb the neighbouring border-collapse bit.
3192        let mut t = T1::initial();
3193        t.vertical_align = StyleVerticalAlign::Percentage(PercentageValue::new(150.0));
3194        t.border_collapse = StyleBorderCollapse::Collapse;
3195        let encoded = t.encode();
3196        assert_eq!(decode_vertical_align(encoded), StyleVerticalAlign::Baseline);
3197        assert_eq!(decode_border_collapse(encoded), StyleBorderCollapse::Collapse);
3198    }
3199
3200    // =========================================================================
3201    // Tier1 u64 packing — field isolation, saturation, arbitrary input
3202    // =========================================================================
3203
3204    #[test]
3205    fn tier1_each_field_at_max_does_not_leak_into_any_other_field() {
3206        // One field at its highest variant, all others at CSS initial. If a
3207        // shift/mask pair is wrong, the extra bits land in a neighbour and that
3208        // neighbour decodes to something other than its initial value.
3209        let sat = T1::saturated();
3210        let mut cases: Vec<(&str, T1)> = Vec::new();
3211
3212        macro_rules! case {
3213            ($field:ident) => {{
3214                let mut t = T1::initial();
3215                t.$field = sat.$field;
3216                cases.push((stringify!($field), t));
3217            }};
3218        }
3219
3220        case!(display);
3221        case!(position);
3222        case!(float);
3223        case!(overflow_x);
3224        case!(overflow_y);
3225        case!(box_sizing);
3226        case!(flex_direction);
3227        case!(flex_wrap);
3228        case!(justify_content);
3229        case!(align_items);
3230        case!(align_content);
3231        case!(writing_mode);
3232        case!(clear);
3233        case!(font_weight);
3234        case!(font_style);
3235        case!(text_align);
3236        case!(visibility);
3237        case!(white_space);
3238        case!(direction);
3239        case!(vertical_align);
3240        case!(border_collapse);
3241
3242        assert_eq!(cases.len(), 21, "every tier1 field must be covered");
3243
3244        for (name, expected) in cases {
3245            let decoded = T1::decode(expected.encode());
3246            assert_eq!(
3247                decoded, expected,
3248                "{name} at its max variant leaked into another tier1 field",
3249            );
3250        }
3251    }
3252
3253    #[test]
3254    fn tier1_all_fields_saturated_roundtrips() {
3255        let sat = T1::saturated();
3256        assert_eq!(T1::decode(sat.encode()), sat);
3257        assert!(tier1_is_populated(sat.encode()));
3258    }
3259
3260    #[test]
3261    fn tier1_encode_never_touches_the_grid_bits_or_bits_above_63() {
3262        // encode_tier1 owns bits [52:0] plus the populated flag at bit 63.
3263        // Bits [62:53] belong to align-self / justify-self / grid-auto-flow /
3264        // justify-items, which the cache builder ORs in separately. If
3265        // encode_tier1 ever spills into that window it silently rewrites a grid
3266        // property that it never received as an argument.
3267        const GRID_WINDOW: u64 = 0x3FF << 53; // bits 53..=62
3268
3269        for t in [T1::initial(), T1::saturated()] {
3270            let encoded = t.encode();
3271            assert_eq!(
3272                encoded & GRID_WINDOW,
3273                0,
3274                "encode_tier1 wrote into the grid bit window [62:53]",
3275            );
3276            assert_eq!(encoded & TIER1_POPULATED_BIT, TIER1_POPULATED_BIT);
3277        }
3278
3279        // The grid fields must survive being ORed on top of a saturated tier1.
3280        let base = T1::saturated().encode();
3281        let with_grid = base
3282            | ((u64::from(layout_align_self_to_u8(LayoutAlignSelf::Baseline))) << ALIGN_SELF_SHIFT)
3283            | ((u64::from(layout_justify_self_to_u8(LayoutJustifySelf::Stretch)))
3284                << JUSTIFY_SELF_SHIFT)
3285            | ((u64::from(layout_grid_auto_flow_to_u8(LayoutGridAutoFlow::ColumnDense)))
3286                << GRID_AUTO_FLOW_SHIFT)
3287            | ((u64::from(layout_justify_items_to_u8(LayoutJustifyItems::Center)))
3288                << JUSTIFY_ITEMS_SHIFT);
3289
3290        assert_eq!(T1::decode(with_grid), T1::saturated());
3291        assert_eq!(
3292            layout_align_self_from_u8(((with_grid >> ALIGN_SELF_SHIFT) & ALIGN_SELF_MASK) as u8),
3293            LayoutAlignSelf::Baseline,
3294        );
3295        assert_eq!(
3296            layout_justify_self_from_u8(
3297                ((with_grid >> JUSTIFY_SELF_SHIFT) & JUSTIFY_SELF_MASK) as u8
3298            ),
3299            LayoutJustifySelf::Stretch,
3300        );
3301        assert_eq!(
3302            layout_grid_auto_flow_from_u8(
3303                ((with_grid >> GRID_AUTO_FLOW_SHIFT) & GRID_AUTO_FLOW_MASK) as u8
3304            ),
3305            LayoutGridAutoFlow::ColumnDense,
3306        );
3307        assert_eq!(
3308            layout_justify_items_from_u8(
3309                ((with_grid >> JUSTIFY_ITEMS_SHIFT) & JUSTIFY_ITEMS_MASK) as u8
3310            ),
3311            LayoutJustifyItems::Center,
3312        );
3313    }
3314
3315    #[test]
3316    fn tier1_zero_is_unpopulated_but_still_decodes_to_css_initial() {
3317        // A `with_capacity`-allocated cache holds 0 for every node until the
3318        // builder fills it in. Reading such a node must be safe and yield the
3319        // CSS initial value, not a garbage variant.
3320        assert!(!tier1_is_populated(0));
3321        assert_eq!(T1::decode(0), T1::initial());
3322    }
3323
3324    #[test]
3325    fn tier1_decodes_arbitrary_u64_deterministically_without_panic() {
3326        // u64::MAX means every mask reads all-ones. Each decoder must clamp to a
3327        // real variant (usually the initial value via the `_` arm) rather than
3328        // panic or produce an out-of-range discriminant.
3329        let m = u64::MAX;
3330        assert!(tier1_is_populated(m));
3331        assert_eq!(
3332            T1::decode(m),
3333            T1 {
3334                display: LayoutDisplay::Block,          // 31 → fallback
3335                position: LayoutPosition::Static,       // 7  → fallback
3336                float: LayoutFloat::None,               // 3  → fallback
3337                overflow_x: LayoutOverflow::Visible,    // 7  → fallback
3338                overflow_y: LayoutOverflow::Visible,    // 7  → fallback
3339                box_sizing: LayoutBoxSizing::BorderBox, // 1  → real variant
3340                flex_direction: LayoutFlexDirection::ColumnReverse, // 3 → real
3341                flex_wrap: LayoutFlexWrap::NoWrap,      // 3  → fallback
3342                justify_content: LayoutJustifyContent::SpaceEvenly, // 7 → real
3343                align_items: LayoutAlignItems::Stretch, // 7  → fallback
3344                align_content: LayoutAlignContent::Stretch, // 7 → fallback
3345                writing_mode: LayoutWritingMode::HorizontalTb, // 3 → fallback
3346                clear: LayoutClear::Both,               // 3  → real variant
3347                font_weight: StyleFontWeight::Normal,   // 15 → fallback
3348                font_style: StyleFontStyle::Normal,     // 3  → fallback
3349                text_align: StyleTextAlign::Start,      // 7  → fallback
3350                visibility: StyleVisibility::Visible,   // 3  → fallback
3351                white_space: StyleWhiteSpace::Normal,   // 7  → fallback
3352                direction: StyleDirection::Rtl,         // 1  → real variant
3353                vertical_align: StyleVerticalAlign::TextBottom, // 7 → real
3354                border_collapse: StyleBorderCollapse::Collapse, // 1 → real
3355            },
3356        );
3357
3358        // A deterministic sweep of adversarial bit patterns: none may panic.
3359        let mut x: u64 = 0x9E37_79B9_7F4A_7C15;
3360        for _ in 0..4096 {
3361            let _ = T1::decode(x);
3362            let _ = tier1_is_populated(x);
3363            x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
3364        }
3365        for x in [0u64, 1, u64::MAX, 0xAAAA_AAAA_AAAA_AAAA, 0x5555_5555_5555_5555] {
3366            let _ = T1::decode(x);
3367        }
3368    }
3369
3370    // =========================================================================
3371    // Packed border styles (u16, four nibbles)
3372    // =========================================================================
3373
3374    #[test]
3375    fn border_styles_packed_roundtrip_for_all_10000_combinations() {
3376        for top in ALL_BORDER_STYLE {
3377            for right in ALL_BORDER_STYLE {
3378                for bottom in ALL_BORDER_STYLE {
3379                    for left in ALL_BORDER_STYLE {
3380                        let packed = encode_border_styles_packed(top, right, bottom, left);
3381                        assert_eq!(decode_border_top_style(packed), top);
3382                        assert_eq!(decode_border_right_style(packed), right);
3383                        assert_eq!(decode_border_bottom_style(packed), bottom);
3384                        assert_eq!(decode_border_left_style(packed), left);
3385                    }
3386                }
3387            }
3388        }
3389    }
3390
3391    #[test]
3392    fn border_styles_packed_nibbles_do_not_alias() {
3393        // Only the top nibble set — the other three must read as None (0),
3394        // not pick up bits from their neighbours.
3395        let packed = encode_border_styles_packed(
3396            BorderStyle::Outset, // 9 — the widest valid nibble
3397            BorderStyle::None,
3398            BorderStyle::None,
3399            BorderStyle::None,
3400        );
3401        assert_eq!(packed, 0x0009);
3402        assert_eq!(decode_border_top_style(packed), BorderStyle::Outset);
3403        assert_eq!(decode_border_right_style(packed), BorderStyle::None);
3404        assert_eq!(decode_border_bottom_style(packed), BorderStyle::None);
3405        assert_eq!(decode_border_left_style(packed), BorderStyle::None);
3406
3407        let packed = encode_border_styles_packed(
3408            BorderStyle::None,
3409            BorderStyle::None,
3410            BorderStyle::None,
3411            BorderStyle::Outset,
3412        );
3413        assert_eq!(packed, 0x9000);
3414        assert_eq!(decode_border_left_style(packed), BorderStyle::Outset);
3415        assert_eq!(decode_border_top_style(packed), BorderStyle::None);
3416    }
3417
3418    #[test]
3419    fn border_styles_packed_decodes_garbage_u16_without_panic() {
3420        // Nibbles 10..=15 have no variant. `0xFFFF` (an all-ones cold-tier row,
3421        // e.g. from a misinitialised buffer) must decode to None everywhere.
3422        for packed in [0u16, 0xFFFF, 0xAAAA, 0x5555, u16::MAX / 2] {
3423            let _ = decode_border_top_style(packed);
3424            let _ = decode_border_right_style(packed);
3425            let _ = decode_border_bottom_style(packed);
3426            let _ = decode_border_left_style(packed);
3427        }
3428        assert_eq!(decode_border_top_style(0xFFFF), BorderStyle::None);
3429        assert_eq!(decode_border_right_style(0xFFFF), BorderStyle::None);
3430        assert_eq!(decode_border_bottom_style(0xFFFF), BorderStyle::None);
3431        assert_eq!(decode_border_left_style(0xFFFF), BorderStyle::None);
3432        // Exhaustive: no u16 may panic any of the four decoders.
3433        for packed in 0..=u16::MAX {
3434            let _ = decode_border_top_style(packed);
3435            let _ = decode_border_left_style(packed);
3436        }
3437    }
3438
3439    // =========================================================================
3440    // Colors (u32 0xRRGGBBAA)
3441    // =========================================================================
3442
3443    #[test]
3444    fn color_u32_channel_order_is_rrggbbaa() {
3445        let c = ColorU {
3446            r: 0x12,
3447            g: 0x34,
3448            b: 0x56,
3449            a: 0x78,
3450        };
3451        assert_eq!(encode_color_u32(&c), 0x1234_5678);
3452        assert_eq!(decode_color_u32(0x1234_5678), Some(c));
3453    }
3454
3455    #[test]
3456    fn color_u32_roundtrips_every_boundary_channel_combination() {
3457        for r in [0u8, 1, 127, 254, 255] {
3458            for g in [0u8, 1, 127, 254, 255] {
3459                for b in [0u8, 1, 127, 254, 255] {
3460                    for a in [0u8, 1, 127, 254, 255] {
3461                        let c = ColorU { r, g, b, a };
3462                        let encoded = encode_color_u32(&c);
3463                        if encoded == 0 {
3464                            // Only fully-transparent black hits the unset sentinel.
3465                            assert_eq!((r, g, b, a), (0, 0, 0, 0));
3466                            assert_eq!(decode_color_u32(encoded), None);
3467                        } else {
3468                            assert_eq!(decode_color_u32(encoded), Some(c));
3469                        }
3470                    }
3471                }
3472            }
3473        }
3474    }
3475
3476    #[test]
3477    fn color_u32_transparent_black_is_the_documented_unset_collision() {
3478        // Documented limitation, pinned so it cannot regress silently:
3479        // rgba(0,0,0,0) is indistinguishable from "property never set".
3480        let transparent_black = ColorU {
3481            r: 0,
3482            g: 0,
3483            b: 0,
3484            a: 0,
3485        };
3486        assert_eq!(encode_color_u32(&transparent_black), 0);
3487        assert_eq!(decode_color_u32(0), None);
3488
3489        // Every other alpha-0 color must still survive the round-trip.
3490        let transparent_red = ColorU {
3491            r: 255,
3492            g: 0,
3493            b: 0,
3494            a: 0,
3495        };
3496        assert_eq!(encode_color_u32(&transparent_red), 0xFF00_0000);
3497        assert_eq!(decode_color_u32(0xFF00_0000), Some(transparent_red));
3498
3499        // …including "black but only just" (alpha 1).
3500        let almost = ColorU {
3501            r: 0,
3502            g: 0,
3503            b: 0,
3504            a: 1,
3505        };
3506        assert_eq!(decode_color_u32(encode_color_u32(&almost)), Some(almost));
3507    }
3508
3509    #[test]
3510    fn color_u32_max_decodes_to_opaque_white() {
3511        assert_eq!(
3512            decode_color_u32(u32::MAX),
3513            Some(ColorU {
3514                r: 255,
3515                g: 255,
3516                b: 255,
3517                a: 255
3518            }),
3519        );
3520    }
3521
3522    // =========================================================================
3523    // PixelValue u32 (4-bit metric + 28-bit signed fixed-point ×1000)
3524    // =========================================================================
3525
3526    #[test]
3527    fn pixel_value_u32_sentinels_all_decode_to_none_and_are_distinct() {
3528        let sentinels = [
3529            U32_SENTINEL,
3530            U32_AUTO,
3531            U32_NONE,
3532            U32_INHERIT,
3533            U32_INITIAL,
3534            U32_MIN_CONTENT,
3535            U32_MAX_CONTENT,
3536        ];
3537        for (i, a) in sentinels.iter().enumerate() {
3538            assert!(
3539                *a >= U32_SENTINEL_THRESHOLD,
3540                "sentinel {a:#X} sits below the threshold and would decode as a value",
3541            );
3542            assert_eq!(decode_pixel_value_u32(*a), None);
3543            for b in &sentinels[i + 1..] {
3544                assert_ne!(a, b, "two u32 sentinels share a bit pattern");
3545            }
3546        }
3547        // The threshold itself is the lowest reserved value.
3548        assert_eq!(decode_pixel_value_u32(U32_SENTINEL_THRESHOLD), None);
3549        // One below the threshold is still a real (negative) value.
3550        assert!(decode_pixel_value_u32(U32_SENTINEL_THRESHOLD - 1).is_some());
3551    }
3552
3553    #[test]
3554    fn pixel_value_u32_roundtrips_at_the_28_bit_boundaries_for_every_metric() {
3555        // ±2^27 is the documented edge of the 28-bit signed fixed-point field.
3556        for metric in ALL_SIZE_METRIC {
3557            for raw in [0isize, 1, -1, 1000, -1000, 134_217_727, -134_217_728] {
3558                let pv = pv_raw(metric, raw);
3559                let encoded = encode_pixel_value_u32(&pv);
3560
3561                // Raw -1 with a high metric nibble collides with the sentinel
3562                // band; that is asserted separately in the bug test below.
3563                if encoded >= U32_SENTINEL_THRESHOLD {
3564                    continue;
3565                }
3566
3567                let decoded = decode_pixel_value_u32(encoded)
3568                    .unwrap_or_else(|| panic!("{metric:?} raw {raw} decoded as a sentinel"));
3569                assert_eq!(decoded.metric, metric, "metric nibble lost for raw {raw}");
3570                assert_eq!(
3571                    decoded.number.number(),
3572                    raw,
3573                    "{metric:?}: raw {raw} did not survive the round-trip",
3574                );
3575            }
3576        }
3577    }
3578
3579    #[test]
3580    fn pixel_value_u32_out_of_28_bit_range_returns_sentinel() {
3581        for metric in ALL_SIZE_METRIC {
3582            for raw in [
3583                134_217_728isize,
3584                -134_217_729,
3585                1_000_000_000,
3586                -1_000_000_000,
3587                isize::MAX,
3588                isize::MIN,
3589            ] {
3590                assert_eq!(
3591                    encode_pixel_value_u32(&pv_raw(metric, raw)),
3592                    U32_SENTINEL,
3593                    "{metric:?}: raw {raw} is outside 28 bits and must escape to tier 3",
3594                );
3595            }
3596        }
3597    }
3598
3599    #[test]
3600    fn pixel_value_u32_decodes_every_low_bit_pattern_without_panic() {
3601        // Metric nibbles 12..=15 have no SizeMetric — they must clamp to Px.
3602        for nibble in 12u32..16 {
3603            let encoded = (1u32 << 4) | nibble;
3604            let decoded = decode_pixel_value_u32(encoded).unwrap();
3605            assert_eq!(decoded.metric, SizeMetric::Px);
3606            assert_eq!(decoded.number.number(), 1);
3607        }
3608        // Sign extension: the top bit of the 28-bit field must arithmetic-shift.
3609        let neg = decode_pixel_value_u32(0x8000_0000).unwrap();
3610        assert_eq!(neg.metric, SizeMetric::Px);
3611        assert_eq!(neg.number.number(), -134_217_728);
3612    }
3613
3614    #[test]
3615    fn pixel_value_u32_negative_0_001_in_vh_vmin_vmax_collides_with_sentinels() {
3616        // BUG (encode_pixel_value_u32 / decode_pixel_value_u32):
3617        //
3618        // raw == -1 (i.e. -0.001 of a unit) sign-extends to 0xFFFF_FFFF, and
3619        // `<< 4` leaves 0xFFFF_FFF0. ORing in a metric nibble >= 9 pushes the
3620        // word into the reserved sentinel band (>= 0xFFFF_FFF9):
3621        //
3622        //   -0.001vh   → 0xFFFF_FFF9 == U32_MAX_CONTENT
3623        //   -0.001vmin → 0xFFFF_FFFA == U32_MIN_CONTENT
3624        //   -0.001vmax → 0xFFFF_FFFB == U32_INITIAL
3625        //
3626        // So a legal (if tiny) negative viewport-relative length is written into
3627        // the cache as `max-content` / `min-content` / `initial`, and the decoder
3628        // reports None (unset) instead of the value. The encoder's range check
3629        // guards the 28-bit magnitude but not the sentinel band it lands in.
3630        //
3631        // Fix would be to reject any encoding that lands >= U32_SENTINEL_THRESHOLD
3632        // and escape to tier 3 instead.
3633        // FIXED (as this test's own comment prescribed): a raw -1 with a high metric
3634        // nibble packs into the reserved sentinel band, so the encoder now ESCAPES it to
3635        // U32_SENTINEL (tier 3) rather than emitting a value that decodes as a wrong,
3636        // aliased sentinel. decode() therefore returns None ("not in the fast cache —
3637        // look in tier 3"), which is the safe, non-aliasing outcome.
3638        for metric in [SizeMetric::Vh, SizeMetric::Vmin, SizeMetric::Vmax] {
3639            let pv = pv_raw(metric, -1);
3640            let encoded = encode_pixel_value_u32(&pv);
3641            assert_eq!(encoded, U32_SENTINEL, "{metric:?}: raw -1 must escape to tier 3");
3642            assert_eq!(
3643                decode_pixel_value_u32(encoded),
3644                None,
3645                "{metric:?}: an escaped value decodes as None, never an aliased sentinel",
3646            );
3647        }
3648    }
3649
3650    // =========================================================================
3651    // Resolved px i16 (×10)
3652    // =========================================================================
3653
3654    #[test]
3655    fn resolved_px_i16_nan_and_infinity_are_defined_and_do_not_panic() {
3656        // `f32 as i32` saturates: NaN → 0, +inf → i32::MAX, -inf → i32::MIN.
3657        assert_eq!(encode_resolved_px_i16(f32::NAN), 0);
3658        assert_eq!(encode_resolved_px_i16(-f32::NAN), 0);
3659        assert_eq!(encode_resolved_px_i16(f32::INFINITY), I16_SENTINEL);
3660        assert_eq!(encode_resolved_px_i16(f32::NEG_INFINITY), I16_SENTINEL);
3661        assert_eq!(encode_resolved_px_i16(f32::MAX), I16_SENTINEL);
3662        assert_eq!(encode_resolved_px_i16(f32::MIN), I16_SENTINEL);
3663        // Subnormals round to zero rather than blowing up.
3664        assert_eq!(encode_resolved_px_i16(f32::MIN_POSITIVE), 0);
3665        assert_eq!(encode_resolved_px_i16(-0.0), 0);
3666    }
3667
3668    #[test]
3669    fn resolved_px_i16_saturates_exactly_at_the_documented_range() {
3670        // Doc: -3276.8 ..= +3276.3 px at 0.1px precision.
3671        assert_eq!(encode_resolved_px_i16(3276.3), 32763);
3672        assert_eq!(encode_resolved_px_i16(-3276.8), -32768);
3673
3674        // One tick outside in either direction escapes to tier 3.
3675        assert_eq!(encode_resolved_px_i16(3276.4), I16_SENTINEL);
3676        assert_eq!(encode_resolved_px_i16(-3276.9), I16_SENTINEL);
3677        assert_eq!(encode_resolved_px_i16(1e9), I16_SENTINEL);
3678        assert_eq!(encode_resolved_px_i16(-1e9), I16_SENTINEL);
3679    }
3680
3681    #[test]
3682    fn resolved_px_i16_sentinels_decode_to_none_and_are_distinct() {
3683        let sentinels = [I16_SENTINEL, I16_AUTO, I16_INHERIT, I16_INITIAL];
3684        for (i, a) in sentinels.iter().enumerate() {
3685            assert!(*a >= I16_SENTINEL_THRESHOLD);
3686            assert_eq!(decode_resolved_px_i16(*a), None);
3687            for b in &sentinels[i + 1..] {
3688                assert_ne!(a, b, "two i16 sentinels share a bit pattern");
3689            }
3690        }
3691        assert_eq!(decode_resolved_px_i16(I16_SENTINEL_THRESHOLD), None);
3692        assert_eq!(decode_resolved_px_i16(I16_SENTINEL_THRESHOLD - 1), Some(3276.3));
3693    }
3694
3695    #[test]
3696    fn resolved_px_i16_every_non_sentinel_value_roundtrips() {
3697        // Exhaustive over the whole non-sentinel i16 domain: decode → encode must
3698        // be the identity, or a value written by one frame reads back shifted on
3699        // the next.
3700        for v in i16::MIN..I16_SENTINEL_THRESHOLD {
3701            let px = decode_resolved_px_i16(v)
3702                .unwrap_or_else(|| panic!("{v} is below the threshold but decoded as a sentinel"));
3703            assert_eq!(
3704                encode_resolved_px_i16(px),
3705                v,
3706                "i16 {v} decoded to {px} px which re-encodes to a different i16",
3707            );
3708        }
3709    }
3710
3711    // =========================================================================
3712    // Flex u16 (×100)
3713    // =========================================================================
3714
3715    #[test]
3716    fn flex_u16_nan_infinity_and_negatives_are_defined_and_do_not_panic() {
3717        assert_eq!(encode_flex_u16(f32::NAN), 0);
3718        assert_eq!(encode_flex_u16(f32::INFINITY), U16_SENTINEL);
3719        assert_eq!(encode_flex_u16(f32::NEG_INFINITY), U16_SENTINEL);
3720        assert_eq!(encode_flex_u16(f32::MAX), U16_SENTINEL);
3721        // flex-grow/shrink are non-negative; a negative escapes to tier 3.
3722        assert_eq!(encode_flex_u16(-1.0), U16_SENTINEL);
3723        assert_eq!(encode_flex_u16(-0.01), U16_SENTINEL);
3724        // …but -0.0 and values that round to zero clamp to 0, not to a sentinel.
3725        assert_eq!(encode_flex_u16(-0.0), 0);
3726        assert_eq!(encode_flex_u16(0.0), 0);
3727    }
3728
3729    #[test]
3730    fn flex_u16_saturates_exactly_at_the_documented_range() {
3731        // Doc: 0.00 ..= 655.27 at 0.01 precision.
3732        assert_eq!(encode_flex_u16(655.27), 65527);
3733        assert_eq!(decode_flex_u16(65527), Some(655.27));
3734        // 65528 is representable but 65529 is the threshold.
3735        assert_eq!(encode_flex_u16(655.28), 65528);
3736        assert_eq!(encode_flex_u16(655.29), U16_SENTINEL);
3737        assert_eq!(encode_flex_u16(1e9), U16_SENTINEL);
3738    }
3739
3740    #[test]
3741    fn flex_u16_sentinel_band_decodes_to_none() {
3742        for v in U16_SENTINEL_THRESHOLD..=u16::MAX {
3743            assert_eq!(decode_flex_u16(v), None, "u16 {v} is reserved and must decode as None");
3744        }
3745        assert_eq!(U16_SENTINEL, u16::MAX);
3746        assert!(decode_flex_u16(U16_SENTINEL_THRESHOLD - 1).is_some());
3747    }
3748
3749    #[test]
3750    fn flex_u16_every_non_sentinel_value_roundtrips() {
3751        for v in 0..U16_SENTINEL_THRESHOLD {
3752            let f = decode_flex_u16(v).unwrap_or_else(|| panic!("{v} decoded as a sentinel"));
3753            assert_eq!(encode_flex_u16(f), v, "u16 {v} → {f} → re-encoded differently");
3754        }
3755    }
3756
3757    // =========================================================================
3758    // encode_css_pixel_as_i16 — CssPropertyValue → i16 keyword sentinels
3759    // =========================================================================
3760
3761    #[test]
3762    fn css_pixel_as_i16_maps_every_keyword_to_its_own_sentinel() {
3763        assert_eq!(
3764            encode_css_pixel_as_i16(&CssPropertyValue::Auto),
3765            I16_AUTO,
3766        );
3767        assert_eq!(
3768            encode_css_pixel_as_i16(&CssPropertyValue::Initial),
3769            I16_INITIAL,
3770        );
3771        assert_eq!(
3772            encode_css_pixel_as_i16(&CssPropertyValue::Inherit),
3773            I16_INHERIT,
3774        );
3775        // None / Revert / Unset have no dedicated slot → generic sentinel (slow path).
3776        assert_eq!(
3777            encode_css_pixel_as_i16(&CssPropertyValue::<PixelValue>::None),
3778            I16_SENTINEL,
3779        );
3780        assert_eq!(
3781            encode_css_pixel_as_i16(&CssPropertyValue::<PixelValue>::Revert),
3782            I16_SENTINEL,
3783        );
3784        assert_eq!(
3785            encode_css_pixel_as_i16(&CssPropertyValue::<PixelValue>::Unset),
3786            I16_SENTINEL,
3787        );
3788    }
3789
3790    #[test]
3791    fn css_pixel_as_i16_only_takes_the_fast_path_for_absolute_px() {
3792        // Only SizeMetric::Px can be pre-resolved without layout context;
3793        // every relative unit must escape to the cascade.
3794        assert_eq!(
3795            encode_css_pixel_as_i16(&CssPropertyValue::Exact(PixelValue::px(12.5))),
3796            125,
3797        );
3798        for metric in ALL_SIZE_METRIC {
3799            if metric == SizeMetric::Px {
3800                continue;
3801            }
3802            assert_eq!(
3803                encode_css_pixel_as_i16(&CssPropertyValue::Exact(pv_raw(metric, 12_500))),
3804                I16_SENTINEL,
3805                "{metric:?} needs resolution context and must not be pre-resolved",
3806            );
3807        }
3808    }
3809
3810    #[test]
3811    fn css_pixel_as_i16_out_of_range_px_escapes_to_the_sentinel() {
3812        assert_eq!(
3813            encode_css_pixel_as_i16(&CssPropertyValue::Exact(PixelValue::px(1e6))),
3814            I16_SENTINEL,
3815        );
3816        assert_eq!(
3817            encode_css_pixel_as_i16(&CssPropertyValue::Exact(PixelValue::px(-1e6))),
3818            I16_SENTINEL,
3819        );
3820        // A px value that happens to land on a keyword sentinel would be
3821        // misread as `auto`/`inherit`; the range check must exclude the band.
3822        for raw in [I16_SENTINEL, I16_AUTO, I16_INHERIT, I16_INITIAL] {
3823            let px = f32::from(raw) / 10.0;
3824            assert_eq!(
3825                encode_css_pixel_as_i16(&CssPropertyValue::Exact(PixelValue::px(px))),
3826                I16_SENTINEL,
3827                "{px} px must not silently encode as the keyword sentinel {raw}",
3828            );
3829        }
3830    }
3831
3832    // =========================================================================
3833    // CompactLayoutCache — constructor invariants, bounds, predicates
3834    // =========================================================================
3835
3836    #[test]
3837    fn empty_cache_is_the_neutral_element() {
3838        let c = CompactLayoutCache::empty();
3839        assert_eq!(c.node_count(), 0);
3840        assert!(c.tier1_enums.is_empty());
3841        assert!(c.tier2_dims.is_empty());
3842        assert!(c.tier2_cold.is_empty());
3843        assert!(c.tier2b_text.is_empty());
3844        assert!(c.font_dirty_nodes.is_empty());
3845        assert!(c.prev_font_hashes.is_empty());
3846        assert!(c.font_hash_to_families.is_empty());
3847        assert_eq!(c.dom_declared_flags, 0);
3848        // with_capacity(0) must produce exactly the same thing.
3849        assert_eq!(CompactLayoutCache::with_capacity(0), c);
3850    }
3851
3852    #[test]
3853    fn with_capacity_keeps_every_tier_the_same_length() {
3854        for n in [0usize, 1, 2, 3, 17, 1024] {
3855            let c = CompactLayoutCache::with_capacity(n);
3856            assert_eq!(c.node_count(), n);
3857            assert_eq!(c.tier1_enums.len(), n);
3858            assert_eq!(c.tier2_dims.len(), n);
3859            assert_eq!(c.tier2_cold.len(), n);
3860            assert_eq!(c.tier2b_text.len(), n);
3861            assert_eq!(c.prev_font_hashes.len(), n);
3862            // Dirty list starts empty regardless of node count.
3863            assert!(c.font_dirty_nodes.is_empty());
3864            assert_eq!(c.dom_declared_flags, 0);
3865        }
3866    }
3867
3868    #[test]
3869    fn freshly_allocated_node_reads_back_every_css_initial_value() {
3870        // `with_capacity` zeroes tier1 and default-fills tier2. A node the
3871        // builder never touched must still answer every getter with the CSS
3872        // initial value — this is what lets the builder skip unset properties.
3873        let c = CompactLayoutCache::with_capacity(3);
3874        for i in 0..3 {
3875            assert_eq!(T1::decode(c.tier1_enums[i]), T1::initial());
3876            assert_eq!(c.get_display(i), LayoutDisplay::Block);
3877            assert_eq!(c.get_position(i), LayoutPosition::Static);
3878            assert_eq!(c.get_float(i), LayoutFloat::None);
3879            assert_eq!(c.get_overflow_x(i), LayoutOverflow::Visible);
3880            assert_eq!(c.get_overflow_y(i), LayoutOverflow::Visible);
3881            assert_eq!(c.get_box_sizing(i), LayoutBoxSizing::ContentBox);
3882            assert_eq!(c.get_flex_direction(i), LayoutFlexDirection::Row);
3883            assert_eq!(c.get_flex_wrap(i), LayoutFlexWrap::NoWrap);
3884            assert_eq!(c.get_justify_content(i), LayoutJustifyContent::FlexStart);
3885            assert_eq!(c.get_align_items(i), LayoutAlignItems::Stretch);
3886            assert_eq!(c.get_align_content(i), LayoutAlignContent::Stretch);
3887            assert_eq!(c.get_writing_mode(i), LayoutWritingMode::HorizontalTb);
3888            assert_eq!(c.get_clear(i), LayoutClear::None);
3889            assert_eq!(c.get_font_weight(i), StyleFontWeight::Normal);
3890            assert_eq!(c.get_font_style(i), StyleFontStyle::Normal);
3891            assert_eq!(c.get_text_align(i), StyleTextAlign::Start);
3892            assert_eq!(c.get_visibility(i), StyleVisibility::Visible);
3893            assert_eq!(c.get_white_space(i), StyleWhiteSpace::Normal);
3894            assert_eq!(c.get_direction(i), StyleDirection::Ltr);
3895            assert_eq!(c.get_vertical_align(i), StyleVerticalAlign::Baseline);
3896            assert_eq!(c.get_border_collapse(i), StyleBorderCollapse::Separate);
3897
3898            // Dimensions: auto / none, i.e. no decodable pixel value.
3899            assert_eq!(c.get_width_raw(i), U32_AUTO);
3900            assert_eq!(c.get_height_raw(i), U32_AUTO);
3901            assert_eq!(c.get_min_width_raw(i), U32_AUTO);
3902            assert_eq!(c.get_min_height_raw(i), U32_AUTO);
3903            assert_eq!(c.get_max_width_raw(i), U32_NONE);
3904            assert_eq!(c.get_max_height_raw(i), U32_NONE);
3905            assert_eq!(c.get_flex_basis_raw(i), U32_AUTO);
3906            assert_eq!(c.get_font_size_raw(i), U32_INITIAL);
3907            assert_eq!(decode_pixel_value_u32(c.get_width_raw(i)), None);
3908            assert_eq!(decode_pixel_value_u32(c.get_font_size_raw(i)), None);
3909
3910            // Box model: zeros are real values (Some), offsets are auto (raw sentinel).
3911            assert_eq!(c.get_padding_top(i), Some(0.0));
3912            assert_eq!(c.get_padding_right(i), Some(0.0));
3913            assert_eq!(c.get_padding_bottom(i), Some(0.0));
3914            assert_eq!(c.get_padding_left(i), Some(0.0));
3915            assert_eq!(c.get_border_top_width(i), Some(0.0));
3916            assert_eq!(c.get_border_left_width(i), Some(0.0));
3917            // margin defaults to 0, NOT auto — centering must not kick in for free.
3918            assert_eq!(c.get_margin_top(i), Some(0.0));
3919            assert_eq!(c.get_margin_left(i), Some(0.0));
3920            assert!(!c.is_margin_top_auto(i));
3921            assert!(!c.is_margin_right_auto(i));
3922            assert!(!c.is_margin_bottom_auto(i));
3923            assert!(!c.is_margin_left_auto(i));
3924            // …but the inset properties DO default to auto.
3925            assert_eq!(c.get_top(i), I16_AUTO);
3926            assert_eq!(c.get_right(i), I16_AUTO);
3927            assert_eq!(c.get_bottom(i), I16_AUTO);
3928            assert_eq!(c.get_left(i), I16_AUTO);
3929
3930            // Flex: grow 0, shrink 1 (the CSS defaults).
3931            assert_eq!(c.get_flex_grow(i), Some(0.0));
3932            assert_eq!(c.get_flex_shrink(i), Some(1.0));
3933
3934            // Cold tier.
3935            assert_eq!(c.get_z_index(i), I16_AUTO);
3936            assert_eq!(c.get_border_styles_packed(i), 0);
3937            assert_eq!(c.get_border_top_style(i), BorderStyle::None);
3938            assert_eq!(c.get_border_right_style(i), BorderStyle::None);
3939            assert_eq!(c.get_border_bottom_style(i), BorderStyle::None);
3940            assert_eq!(c.get_border_left_style(i), BorderStyle::None);
3941            assert_eq!(c.get_border_top_color_raw(i), 0);
3942            assert_eq!(decode_color_u32(c.get_border_top_color_raw(i)), None);
3943            assert_eq!(c.get_border_top_left_radius_raw(i), I16_SENTINEL);
3944            assert_eq!(c.get_tab_size_raw(i), I16_SENTINEL);
3945            assert_eq!(c.get_border_spacing_h_raw(i), 0);
3946            assert_eq!(c.get_border_spacing_v_raw(i), 0);
3947            assert_eq!(c.get_opacity_raw(i), OPACITY_SENTINEL);
3948            assert_eq!(c.get_hot_flags(i), 0);
3949            assert_eq!(c.get_scrollbar_gutter_bits(i), SCROLLBAR_GUTTER_AUTO);
3950
3951            // Every "has this rare prop" predicate must be false on a fresh node,
3952            // otherwise the fast path would take a cascade walk for every node.
3953            assert!(!c.has_transform(i));
3954            assert!(!c.has_transform_origin(i));
3955            assert!(!c.has_box_shadow(i));
3956            assert!(!c.has_text_decoration(i));
3957            assert!(!c.has_background(i));
3958            assert!(!c.has_clip_path(i));
3959            assert!(!c.has_scrollbar_css(i));
3960            assert!(!c.has_counter(i));
3961            assert!(!c.has_break(i));
3962            assert!(!c.has_text_orientation(i));
3963            assert!(!c.has_text_shadow(i));
3964            assert!(!c.has_backdrop_filter(i));
3965            assert!(!c.has_filter(i));
3966            assert!(!c.has_mix_blend_mode(i));
3967
3968            // Text tier.
3969            assert_eq!(c.get_text_color_raw(i), 0);
3970            assert_eq!(c.get_font_family_hash(i), 0);
3971            assert_eq!(c.get_line_height(i), None); // "normal" → slow path
3972            assert_eq!(c.get_letter_spacing(i), Some(0.0));
3973            assert_eq!(c.get_word_spacing(i), Some(0.0));
3974            assert_eq!(c.get_text_indent(i), Some(0.0));
3975        }
3976    }
3977
3978    #[test]
3979    fn hot_flag_predicates_read_only_their_own_bit() {
3980        let flags = [
3981            ("transform", HOT_FLAG_HAS_TRANSFORM),
3982            ("transform_origin", HOT_FLAG_HAS_TRANSFORM_ORIGIN),
3983            ("box_shadow", HOT_FLAG_HAS_BOX_SHADOW),
3984            ("text_decoration", HOT_FLAG_HAS_TEXT_DECORATION),
3985            ("background", HOT_FLAG_HAS_BACKGROUND),
3986            ("clip_path", HOT_FLAG_HAS_CLIP_PATH),
3987        ];
3988
3989        for (name, bit) in flags {
3990            let mut c = CompactLayoutCache::with_capacity(1);
3991            c.tier2_cold[0].hot_flags = bit;
3992
3993            let observed = [
3994                ("transform", c.has_transform(0)),
3995                ("transform_origin", c.has_transform_origin(0)),
3996                ("box_shadow", c.has_box_shadow(0)),
3997                ("text_decoration", c.has_text_decoration(0)),
3998                ("background", c.has_background(0)),
3999                ("clip_path", c.has_clip_path(0)),
4000            ];
4001            for (other, is_set) in observed {
4002                assert_eq!(
4003                    is_set,
4004                    other == name,
4005                    "hot_flags = {bit:#010b}: has_{other}() should be {}",
4006                    other == name,
4007                );
4008            }
4009            // The gutter field lives in bits 4-5 and must be unaffected.
4010            assert_eq!(c.get_scrollbar_gutter_bits(0), SCROLLBAR_GUTTER_AUTO);
4011        }
4012
4013        // No two hot flags may share a bit, and none may overlap the gutter field.
4014        let mut seen = 0u8;
4015        for (_, bit) in flags {
4016            assert_eq!(seen & bit, 0, "two hot flags share bit {bit:#010b}");
4017            assert_eq!(
4018                bit & HOT_FLAG_SCROLLBAR_GUTTER_MASK,
4019                0,
4020                "hot flag {bit:#010b} overlaps the scrollbar-gutter field",
4021            );
4022            seen |= bit;
4023        }
4024    }
4025
4026    #[test]
4027    fn scrollbar_gutter_bits_survive_a_fully_set_hot_flags_byte() {
4028        for gutter in [
4029            SCROLLBAR_GUTTER_AUTO,
4030            SCROLLBAR_GUTTER_STABLE,
4031            SCROLLBAR_GUTTER_BOTH_EDGES,
4032            SCROLLBAR_GUTTER_MIRROR,
4033        ] {
4034            let mut c = CompactLayoutCache::with_capacity(1);
4035            // Every boolean flag set *and* a gutter value: the gutter must still
4036            // read back cleanly out of the middle of the byte.
4037            c.tier2_cold[0].hot_flags = HOT_FLAG_HAS_TRANSFORM
4038                | HOT_FLAG_HAS_TRANSFORM_ORIGIN
4039                | HOT_FLAG_HAS_BOX_SHADOW
4040                | HOT_FLAG_HAS_TEXT_DECORATION
4041                | HOT_FLAG_HAS_BACKGROUND
4042                | HOT_FLAG_HAS_CLIP_PATH
4043                | (gutter << HOT_FLAG_SCROLLBAR_GUTTER_SHIFT);
4044
4045            assert_eq!(c.get_scrollbar_gutter_bits(0), gutter);
4046            assert!(c.has_transform(0));
4047            assert!(c.has_clip_path(0));
4048        }
4049
4050        // An all-ones byte reads the max gutter value, never something out of range.
4051        let mut c = CompactLayoutCache::with_capacity(1);
4052        c.tier2_cold[0].hot_flags = u8::MAX;
4053        assert_eq!(c.get_scrollbar_gutter_bits(0), SCROLLBAR_GUTTER_MIRROR);
4054        assert!(c.get_scrollbar_gutter_bits(0) <= 3);
4055    }
4056
4057    #[test]
4058    fn extra_flag_predicates_read_only_their_own_bit() {
4059        let flags = [
4060            ("scrollbar_css", EXTRA_FLAG_HAS_SCROLLBAR_CSS),
4061            ("counter", EXTRA_FLAG_HAS_COUNTER),
4062            ("break", EXTRA_FLAG_HAS_BREAK),
4063            ("text_orientation", EXTRA_FLAG_HAS_TEXT_ORIENTATION),
4064            ("text_shadow", EXTRA_FLAG_HAS_TEXT_SHADOW),
4065            ("backdrop_filter", EXTRA_FLAG_HAS_BACKDROP_FILTER),
4066            ("filter", EXTRA_FLAG_HAS_FILTER),
4067            ("mix_blend_mode", EXTRA_FLAG_HAS_MIX_BLEND_MODE),
4068        ];
4069
4070        // All 8 bits must be distinct and together cover the whole byte.
4071        let mut seen = 0u8;
4072        for (_, bit) in flags {
4073            assert_eq!(seen & bit, 0, "two extra flags share bit {bit:#010b}");
4074            seen |= bit;
4075        }
4076        assert_eq!(seen, u8::MAX);
4077
4078        for (name, bit) in flags {
4079            let mut c = CompactLayoutCache::with_capacity(1);
4080            c.tier2_cold[0].extra_flags = bit;
4081            let observed = [
4082                ("scrollbar_css", c.has_scrollbar_css(0)),
4083                ("counter", c.has_counter(0)),
4084                ("break", c.has_break(0)),
4085                ("text_orientation", c.has_text_orientation(0)),
4086                ("text_shadow", c.has_text_shadow(0)),
4087                ("backdrop_filter", c.has_backdrop_filter(0)),
4088                ("filter", c.has_filter(0)),
4089                ("mix_blend_mode", c.has_mix_blend_mode(0)),
4090            ];
4091            for (other, is_set) in observed {
4092                assert_eq!(
4093                    is_set,
4094                    other == name,
4095                    "extra_flags = {bit:#010b}: has_{other}() should be {}",
4096                    other == name,
4097                );
4098            }
4099            // Setting an extra flag must not make any hot-flag predicate fire.
4100            assert!(!c.has_transform(0));
4101            assert!(!c.has_background(0));
4102        }
4103    }
4104
4105    #[test]
4106    fn dom_declared_flags_are_distinct_and_queryable() {
4107        let flags = [
4108            DOM_HAS_SHAPE_INSIDE,
4109            DOM_HAS_SHAPE_OUTSIDE,
4110            DOM_HAS_TEXT_JUSTIFY,
4111            DOM_HAS_TEXT_INDENT,
4112            DOM_HAS_COLUMN_COUNT,
4113            DOM_HAS_COLUMN_GAP,
4114            DOM_HAS_INITIAL_LETTER,
4115            DOM_HAS_INITIAL_LETTER_ALIGN,
4116            DOM_HAS_LINE_CLAMP,
4117            DOM_HAS_HANGING_PUNCTUATION,
4118            DOM_HAS_TEXT_COMBINE_UPRIGHT,
4119            DOM_HAS_EXCLUSION_MARGIN,
4120            DOM_HAS_HYPHENATION_LANGUAGE,
4121            DOM_HAS_UNICODE_BIDI,
4122            DOM_HAS_TEXT_BOX_TRIM,
4123            DOM_HAS_HYPHENS,
4124            DOM_HAS_WORD_BREAK,
4125            DOM_HAS_OVERFLOW_WRAP,
4126            DOM_HAS_LINE_BREAK,
4127            DOM_HAS_TEXT_ALIGN_LAST,
4128            DOM_HAS_LINE_HEIGHT,
4129            DOM_HAS_COLUMN_WIDTH,
4130            DOM_HAS_SHAPE_MARGIN,
4131        ];
4132
4133        let mut seen = 0u32;
4134        for f in flags {
4135            assert_eq!(f.count_ones(), 1, "{f:#X} is not a single-bit flag");
4136            assert_eq!(seen & f, 0, "two DOM_HAS_* flags share bit {f:#X}");
4137            seen |= f;
4138        }
4139
4140        let mut c = CompactLayoutCache::empty();
4141        // Nothing declared: every query is false, including the degenerate ones.
4142        for f in flags {
4143            assert!(!c.dom_declared(f));
4144        }
4145        assert!(!c.dom_declared(0));
4146        assert!(!c.dom_declared(u32::MAX));
4147
4148        // One flag declared: only that query is true.
4149        c.dom_declared_flags = DOM_HAS_LINE_HEIGHT;
4150        for f in flags {
4151            assert_eq!(c.dom_declared(f), f == DOM_HAS_LINE_HEIGHT);
4152        }
4153        assert!(!c.dom_declared(0), "an empty flag query must never report declared");
4154        assert!(c.dom_declared(u32::MAX));
4155
4156        // Everything declared: every query is true.
4157        c.dom_declared_flags = u32::MAX;
4158        for f in flags {
4159            assert!(c.dom_declared(f));
4160        }
4161    }
4162
4163    #[test]
4164    fn getters_at_the_last_valid_index_do_not_panic() {
4165        let c = CompactLayoutCache::with_capacity(4);
4166        let last = c.node_count() - 1;
4167        let _ = c.get_display(last);
4168        let _ = c.get_width_raw(last);
4169        let _ = c.get_padding_top(last);
4170        let _ = c.get_z_index(last);
4171        let _ = c.get_border_top_style(last);
4172        let _ = c.get_line_height(last);
4173        let _ = c.has_transform(last);
4174    }
4175
4176    #[test]
4177    #[should_panic(expected = "index out of bounds")]
4178    fn tier1_getter_on_an_empty_cache_panics_rather_than_reading_oob() {
4179        let c = CompactLayoutCache::empty();
4180        let _ = c.get_display(0);
4181    }
4182
4183    #[test]
4184    #[should_panic(expected = "index out of bounds")]
4185    fn tier2_getter_past_the_end_panics_rather_than_reading_oob() {
4186        let c = CompactLayoutCache::with_capacity(2);
4187        let _ = c.get_padding_top(2);
4188    }
4189
4190    #[test]
4191    #[should_panic(expected = "index out of bounds")]
4192    fn cold_tier_getter_at_usize_max_panics_rather_than_wrapping() {
4193        // usize::MAX would wrap to a valid offset if the index were ever used in
4194        // pointer arithmetic without a bounds check.
4195        let c = CompactLayoutCache::with_capacity(1);
4196        let _ = c.get_z_index(usize::MAX);
4197    }
4198
4199    #[test]
4200    #[should_panic(expected = "index out of bounds")]
4201    fn text_tier_getter_past_the_end_panics_rather_than_reading_oob() {
4202        let c = CompactLayoutCache::with_capacity(1);
4203        let _ = c.get_font_family_hash(1);
4204    }
4205
4206    // =========================================================================
4207    // Struct layout — the compact cache's whole point is its byte budget
4208    // =========================================================================
4209
4210    #[test]
4211    fn compact_structs_stay_within_their_documented_byte_budget() {
4212        // These sizes are load-bearing: the cache is sized as N × these, and the
4213        // module header quotes them. A field added without updating the header
4214        // silently doubles the per-node memory cost.
4215        assert_eq!(size_of::<CompactNodeProps>(), 72);
4216        assert_eq!(size_of::<CompactNodePropsCold>(), 48);
4217        assert_eq!(size_of::<CompactTextProps>(), 24);
4218        // Tier 1 is exactly one u64 per node.
4219        assert_eq!(size_of::<u64>(), 8);
4220        // No padding surprises from #[repr(C)] reordering.
4221        assert_eq!(align_of::<CompactNodeProps>(), 4);
4222        assert_eq!(align_of::<CompactNodePropsCold>(), 4);
4223        assert_eq!(align_of::<CompactTextProps>(), 8);
4224    }
4225}