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