Skip to main content

azul_css/
compact_cache.rs

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