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
13use crate::props::basic::length::{FloatValue, SizeMetric};
14use crate::props::basic::pixel::PixelValue;
15use crate::props::layout::{
16    display::LayoutDisplay,
17    dimensions::{LayoutHeight, LayoutWidth, LayoutMaxHeight, LayoutMaxWidth, LayoutMinHeight, LayoutMinWidth},
18    flex::{
19        LayoutAlignContent, LayoutAlignItems, LayoutAlignSelf, LayoutFlexDirection, LayoutFlexWrap,
20        LayoutJustifyContent,
21    },
22    grid::{LayoutGridAutoFlow, LayoutJustifySelf, LayoutJustifyItems},
23    overflow::LayoutOverflow,
24    position::LayoutPosition,
25    wrapping::{LayoutClear, LayoutWritingMode},
26    table::StyleBorderCollapse,
27};
28use crate::props::layout::display::LayoutFloat;
29use crate::props::layout::dimensions::LayoutBoxSizing;
30use crate::props::basic::font::{StyleFontStyle, StyleFontWeight};
31use crate::props::basic::color::ColorU;
32use crate::props::style::{StyleTextAlign, StyleVerticalAlign, StyleVisibility, StyleWhiteSpace, StyleDirection};
33use crate::props::style::border::BorderStyle;
34use crate::props::property::{CssProperty, CssPropertyType};
35use crate::css::CssPropertyValue;
36use alloc::boxed::Box;
37use alloc::vec::Vec;
38
39// =============================================================================
40// Sentinel Constants
41// =============================================================================
42
43/// u16 sentinel values (for resolved-px ×10 and flex ×100)
44pub const U16_SENTINEL: u16 = 0xFFFF;
45/// Any u16 value >= this threshold is a sentinel, not a real value
46pub const U16_SENTINEL_THRESHOLD: u16 = 0xFFF9;
47
48/// i16 sentinel values (for signed resolved-px ×10)
49pub const I16_SENTINEL: i16 = 0x7FFF;       // 32767
50pub const I16_AUTO: i16 = 0x7FFE;           // 32766
51pub const I16_INHERIT: i16 = 0x7FFD;        // 32765
52pub const I16_INITIAL: i16 = 0x7FFC;        // 32764
53/// Any i16 value >= this threshold is a sentinel
54pub const I16_SENTINEL_THRESHOLD: i16 = 0x7FFC; // 32764
55
56/// u32 sentinel values (for dimension properties with unit info)
57pub const U32_SENTINEL: u32 = 0xFFFFFFFF;
58pub const U32_AUTO: u32 = 0xFFFFFFFE;
59pub const U32_NONE: u32 = 0xFFFFFFFD;
60pub const U32_INHERIT: u32 = 0xFFFFFFFC;
61pub const U32_INITIAL: u32 = 0xFFFFFFFB;
62pub const U32_MIN_CONTENT: u32 = 0xFFFFFFFA;
63pub const U32_MAX_CONTENT: u32 = 0xFFFFFFF9;
64/// Any u32 value >= this threshold is a sentinel
65pub const U32_SENTINEL_THRESHOLD: u32 = 0xFFFFFFF9;
66
67// =============================================================================
68// Tier 1: u64 bitfield — ALL enum properties
69// =============================================================================
70//
71// Bit layout (52 bits used, 12 spare):
72//   [4:0]    display          5 bits  (22 variants)
73//   [7:5]    position         3 bits  (5 variants)
74//   [9:8]    float            2 bits  (3 variants)
75//   [12:10]  overflow_x       3 bits  (5 variants)
76//   [15:13]  overflow_y       3 bits  (5 variants)
77//   [16]     box_sizing       1 bit   (2 variants)
78//   [18:17]  flex_direction   2 bits  (4 variants)
79//   [20:19]  flex_wrap        2 bits  (3 variants)
80//   [23:21]  justify_content  3 bits  (8 variants)
81//   [26:24]  align_items      3 bits  (5 variants)
82//   [29:27]  align_content    3 bits  (6 variants)
83//   [31:30]  writing_mode     2 bits  (3 variants)
84//   [33:32]  clear            2 bits  (4 variants)
85//   [37:34]  font_weight      4 bits  (11 variants)
86//   [39:38]  font_style       2 bits  (3 variants)
87//   [42:40]  text_align       3 bits  (6 variants)
88//   [44:43]  visibility       2 bits  (3 variants)
89//   [47:45]  white_space      3 bits  (6 variants)
90//   [48]     direction        1 bit   (2 variants)
91//   [51:49]  vertical_align   3 bits  (8 variants)
92//   [52]     border_collapse  1 bit   (2 variants)
93//   [63:53]  (spare / sentinel flags)
94
95// Bit offsets within u64
96pub const DISPLAY_SHIFT: u32 = 0;
97pub const POSITION_SHIFT: u32 = 5;
98pub const FLOAT_SHIFT: u32 = 8;
99pub const OVERFLOW_X_SHIFT: u32 = 10;
100pub const OVERFLOW_Y_SHIFT: u32 = 13;
101pub const BOX_SIZING_SHIFT: u32 = 16;
102pub const FLEX_DIRECTION_SHIFT: u32 = 17;
103pub const FLEX_WRAP_SHIFT: u32 = 19;
104pub const JUSTIFY_CONTENT_SHIFT: u32 = 21;
105pub const ALIGN_ITEMS_SHIFT: u32 = 24;
106pub const ALIGN_CONTENT_SHIFT: u32 = 27;
107pub const WRITING_MODE_SHIFT: u32 = 30;
108pub const CLEAR_SHIFT: u32 = 32;
109pub const FONT_WEIGHT_SHIFT: u32 = 34;
110pub const FONT_STYLE_SHIFT: u32 = 38;
111pub const TEXT_ALIGN_SHIFT: u32 = 40;
112pub const VISIBILITY_SHIFT: u32 = 43;
113pub const WHITE_SPACE_SHIFT: u32 = 45;
114pub const DIRECTION_SHIFT: u32 = 48;
115pub const VERTICAL_ALIGN_SHIFT: u32 = 49;
116pub const BORDER_COLLAPSE_SHIFT: u32 = 52;
117
118// Bit masks
119pub const DISPLAY_MASK: u64 = 0x1F;     // 5 bits
120pub const POSITION_MASK: u64 = 0x07;    // 3 bits
121pub const FLOAT_MASK: u64 = 0x03;       // 2 bits
122pub const OVERFLOW_MASK: u64 = 0x07;    // 3 bits
123pub const BOX_SIZING_MASK: u64 = 0x01;  // 1 bit
124pub const FLEX_DIR_MASK: u64 = 0x03;    // 2 bits
125pub const FLEX_WRAP_MASK: u64 = 0x03;   // 2 bits
126pub const JUSTIFY_MASK: u64 = 0x07;     // 3 bits
127pub const ALIGN_MASK: u64 = 0x07;       // 3 bits
128pub const WRITING_MODE_MASK: u64 = 0x03;// 2 bits
129pub const CLEAR_MASK: u64 = 0x03;       // 2 bits
130pub const FONT_WEIGHT_MASK: u64 = 0x0F; // 4 bits
131pub const FONT_STYLE_MASK: u64 = 0x03;  // 2 bits
132pub const TEXT_ALIGN_MASK: u64 = 0x07;  // 3 bits
133pub const VISIBILITY_MASK: u64 = 0x03;  // 2 bits
134pub const WHITE_SPACE_MASK: u64 = 0x07; // 3 bits
135pub const DIRECTION_MASK: u64 = 0x01;   // 1 bit
136pub const VERTICAL_ALIGN_MASK: u64 = 0x07; // 3 bits
137pub const BORDER_COLLAPSE_MASK: u64 = 0x01; // 1 bit
138
139pub const ALIGN_SELF_SHIFT: u32 = 53;
140pub const JUSTIFY_SELF_SHIFT: u32 = 56;
141pub const GRID_AUTO_FLOW_SHIFT: u32 = 59;
142pub const JUSTIFY_ITEMS_SHIFT: u32 = 61;
143pub const ALIGN_SELF_MASK: u64 = 0x07;     // 3 bits
144pub const JUSTIFY_SELF_MASK: u64 = 0x07;   // 3 bits
145pub const GRID_AUTO_FLOW_MASK: u64 = 0x03; // 2 bits (row/col × dense)
146pub const JUSTIFY_ITEMS_MASK: u64 = 0x03;  // 2 bits (start/center/end/stretch)
147
148/// Special value stored in the spare bits [63:51] to indicate this node has
149/// NO tier-1 data (i.e., all defaults). 0 is a valid all-defaults encoding,
150/// so we use bit 63 as a "tier1 populated" flag. If bit 63 is 0 and all other
151/// bits are 0, it means "all defaults" (Display::Block, Position::Static, etc.).
152/// We set bit 63 = 1 to mark that the node HAS been populated.
153pub const TIER1_POPULATED_BIT: u64 = 1 << 63;
154
155// =============================================================================
156// Safe from_u8 conversion functions (no transmute!)
157// =============================================================================
158
159/// Decode display from u8. **0 = Block** (most common HTML default).
160/// Value 31 (0x1F) = sentinel: look up in slow path for uncommon values.
161/// Returns default (Block) on invalid input.
162#[inline(always)]
163pub fn layout_display_from_u8(v: u8) -> LayoutDisplay {
164    match v {
165        0 => LayoutDisplay::Block,        // default when bits are 0
166        1 => LayoutDisplay::Inline,
167        2 => LayoutDisplay::InlineBlock,
168        3 => LayoutDisplay::Flex,
169        4 => LayoutDisplay::None,
170        5 => LayoutDisplay::InlineFlex,
171        6 => LayoutDisplay::Table,
172        7 => LayoutDisplay::InlineTable,
173        8 => LayoutDisplay::TableRowGroup,
174        9 => LayoutDisplay::TableHeaderGroup,
175        10 => LayoutDisplay::TableFooterGroup,
176        11 => LayoutDisplay::TableRow,
177        12 => LayoutDisplay::TableColumnGroup,
178        13 => LayoutDisplay::TableColumn,
179        14 => LayoutDisplay::TableCell,
180        15 => LayoutDisplay::TableCaption,
181        16 => LayoutDisplay::FlowRoot,
182        17 => LayoutDisplay::ListItem,
183        18 => LayoutDisplay::RunIn,
184        19 => LayoutDisplay::Marker,
185        20 => LayoutDisplay::Grid,
186        21 => LayoutDisplay::InlineGrid,
187        22 => LayoutDisplay::Contents,
188        _ => LayoutDisplay::Block, // fallback + sentinel (31)
189    }
190}
191
192/// Encode display to u8. **0 = Block** (most common HTML default).
193#[inline(always)]
194pub fn layout_display_to_u8(v: LayoutDisplay) -> u8 {
195    match v {
196        LayoutDisplay::Block => 0,         // 0 = default when bits unset
197        LayoutDisplay::Inline => 1,
198        LayoutDisplay::InlineBlock => 2,
199        LayoutDisplay::Flex => 3,
200        LayoutDisplay::None => 4,
201        LayoutDisplay::InlineFlex => 5,
202        LayoutDisplay::Table => 6,
203        LayoutDisplay::InlineTable => 7,
204        LayoutDisplay::TableRowGroup => 8,
205        LayoutDisplay::TableHeaderGroup => 9,
206        LayoutDisplay::TableFooterGroup => 10,
207        LayoutDisplay::TableRow => 11,
208        LayoutDisplay::TableColumnGroup => 12,
209        LayoutDisplay::TableColumn => 13,
210        LayoutDisplay::TableCell => 14,
211        LayoutDisplay::TableCaption => 15,
212        LayoutDisplay::FlowRoot => 16,
213        LayoutDisplay::ListItem => 17,
214        LayoutDisplay::RunIn => 18,
215        LayoutDisplay::Marker => 19,
216        LayoutDisplay::Grid => 20,
217        LayoutDisplay::InlineGrid => 21,
218        LayoutDisplay::Contents => 22,
219    }
220}
221
222#[inline(always)]
223pub fn layout_position_from_u8(v: u8) -> LayoutPosition {
224    match v {
225        0 => LayoutPosition::Static,
226        1 => LayoutPosition::Relative,
227        2 => LayoutPosition::Absolute,
228        3 => LayoutPosition::Fixed,
229        4 => LayoutPosition::Sticky,
230        _ => LayoutPosition::Static,
231    }
232}
233
234#[inline(always)]
235pub fn layout_position_to_u8(v: LayoutPosition) -> u8 {
236    match v {
237        LayoutPosition::Static => 0,
238        LayoutPosition::Relative => 1,
239        LayoutPosition::Absolute => 2,
240        LayoutPosition::Fixed => 3,
241        LayoutPosition::Sticky => 4,
242    }
243}
244
245/// Decode float from u8. **0 = None** (CSS initial value).
246#[inline(always)]
247pub fn layout_float_from_u8(v: u8) -> LayoutFloat {
248    match v {
249        0 => LayoutFloat::None,            // default when bits unset
250        1 => LayoutFloat::Left,
251        2 => LayoutFloat::Right,
252        _ => LayoutFloat::None,
253    }
254}
255
256/// Encode float to u8. **0 = None** (CSS initial value).
257#[inline(always)]
258pub fn layout_float_to_u8(v: LayoutFloat) -> u8 {
259    match v {
260        LayoutFloat::None => 0,
261        LayoutFloat::Left => 1,
262        LayoutFloat::Right => 2,
263    }
264}
265
266/// Decode overflow from u8. **0 = Visible** (CSS initial value).
267#[inline(always)]
268pub fn layout_overflow_from_u8(v: u8) -> LayoutOverflow {
269    match v {
270        0 => LayoutOverflow::Visible,      // default when bits unset
271        1 => LayoutOverflow::Hidden,
272        2 => LayoutOverflow::Scroll,
273        3 => LayoutOverflow::Auto,
274        4 => LayoutOverflow::Clip,
275        _ => LayoutOverflow::Visible,
276    }
277}
278
279/// Encode overflow to u8. **0 = Visible** (CSS initial value).
280#[inline(always)]
281pub fn layout_overflow_to_u8(v: LayoutOverflow) -> u8 {
282    match v {
283        LayoutOverflow::Visible => 0,      // 0 = default when bits unset
284        LayoutOverflow::Hidden => 1,
285        LayoutOverflow::Scroll => 2,
286        LayoutOverflow::Auto => 3,
287        LayoutOverflow::Clip => 4,
288    }
289}
290
291#[inline(always)]
292pub fn layout_box_sizing_from_u8(v: u8) -> LayoutBoxSizing {
293    match v {
294        0 => LayoutBoxSizing::ContentBox,
295        1 => LayoutBoxSizing::BorderBox,
296        _ => LayoutBoxSizing::ContentBox,
297    }
298}
299
300#[inline(always)]
301pub fn layout_box_sizing_to_u8(v: LayoutBoxSizing) -> u8 {
302    match v {
303        LayoutBoxSizing::ContentBox => 0,
304        LayoutBoxSizing::BorderBox => 1,
305    }
306}
307
308#[inline(always)]
309pub fn layout_flex_direction_from_u8(v: u8) -> LayoutFlexDirection {
310    match v {
311        0 => LayoutFlexDirection::Row,
312        1 => LayoutFlexDirection::RowReverse,
313        2 => LayoutFlexDirection::Column,
314        3 => LayoutFlexDirection::ColumnReverse,
315        _ => LayoutFlexDirection::Row,
316    }
317}
318
319#[inline(always)]
320pub fn layout_flex_direction_to_u8(v: LayoutFlexDirection) -> u8 {
321    match v {
322        LayoutFlexDirection::Row => 0,
323        LayoutFlexDirection::RowReverse => 1,
324        LayoutFlexDirection::Column => 2,
325        LayoutFlexDirection::ColumnReverse => 3,
326    }
327}
328
329/// 0 = NoWrap (CSS initial value for flex-wrap)
330#[inline(always)]
331pub fn layout_flex_wrap_from_u8(v: u8) -> LayoutFlexWrap {
332    match v {
333        0 => LayoutFlexWrap::NoWrap,       // CSS initial
334        1 => LayoutFlexWrap::Wrap,
335        2 => LayoutFlexWrap::WrapReverse,
336        _ => LayoutFlexWrap::NoWrap,
337    }
338}
339
340#[inline(always)]
341pub fn layout_flex_wrap_to_u8(v: LayoutFlexWrap) -> u8 {
342    match v {
343        LayoutFlexWrap::NoWrap => 0,
344        LayoutFlexWrap::Wrap => 1,
345        LayoutFlexWrap::WrapReverse => 2,
346    }
347}
348
349#[inline(always)]
350pub fn layout_justify_content_from_u8(v: u8) -> LayoutJustifyContent {
351    match v {
352        0 => LayoutJustifyContent::FlexStart,
353        1 => LayoutJustifyContent::FlexEnd,
354        2 => LayoutJustifyContent::Start,
355        3 => LayoutJustifyContent::End,
356        4 => LayoutJustifyContent::Center,
357        5 => LayoutJustifyContent::SpaceBetween,
358        6 => LayoutJustifyContent::SpaceAround,
359        7 => LayoutJustifyContent::SpaceEvenly,
360        _ => LayoutJustifyContent::FlexStart,
361    }
362}
363
364#[inline(always)]
365pub fn layout_justify_content_to_u8(v: LayoutJustifyContent) -> u8 {
366    match v {
367        LayoutJustifyContent::FlexStart => 0,
368        LayoutJustifyContent::FlexEnd => 1,
369        LayoutJustifyContent::Start => 2,
370        LayoutJustifyContent::End => 3,
371        LayoutJustifyContent::Center => 4,
372        LayoutJustifyContent::SpaceBetween => 5,
373        LayoutJustifyContent::SpaceAround => 6,
374        LayoutJustifyContent::SpaceEvenly => 7,
375    }
376}
377
378#[inline(always)]
379pub fn layout_align_items_from_u8(v: u8) -> LayoutAlignItems {
380    match v {
381        0 => LayoutAlignItems::Stretch,
382        1 => LayoutAlignItems::Center,
383        2 => LayoutAlignItems::Start,
384        3 => LayoutAlignItems::End,
385        4 => LayoutAlignItems::Baseline,
386        _ => LayoutAlignItems::Stretch,
387    }
388}
389
390#[inline(always)]
391pub fn layout_align_items_to_u8(v: LayoutAlignItems) -> u8 {
392    match v {
393        LayoutAlignItems::Stretch => 0,
394        LayoutAlignItems::Center => 1,
395        LayoutAlignItems::Start => 2,
396        LayoutAlignItems::End => 3,
397        LayoutAlignItems::Baseline => 4,
398    }
399}
400
401#[inline(always)]
402pub fn layout_align_self_to_u8(v: LayoutAlignSelf) -> u8 {
403    match v {
404        LayoutAlignSelf::Auto => 0,
405        LayoutAlignSelf::Stretch => 1,
406        LayoutAlignSelf::Center => 2,
407        LayoutAlignSelf::Start => 3,
408        LayoutAlignSelf::End => 4,
409        LayoutAlignSelf::Baseline => 5,
410    }
411}
412
413#[inline(always)]
414pub fn layout_align_self_from_u8(v: u8) -> LayoutAlignSelf {
415    match v {
416        0 => LayoutAlignSelf::Auto,
417        1 => LayoutAlignSelf::Stretch,
418        2 => LayoutAlignSelf::Center,
419        3 => LayoutAlignSelf::Start,
420        4 => LayoutAlignSelf::End,
421        5 => LayoutAlignSelf::Baseline,
422        _ => LayoutAlignSelf::Auto,
423    }
424}
425
426#[inline(always)]
427pub fn layout_justify_self_to_u8(v: LayoutJustifySelf) -> u8 {
428    match v {
429        LayoutJustifySelf::Auto => 0,
430        LayoutJustifySelf::Start => 1,
431        LayoutJustifySelf::End => 2,
432        LayoutJustifySelf::Center => 3,
433        LayoutJustifySelf::Stretch => 4,
434    }
435}
436
437#[inline(always)]
438pub fn layout_justify_self_from_u8(v: u8) -> LayoutJustifySelf {
439    match v {
440        0 => LayoutJustifySelf::Auto,
441        1 => LayoutJustifySelf::Start,
442        2 => LayoutJustifySelf::End,
443        3 => LayoutJustifySelf::Center,
444        4 => LayoutJustifySelf::Stretch,
445        _ => LayoutJustifySelf::Auto,
446    }
447}
448
449// Tier1 uses 0 as the "unset" sentinel for every enum. For justify-items
450// the CSS default is `normal` which behaves as `stretch` on grid items,
451// so 0 must decode to Stretch (not Start). Getting this wrong leaves
452// every unset grid container reporting justify-items: Start, which
453// forces taffy to content-size items instead of stretching them across
454// their column tracks — exactly the calc.c regression.
455#[inline(always)]
456pub fn layout_justify_items_to_u8(v: LayoutJustifyItems) -> u8 {
457    match v {
458        LayoutJustifyItems::Stretch => 0,
459        LayoutJustifyItems::Start => 1,
460        LayoutJustifyItems::End => 2,
461        LayoutJustifyItems::Center => 3,
462    }
463}
464
465#[inline(always)]
466pub fn layout_justify_items_from_u8(v: u8) -> LayoutJustifyItems {
467    match v {
468        0 => LayoutJustifyItems::Stretch,
469        1 => LayoutJustifyItems::Start,
470        2 => LayoutJustifyItems::End,
471        3 => LayoutJustifyItems::Center,
472        _ => LayoutJustifyItems::Stretch,
473    }
474}
475
476#[inline(always)]
477pub fn layout_grid_auto_flow_to_u8(v: LayoutGridAutoFlow) -> u8 {
478    match v {
479        LayoutGridAutoFlow::Row => 0,
480        LayoutGridAutoFlow::Column => 1,
481        LayoutGridAutoFlow::RowDense => 2,
482        LayoutGridAutoFlow::ColumnDense => 3,
483    }
484}
485
486#[inline(always)]
487pub fn layout_grid_auto_flow_from_u8(v: u8) -> LayoutGridAutoFlow {
488    match v {
489        0 => LayoutGridAutoFlow::Row,
490        1 => LayoutGridAutoFlow::Column,
491        2 => LayoutGridAutoFlow::RowDense,
492        3 => LayoutGridAutoFlow::ColumnDense,
493        _ => LayoutGridAutoFlow::Row,
494    }
495}
496
497#[inline(always)]
498pub fn layout_align_content_from_u8(v: u8) -> LayoutAlignContent {
499    match v {
500        0 => LayoutAlignContent::Stretch,
501        1 => LayoutAlignContent::Center,
502        2 => LayoutAlignContent::Start,
503        3 => LayoutAlignContent::End,
504        4 => LayoutAlignContent::SpaceBetween,
505        5 => LayoutAlignContent::SpaceAround,
506        _ => LayoutAlignContent::Stretch,
507    }
508}
509
510#[inline(always)]
511pub fn layout_align_content_to_u8(v: LayoutAlignContent) -> u8 {
512    match v {
513        LayoutAlignContent::Stretch => 0,
514        LayoutAlignContent::Center => 1,
515        LayoutAlignContent::Start => 2,
516        LayoutAlignContent::End => 3,
517        LayoutAlignContent::SpaceBetween => 4,
518        LayoutAlignContent::SpaceAround => 5,
519    }
520}
521
522#[inline(always)]
523pub fn layout_writing_mode_from_u8(v: u8) -> LayoutWritingMode {
524    match v {
525        0 => LayoutWritingMode::HorizontalTb,
526        1 => LayoutWritingMode::VerticalRl,
527        2 => LayoutWritingMode::VerticalLr,
528        _ => LayoutWritingMode::HorizontalTb,
529    }
530}
531
532#[inline(always)]
533pub fn layout_writing_mode_to_u8(v: LayoutWritingMode) -> u8 {
534    match v {
535        LayoutWritingMode::HorizontalTb => 0,
536        LayoutWritingMode::VerticalRl => 1,
537        LayoutWritingMode::VerticalLr => 2,
538    }
539}
540
541#[inline(always)]
542pub fn layout_clear_from_u8(v: u8) -> LayoutClear {
543    match v {
544        0 => LayoutClear::None,
545        1 => LayoutClear::Left,
546        2 => LayoutClear::Right,
547        3 => LayoutClear::Both,
548        _ => LayoutClear::None,
549    }
550}
551
552#[inline(always)]
553pub fn layout_clear_to_u8(v: LayoutClear) -> u8 {
554    match v {
555        LayoutClear::None => 0,
556        LayoutClear::Left => 1,
557        LayoutClear::Right => 2,
558        LayoutClear::Both => 3,
559    }
560}
561
562#[inline(always)]
563/// 0 = Normal/400 (CSS initial value for font-weight)
564pub fn style_font_weight_from_u8(v: u8) -> StyleFontWeight {
565    match v {
566        0 => StyleFontWeight::Normal,     // CSS initial (400)
567        1 => StyleFontWeight::W100,
568        2 => StyleFontWeight::W200,
569        3 => StyleFontWeight::W300,
570        4 => StyleFontWeight::W500,
571        5 => StyleFontWeight::W600,
572        6 => StyleFontWeight::Bold,       // 700
573        7 => StyleFontWeight::W800,
574        8 => StyleFontWeight::W900,
575        9 => StyleFontWeight::Lighter,
576        10 => StyleFontWeight::Bolder,
577        _ => StyleFontWeight::Normal,
578    }
579}
580
581#[inline(always)]
582/// 0 = Normal/400 (CSS initial value for font-weight)
583pub fn style_font_weight_to_u8(v: StyleFontWeight) -> u8 {
584    match v {
585        StyleFontWeight::Normal => 0,     // CSS initial (400)
586        StyleFontWeight::W100 => 1,
587        StyleFontWeight::W200 => 2,
588        StyleFontWeight::W300 => 3,
589        StyleFontWeight::W500 => 4,
590        StyleFontWeight::W600 => 5,
591        StyleFontWeight::Bold => 6,       // 700
592        StyleFontWeight::W800 => 7,
593        StyleFontWeight::W900 => 8,
594        StyleFontWeight::Lighter => 9,
595        StyleFontWeight::Bolder => 10,
596    }
597}
598
599#[inline(always)]
600pub fn style_font_style_from_u8(v: u8) -> StyleFontStyle {
601    match v {
602        0 => StyleFontStyle::Normal,
603        1 => StyleFontStyle::Italic,
604        2 => StyleFontStyle::Oblique,
605        _ => StyleFontStyle::Normal,
606    }
607}
608
609#[inline(always)]
610pub fn style_font_style_to_u8(v: StyleFontStyle) -> u8 {
611    match v {
612        StyleFontStyle::Normal => 0,
613        StyleFontStyle::Italic => 1,
614        StyleFontStyle::Oblique => 2,
615    }
616}
617
618#[inline(always)]
619pub fn style_text_align_from_u8(v: u8) -> StyleTextAlign {
620    match v {
621        0 => StyleTextAlign::Left,
622        1 => StyleTextAlign::Center,
623        2 => StyleTextAlign::Right,
624        3 => StyleTextAlign::Justify,
625        4 => StyleTextAlign::Start,
626        5 => StyleTextAlign::End,
627        _ => StyleTextAlign::Left,
628    }
629}
630
631#[inline(always)]
632pub fn style_text_align_to_u8(v: StyleTextAlign) -> u8 {
633    match v {
634        StyleTextAlign::Left => 0,
635        StyleTextAlign::Center => 1,
636        StyleTextAlign::Right => 2,
637        StyleTextAlign::Justify => 3,
638        StyleTextAlign::Start => 4,
639        StyleTextAlign::End => 5,
640    }
641}
642
643#[inline(always)]
644pub fn style_visibility_from_u8(v: u8) -> StyleVisibility {
645    match v {
646        0 => StyleVisibility::Visible,
647        1 => StyleVisibility::Hidden,
648        2 => StyleVisibility::Collapse,
649        _ => StyleVisibility::Visible,
650    }
651}
652
653#[inline(always)]
654pub fn style_visibility_to_u8(v: StyleVisibility) -> u8 {
655    match v {
656        StyleVisibility::Visible => 0,
657        StyleVisibility::Hidden => 1,
658        StyleVisibility::Collapse => 2,
659    }
660}
661
662#[inline(always)]
663pub fn style_white_space_from_u8(v: u8) -> StyleWhiteSpace {
664    match v {
665        0 => StyleWhiteSpace::Normal,
666        1 => StyleWhiteSpace::Pre,
667        2 => StyleWhiteSpace::Nowrap,
668        3 => StyleWhiteSpace::PreWrap,
669        4 => StyleWhiteSpace::PreLine,
670        5 => StyleWhiteSpace::BreakSpaces,
671        _ => StyleWhiteSpace::Normal,
672    }
673}
674
675#[inline(always)]
676pub fn style_white_space_to_u8(v: StyleWhiteSpace) -> u8 {
677    match v {
678        StyleWhiteSpace::Normal => 0,
679        StyleWhiteSpace::Pre => 1,
680        StyleWhiteSpace::Nowrap => 2,
681        StyleWhiteSpace::PreWrap => 3,
682        StyleWhiteSpace::PreLine => 4,
683        StyleWhiteSpace::BreakSpaces => 5,
684    }
685}
686
687#[inline(always)]
688pub fn style_direction_from_u8(v: u8) -> StyleDirection {
689    match v {
690        0 => StyleDirection::Ltr,
691        1 => StyleDirection::Rtl,
692        _ => StyleDirection::Ltr,
693    }
694}
695
696#[inline(always)]
697pub fn style_direction_to_u8(v: StyleDirection) -> u8 {
698    match v {
699        StyleDirection::Ltr => 0,
700        StyleDirection::Rtl => 1,
701    }
702}
703
704#[inline(always)]
705pub fn style_vertical_align_from_u8(v: u8) -> StyleVerticalAlign {
706    match v {
707        0 => StyleVerticalAlign::Baseline,
708        1 => StyleVerticalAlign::Top,
709        2 => StyleVerticalAlign::Middle,
710        3 => StyleVerticalAlign::Bottom,
711        4 => StyleVerticalAlign::Sub,
712        5 => StyleVerticalAlign::Superscript,
713        6 => StyleVerticalAlign::TextTop,
714        7 => StyleVerticalAlign::TextBottom,
715        _ => StyleVerticalAlign::Baseline,
716    }
717}
718
719#[inline(always)]
720pub fn style_vertical_align_to_u8(v: StyleVerticalAlign) -> u8 {
721    match v {
722        StyleVerticalAlign::Baseline => 0,
723        StyleVerticalAlign::Top => 1,
724        StyleVerticalAlign::Middle => 2,
725        StyleVerticalAlign::Bottom => 3,
726        StyleVerticalAlign::Sub => 4,
727        StyleVerticalAlign::Superscript => 5,
728        StyleVerticalAlign::TextTop => 6,
729        StyleVerticalAlign::TextBottom => 7,
730        // Percentage/Length cannot be stored in the 3-bit compact cache field;
731        // fall back to 0 (Baseline). Callers must use the slow cascade path
732        // for vertical-align values with length/percentage units.
733        StyleVerticalAlign::Percentage(_) | StyleVerticalAlign::Length(_) => 0,
734    }
735}
736
737#[inline(always)]
738pub fn border_collapse_from_u8(v: u8) -> StyleBorderCollapse {
739    match v {
740        0 => StyleBorderCollapse::Separate,
741        1 => StyleBorderCollapse::Collapse,
742        _ => StyleBorderCollapse::Separate,
743    }
744}
745
746#[inline(always)]
747pub fn border_collapse_to_u8(v: StyleBorderCollapse) -> u8 {
748    match v {
749        StyleBorderCollapse::Separate => 0,
750        StyleBorderCollapse::Collapse => 1,
751    }
752}
753
754#[inline(always)]
755pub fn border_style_from_u8(v: u8) -> BorderStyle {
756    match v {
757        0 => BorderStyle::None,
758        1 => BorderStyle::Solid,
759        2 => BorderStyle::Double,
760        3 => BorderStyle::Dotted,
761        4 => BorderStyle::Dashed,
762        5 => BorderStyle::Hidden,
763        6 => BorderStyle::Groove,
764        7 => BorderStyle::Ridge,
765        8 => BorderStyle::Inset,
766        9 => BorderStyle::Outset,
767        _ => BorderStyle::None,
768    }
769}
770
771#[inline(always)]
772pub fn border_style_to_u8(v: BorderStyle) -> u8 {
773    match v {
774        BorderStyle::None => 0,
775        BorderStyle::Solid => 1,
776        BorderStyle::Double => 2,
777        BorderStyle::Dotted => 3,
778        BorderStyle::Dashed => 4,
779        BorderStyle::Hidden => 5,
780        BorderStyle::Groove => 6,
781        BorderStyle::Ridge => 7,
782        BorderStyle::Inset => 8,
783        BorderStyle::Outset => 9,
784    }
785}
786
787/// Encode 4 border styles into a u16: [3:0]=top, [7:4]=right, [11:8]=bottom, [15:12]=left
788#[inline]
789pub fn encode_border_styles_packed(top: BorderStyle, right: BorderStyle, bottom: BorderStyle, left: BorderStyle) -> u16 {
790    (border_style_to_u8(top) as u16)
791        | ((border_style_to_u8(right) as u16) << 4)
792        | ((border_style_to_u8(bottom) as u16) << 8)
793        | ((border_style_to_u8(left) as u16) << 12)
794}
795
796/// Decode border-top-style from packed u16
797#[inline(always)]
798pub fn decode_border_top_style(packed: u16) -> BorderStyle {
799    border_style_from_u8((packed & 0x0F) as u8)
800}
801
802/// Decode border-right-style from packed u16
803#[inline(always)]
804pub fn decode_border_right_style(packed: u16) -> BorderStyle {
805    border_style_from_u8(((packed >> 4) & 0x0F) as u8)
806}
807
808/// Decode border-bottom-style from packed u16
809#[inline(always)]
810pub fn decode_border_bottom_style(packed: u16) -> BorderStyle {
811    border_style_from_u8(((packed >> 8) & 0x0F) as u8)
812}
813
814/// Decode border-left-style from packed u16
815#[inline(always)]
816pub fn decode_border_left_style(packed: u16) -> BorderStyle {
817    border_style_from_u8(((packed >> 12) & 0x0F) as u8)
818}
819
820/// Encode a ColorU as u32 (0xRRGGBBAA). Returns 0 for sentinel/unset.
821#[inline(always)]
822pub fn encode_color_u32(c: &ColorU) -> u32 {
823    ((c.r as u32) << 24) | ((c.g as u32) << 16) | ((c.b as u32) << 8) | (c.a as u32)
824}
825
826/// Decode a u32 back to ColorU. Returns `None` if sentinel (`0x00000000`).
827///
828/// **Limitation:** `rgba(0,0,0,0)` (fully transparent black) also encodes as
829/// `0x00000000` and will be decoded as `None` (unset). This is acceptable
830/// because fully transparent black is visually indistinguishable from unset.
831#[inline(always)]
832pub fn decode_color_u32(v: u32) -> Option<ColorU> {
833    if v == 0 { return None; }
834    Some(ColorU {
835        r: ((v >> 24) & 0xFF) as u8,
836        g: ((v >> 16) & 0xFF) as u8,
837        b: ((v >> 8) & 0xFF) as u8,
838        a: (v & 0xFF) as u8,
839    })
840}
841
842// =============================================================================
843// Tier 1: Encode / Decode
844// =============================================================================
845
846/// Pack all 21 enum properties into a single u64.
847#[inline]
848pub fn encode_tier1(
849    display: LayoutDisplay,
850    position: LayoutPosition,
851    float: LayoutFloat,
852    overflow_x: LayoutOverflow,
853    overflow_y: LayoutOverflow,
854    box_sizing: LayoutBoxSizing,
855    flex_direction: LayoutFlexDirection,
856    flex_wrap: LayoutFlexWrap,
857    justify_content: LayoutJustifyContent,
858    align_items: LayoutAlignItems,
859    align_content: LayoutAlignContent,
860    writing_mode: LayoutWritingMode,
861    clear: LayoutClear,
862    font_weight: StyleFontWeight,
863    font_style: StyleFontStyle,
864    text_align: StyleTextAlign,
865    visibility: StyleVisibility,
866    white_space: StyleWhiteSpace,
867    direction: StyleDirection,
868    vertical_align: StyleVerticalAlign,
869    border_collapse: StyleBorderCollapse,
870) -> u64 {
871    let mut v: u64 = TIER1_POPULATED_BIT;
872    v |= (layout_display_to_u8(display) as u64) << DISPLAY_SHIFT;
873    v |= (layout_position_to_u8(position) as u64) << POSITION_SHIFT;
874    v |= (layout_float_to_u8(float) as u64) << FLOAT_SHIFT;
875    v |= (layout_overflow_to_u8(overflow_x) as u64) << OVERFLOW_X_SHIFT;
876    v |= (layout_overflow_to_u8(overflow_y) as u64) << OVERFLOW_Y_SHIFT;
877    v |= (layout_box_sizing_to_u8(box_sizing) as u64) << BOX_SIZING_SHIFT;
878    v |= (layout_flex_direction_to_u8(flex_direction) as u64) << FLEX_DIRECTION_SHIFT;
879    v |= (layout_flex_wrap_to_u8(flex_wrap) as u64) << FLEX_WRAP_SHIFT;
880    v |= (layout_justify_content_to_u8(justify_content) as u64) << JUSTIFY_CONTENT_SHIFT;
881    v |= (layout_align_items_to_u8(align_items) as u64) << ALIGN_ITEMS_SHIFT;
882    v |= (layout_align_content_to_u8(align_content) as u64) << ALIGN_CONTENT_SHIFT;
883    v |= (layout_writing_mode_to_u8(writing_mode) as u64) << WRITING_MODE_SHIFT;
884    v |= (layout_clear_to_u8(clear) as u64) << CLEAR_SHIFT;
885    v |= (style_font_weight_to_u8(font_weight) as u64) << FONT_WEIGHT_SHIFT;
886    v |= (style_font_style_to_u8(font_style) as u64) << FONT_STYLE_SHIFT;
887    v |= (style_text_align_to_u8(text_align) as u64) << TEXT_ALIGN_SHIFT;
888    v |= (style_visibility_to_u8(visibility) as u64) << VISIBILITY_SHIFT;
889    v |= (style_white_space_to_u8(white_space) as u64) << WHITE_SPACE_SHIFT;
890    v |= (style_direction_to_u8(direction) as u64) << DIRECTION_SHIFT;
891    v |= (style_vertical_align_to_u8(vertical_align) as u64) << VERTICAL_ALIGN_SHIFT;
892    v |= (border_collapse_to_u8(border_collapse) as u64) << BORDER_COLLAPSE_SHIFT;
893    v
894}
895
896/// Decode individual enum properties from a Tier 1 u64.
897/// Each function is `#[inline(always)]` for zero-cost extraction.
898
899#[inline(always)]
900pub fn decode_display(t1: u64) -> LayoutDisplay {
901    layout_display_from_u8(((t1 >> DISPLAY_SHIFT) & DISPLAY_MASK) as u8)
902}
903
904#[inline(always)]
905pub fn decode_position(t1: u64) -> LayoutPosition {
906    layout_position_from_u8(((t1 >> POSITION_SHIFT) & POSITION_MASK) as u8)
907}
908
909#[inline(always)]
910pub fn decode_float(t1: u64) -> LayoutFloat {
911    layout_float_from_u8(((t1 >> FLOAT_SHIFT) & FLOAT_MASK) as u8)
912}
913
914#[inline(always)]
915pub fn decode_overflow_x(t1: u64) -> LayoutOverflow {
916    layout_overflow_from_u8(((t1 >> OVERFLOW_X_SHIFT) & OVERFLOW_MASK) as u8)
917}
918
919#[inline(always)]
920pub fn decode_overflow_y(t1: u64) -> LayoutOverflow {
921    layout_overflow_from_u8(((t1 >> OVERFLOW_Y_SHIFT) & OVERFLOW_MASK) as u8)
922}
923
924#[inline(always)]
925pub fn decode_box_sizing(t1: u64) -> LayoutBoxSizing {
926    layout_box_sizing_from_u8(((t1 >> BOX_SIZING_SHIFT) & BOX_SIZING_MASK) as u8)
927}
928
929#[inline(always)]
930pub fn decode_flex_direction(t1: u64) -> LayoutFlexDirection {
931    layout_flex_direction_from_u8(((t1 >> FLEX_DIRECTION_SHIFT) & FLEX_DIR_MASK) as u8)
932}
933
934#[inline(always)]
935pub fn decode_flex_wrap(t1: u64) -> LayoutFlexWrap {
936    layout_flex_wrap_from_u8(((t1 >> FLEX_WRAP_SHIFT) & FLEX_WRAP_MASK) as u8)
937}
938
939#[inline(always)]
940pub fn decode_justify_content(t1: u64) -> LayoutJustifyContent {
941    layout_justify_content_from_u8(((t1 >> JUSTIFY_CONTENT_SHIFT) & JUSTIFY_MASK) as u8)
942}
943
944#[inline(always)]
945pub fn decode_align_items(t1: u64) -> LayoutAlignItems {
946    layout_align_items_from_u8(((t1 >> ALIGN_ITEMS_SHIFT) & ALIGN_MASK) as u8)
947}
948
949#[inline(always)]
950pub fn decode_align_content(t1: u64) -> LayoutAlignContent {
951    layout_align_content_from_u8(((t1 >> ALIGN_CONTENT_SHIFT) & ALIGN_MASK) as u8)
952}
953
954#[inline(always)]
955pub fn decode_writing_mode(t1: u64) -> LayoutWritingMode {
956    layout_writing_mode_from_u8(((t1 >> WRITING_MODE_SHIFT) & WRITING_MODE_MASK) as u8)
957}
958
959#[inline(always)]
960pub fn decode_clear(t1: u64) -> LayoutClear {
961    layout_clear_from_u8(((t1 >> CLEAR_SHIFT) & CLEAR_MASK) as u8)
962}
963
964#[inline(always)]
965pub fn decode_font_weight(t1: u64) -> StyleFontWeight {
966    style_font_weight_from_u8(((t1 >> FONT_WEIGHT_SHIFT) & FONT_WEIGHT_MASK) as u8)
967}
968
969#[inline(always)]
970pub fn decode_font_style(t1: u64) -> StyleFontStyle {
971    style_font_style_from_u8(((t1 >> FONT_STYLE_SHIFT) & FONT_STYLE_MASK) as u8)
972}
973
974#[inline(always)]
975pub fn decode_text_align(t1: u64) -> StyleTextAlign {
976    style_text_align_from_u8(((t1 >> TEXT_ALIGN_SHIFT) & TEXT_ALIGN_MASK) as u8)
977}
978
979#[inline(always)]
980pub fn decode_visibility(t1: u64) -> StyleVisibility {
981    style_visibility_from_u8(((t1 >> VISIBILITY_SHIFT) & VISIBILITY_MASK) as u8)
982}
983
984#[inline(always)]
985pub fn decode_white_space(t1: u64) -> StyleWhiteSpace {
986    style_white_space_from_u8(((t1 >> WHITE_SPACE_SHIFT) & WHITE_SPACE_MASK) as u8)
987}
988
989#[inline(always)]
990pub fn decode_direction(t1: u64) -> StyleDirection {
991    style_direction_from_u8(((t1 >> DIRECTION_SHIFT) & DIRECTION_MASK) as u8)
992}
993
994#[inline(always)]
995pub fn decode_vertical_align(t1: u64) -> StyleVerticalAlign {
996    style_vertical_align_from_u8(((t1 >> VERTICAL_ALIGN_SHIFT) & VERTICAL_ALIGN_MASK) as u8)
997}
998
999#[inline(always)]
1000pub fn decode_border_collapse(t1: u64) -> StyleBorderCollapse {
1001    border_collapse_from_u8(((t1 >> BORDER_COLLAPSE_SHIFT) & BORDER_COLLAPSE_MASK) as u8)
1002}
1003
1004/// Returns true if the tier1 u64 was actually populated by `encode_tier1`.
1005#[inline(always)]
1006#[cfg(test)]
1007pub fn tier1_is_populated(t1: u64) -> bool {
1008    (t1 & TIER1_POPULATED_BIT) != 0
1009}
1010
1011// =============================================================================
1012// Tier 2: CompactNodeProps — numeric dimensions (64 bytes/node)
1013// =============================================================================
1014
1015/// u32 encoding for dimension properties (width, height, min-*, max-*, flex-basis, font-size).
1016///
1017/// Layout: `[3:0] SizeMetric (4 bits) | [31:4] signed fixed-point ×1000 (28 bits)`
1018///
1019/// This matches FloatValue's internal representation (isize × 1000).
1020/// Range: ±134,217.727 at 0.001 precision (28-bit signed = ±2^27 = ±134,217,728 / 1000).
1021///
1022/// Sentinel values use the top of the u32 range (0xFFFFFFF9..0xFFFFFFFF).
1023
1024/// Encode a PixelValue into u32 with SizeMetric. Returns U32_SENTINEL if out of range.
1025#[inline]
1026pub fn encode_pixel_value_u32(pv: &PixelValue) -> u32 {
1027    let metric = size_metric_to_u8(pv.metric) as u32;
1028    let raw = pv.number.number; // already × 1000 (FloatValue internal repr)
1029    // 28-bit signed range: -134_217_728 ..= +134_217_727
1030    if !(-134_217_728..=134_217_727).contains(&raw) {
1031        return U32_SENTINEL; // overflow → tier 3
1032    }
1033    // Pack: low 4 bits = metric, upper 28 bits = value (as unsigned offset)
1034    let value_bits = ((raw as i32) as u32) << 4;
1035    value_bits | metric
1036}
1037
1038/// Decode a u32 back to PixelValue. Returns None for sentinel values.
1039#[inline]
1040pub fn decode_pixel_value_u32(encoded: u32) -> Option<PixelValue> {
1041    if encoded >= U32_SENTINEL_THRESHOLD {
1042        return None; // sentinel
1043    }
1044    let metric = size_metric_from_u8((encoded & 0xF) as u8);
1045    // Cast to i32 FIRST, then arithmetic right-shift to preserve sign bit
1046    let value_bits = (encoded as i32) >> 4;
1047    let raw = value_bits as isize; // × 1000
1048    Some(PixelValue {
1049        metric,
1050        number: FloatValue { number: raw },
1051    })
1052}
1053
1054/// Encode an i16 resolved px value (×10). Returns I16_SENTINEL if out of range.
1055/// Range: -3276.8 ..= +3276.3 px at 0.1px precision.
1056#[inline]
1057pub fn encode_resolved_px_i16(px: f32) -> i16 {
1058    let scaled = (px * 10.0).round() as i32;
1059    if scaled < -32768 || scaled > I16_SENTINEL_THRESHOLD as i32 - 1 {
1060        return I16_SENTINEL; // overflow or too large → tier 3
1061    }
1062    scaled as i16
1063}
1064
1065/// Decode an i16 back to resolved px. Returns None for sentinel values.
1066#[inline(always)]
1067pub fn decode_resolved_px_i16(v: i16) -> Option<f32> {
1068    if v >= I16_SENTINEL_THRESHOLD {
1069        return None;
1070    }
1071    Some(v as f32 / 10.0)
1072}
1073
1074/// Encode a u16 flex value (×100). Returns U16_SENTINEL if out of range.
1075/// Range: 0.00 ..= 655.27 at 0.01 precision.
1076#[inline]
1077pub fn encode_flex_u16(value: f32) -> u16 {
1078    let scaled = (value * 100.0).round() as i32;
1079    if scaled < 0 || scaled >= U16_SENTINEL_THRESHOLD as i32 {
1080        return U16_SENTINEL;
1081    }
1082    scaled as u16
1083}
1084
1085/// Decode a u16 flex value back to f32. Returns None for sentinel values.
1086#[inline(always)]
1087pub fn decode_flex_u16(v: u16) -> Option<f32> {
1088    if v >= U16_SENTINEL_THRESHOLD {
1089        return None;
1090    }
1091    Some(v as f32 / 100.0)
1092}
1093
1094/// SizeMetric → u8 (4 bits, 12 variants)
1095#[inline(always)]
1096pub fn size_metric_to_u8(m: SizeMetric) -> u8 {
1097    match m {
1098        SizeMetric::Px => 0,
1099        SizeMetric::Pt => 1,
1100        SizeMetric::Em => 2,
1101        SizeMetric::Rem => 3,
1102        SizeMetric::In => 4,
1103        SizeMetric::Cm => 5,
1104        SizeMetric::Mm => 6,
1105        SizeMetric::Percent => 7,
1106        SizeMetric::Vw => 8,
1107        SizeMetric::Vh => 9,
1108        SizeMetric::Vmin => 10,
1109        SizeMetric::Vmax => 11,
1110    }
1111}
1112
1113/// u8 → SizeMetric
1114#[inline(always)]
1115pub fn size_metric_from_u8(v: u8) -> SizeMetric {
1116    match v {
1117        0 => SizeMetric::Px,
1118        1 => SizeMetric::Pt,
1119        2 => SizeMetric::Em,
1120        3 => SizeMetric::Rem,
1121        4 => SizeMetric::In,
1122        5 => SizeMetric::Cm,
1123        6 => SizeMetric::Mm,
1124        7 => SizeMetric::Percent,
1125        8 => SizeMetric::Vw,
1126        9 => SizeMetric::Vh,
1127        10 => SizeMetric::Vmin,
1128        11 => SizeMetric::Vmax,
1129        _ => SizeMetric::Px,
1130    }
1131}
1132
1133/// Layout-hot compact numeric properties for a single node (68 bytes).
1134/// Only fields accessed during the constraint-solving loop.
1135/// All dimensions use MSB-sentinel encoding.
1136#[derive(Debug, Copy, Clone, PartialEq)]
1137#[repr(C)]
1138pub struct CompactNodeProps {
1139    // --- Dimensions needing unit (u32 MSB-sentinel) ---
1140    pub width: u32,
1141    pub height: u32,
1142    pub min_width: u32,
1143    pub max_width: u32,
1144    pub min_height: u32,
1145    pub max_height: u32,
1146    pub flex_basis: u32,
1147    pub font_size: u32,
1148
1149    // --- Resolved px values (i16 MSB-sentinel, ×10) ---
1150    pub padding_top: i16,
1151    pub padding_right: i16,
1152    pub padding_bottom: i16,
1153    pub padding_left: i16,
1154    pub margin_top: i16,
1155    pub margin_right: i16,
1156    pub margin_bottom: i16,
1157    pub margin_left: i16,
1158    pub border_top_width: i16,
1159    pub border_right_width: i16,
1160    pub border_bottom_width: i16,
1161    pub border_left_width: i16,
1162    pub top: i16,
1163    pub right: i16,
1164    pub bottom: i16,
1165    pub left: i16,
1166
1167    // --- Flex (u16 MSB-sentinel, ×100) ---
1168    pub flex_grow: u16,
1169    pub flex_shrink: u16,
1170
1171    // --- Gap (i16 px×10, 0 = default) ---
1172    pub row_gap: i16,
1173    pub column_gap: i16,
1174}
1175
1176/// Paint-cold compact properties for a single node.
1177/// Only accessed during display list generation, table layout, or text shaping.
1178#[derive(Debug, Copy, Clone, PartialEq)]
1179#[repr(C)]
1180pub struct CompactNodePropsCold {
1181    // --- Border colors (u32 RGBA as 0xRRGGBBAA, 0 = unset sentinel) ---
1182    pub border_top_color: u32,
1183    pub border_right_color: u32,
1184    pub border_bottom_color: u32,
1185    pub border_left_color: u32,
1186
1187    // --- Border radii (i16 px × 10, I16_SENTINEL = unset/default = 0) ---
1188    pub border_top_left_radius: i16,
1189    pub border_top_right_radius: i16,
1190    pub border_bottom_left_radius: i16,
1191    pub border_bottom_right_radius: i16,
1192
1193    // --- Other ---
1194    pub z_index: i16,   // range ±32764, sentinel = 0x7FFF
1195    /// Border styles packed: [3:0]=top, [7:4]=right, [11:8]=bottom, [15:12]=left
1196    pub border_styles_packed: u16,
1197    pub border_spacing_h: i16,
1198    pub border_spacing_v: i16,
1199    pub tab_size: i16,
1200    /// Grid column start (I16_AUTO = auto, positive = line number, negative = span)
1201    pub grid_col_start: i16,
1202    /// Grid column end
1203    pub grid_col_end: i16,
1204    /// Grid row start
1205    pub grid_row_start: i16,
1206    /// Grid row end
1207    pub grid_row_end: i16,
1208
1209    // --- GPU / hot paint props ---
1210    /// Opacity × 254 (0 = fully transparent, 254 = opaque). 255 = unset/default (= 1.0).
1211    pub opacity: u8,
1212    /// Bitflags for properties that are usually unset. Lets the getter
1213    /// short-circuit without a cascade walk when the value is the default.
1214    ///
1215    /// bit 0: has_transform                (slow-walk only when set)
1216    /// bit 1: has_transform_origin
1217    /// bit 2: has_box_shadow
1218    /// bit 3: has_text_decoration          (slow-walk only when set)
1219    /// bits 4-5: scrollbar_gutter (0 = auto default, 1 = stable, 2 = both-edges, 3 = mirror)
1220    /// bit 6: has_background                (slow-walk only when set; ≈ negative fast path)
1221    /// bit 7: has_clip_path                 (slow-walk only when set)
1222    pub hot_flags: u8,
1223    /// Second byte of flags for rarely-set properties.
1224    ///
1225    /// bit 0: has_any_scrollbar_css
1226    ///        OR of all -azul-scrollbar-* / scrollbar-color / scrollbar-width props.
1227    ///        When clear, `get_scrollbar_style` can skip 8 cascade walks and use
1228    ///        the UA-default result.
1229    /// bit 1: has_counter      (counter-reset OR counter-increment)
1230    /// bit 2: has_break        (break-before OR break-after)
1231    /// bit 3: has_text_orientation
1232    /// bit 4: has_text_shadow
1233    /// bit 5: has_backdrop_filter
1234    /// bit 6: has_filter
1235    /// bit 7: has_mix_blend_mode
1236    pub extra_flags: u8,
1237}
1238
1239pub const OPACITY_SENTINEL: u8 = 255;
1240pub const HOT_FLAG_HAS_TRANSFORM: u8 = 1 << 0;
1241pub const HOT_FLAG_HAS_TRANSFORM_ORIGIN: u8 = 1 << 1;
1242pub const HOT_FLAG_HAS_BOX_SHADOW: u8 = 1 << 2;
1243pub const HOT_FLAG_HAS_TEXT_DECORATION: u8 = 1 << 3;
1244pub const HOT_FLAG_SCROLLBAR_GUTTER_SHIFT: u8 = 4;
1245pub const HOT_FLAG_SCROLLBAR_GUTTER_MASK: u8 = 0b0011_0000;
1246pub const HOT_FLAG_HAS_BACKGROUND: u8 = 1 << 6;
1247pub const HOT_FLAG_HAS_CLIP_PATH: u8 = 1 << 7;
1248pub const EXTRA_FLAG_HAS_SCROLLBAR_CSS: u8 = 1 << 0;
1249pub const EXTRA_FLAG_HAS_COUNTER: u8 = 1 << 1;
1250pub const EXTRA_FLAG_HAS_BREAK: u8 = 1 << 2;
1251pub const EXTRA_FLAG_HAS_TEXT_ORIENTATION: u8 = 1 << 3;
1252pub const EXTRA_FLAG_HAS_TEXT_SHADOW: u8 = 1 << 4;
1253pub const EXTRA_FLAG_HAS_BACKDROP_FILTER: u8 = 1 << 5;
1254pub const EXTRA_FLAG_HAS_FILTER: u8 = 1 << 6;
1255pub const EXTRA_FLAG_HAS_MIX_BLEND_MODE: u8 = 1 << 7;
1256
1257// ---- DOM-level rare text prop flags (stored on CompactLayoutCache) ----
1258// Each bit = "some node in this DOM declared this property".
1259// When clear, cascade walks for that prop anywhere in the DOM
1260// necessarily return None → callers can skip the walk and use
1261// the default value. Eliminates ~N × IFC-count walks per layout
1262// in typical pages where these props are never declared.
1263pub const DOM_HAS_SHAPE_INSIDE: u32 = 1 << 0;
1264pub const DOM_HAS_SHAPE_OUTSIDE: u32 = 1 << 1;
1265pub const DOM_HAS_TEXT_JUSTIFY: u32 = 1 << 2;
1266pub const DOM_HAS_TEXT_INDENT: u32 = 1 << 3;
1267pub const DOM_HAS_COLUMN_COUNT: u32 = 1 << 4;
1268pub const DOM_HAS_COLUMN_GAP: u32 = 1 << 5;
1269pub const DOM_HAS_INITIAL_LETTER: u32 = 1 << 6;
1270pub const DOM_HAS_INITIAL_LETTER_ALIGN: u32 = 1 << 7;
1271pub const DOM_HAS_LINE_CLAMP: u32 = 1 << 8;
1272pub const DOM_HAS_HANGING_PUNCTUATION: u32 = 1 << 9;
1273pub const DOM_HAS_TEXT_COMBINE_UPRIGHT: u32 = 1 << 10;
1274pub const DOM_HAS_EXCLUSION_MARGIN: u32 = 1 << 11;
1275pub const DOM_HAS_HYPHENATION_LANGUAGE: u32 = 1 << 12;
1276pub const DOM_HAS_UNICODE_BIDI: u32 = 1 << 13;
1277pub const DOM_HAS_TEXT_BOX_TRIM: u32 = 1 << 14;
1278pub const DOM_HAS_HYPHENS: u32 = 1 << 15;
1279pub const DOM_HAS_WORD_BREAK: u32 = 1 << 16;
1280pub const DOM_HAS_OVERFLOW_WRAP: u32 = 1 << 17;
1281pub const DOM_HAS_LINE_BREAK: u32 = 1 << 18;
1282pub const DOM_HAS_TEXT_ALIGN_LAST: u32 = 1 << 19;
1283pub const DOM_HAS_LINE_HEIGHT: u32 = 1 << 20;
1284pub const DOM_HAS_COLUMN_WIDTH: u32 = 1 << 21;
1285pub const DOM_HAS_SHAPE_MARGIN: u32 = 1 << 22;
1286pub const SCROLLBAR_GUTTER_AUTO: u8 = 0;
1287pub const SCROLLBAR_GUTTER_STABLE: u8 = 1;
1288pub const SCROLLBAR_GUTTER_BOTH_EDGES: u8 = 2;
1289pub const SCROLLBAR_GUTTER_MIRROR: u8 = 3;
1290
1291impl Default for CompactNodeProps {
1292    fn default() -> Self {
1293        Self {
1294            // All dimensions default to Auto
1295            width: U32_AUTO,
1296            height: U32_AUTO,
1297            min_width: U32_AUTO,
1298            max_width: U32_NONE,
1299            min_height: U32_AUTO,
1300            max_height: U32_NONE,
1301            flex_basis: U32_AUTO,
1302            font_size: U32_INITIAL,
1303            // All resolved px default to 0
1304            padding_top: 0,
1305            padding_right: 0,
1306            padding_bottom: 0,
1307            padding_left: 0,
1308            margin_top: 0,
1309            margin_right: 0,
1310            margin_bottom: 0,
1311            margin_left: 0,
1312            border_top_width: 0,
1313            border_right_width: 0,
1314            border_bottom_width: 0,
1315            border_left_width: 0,
1316            top: I16_AUTO,
1317            right: I16_AUTO,
1318            bottom: I16_AUTO,
1319            left: I16_AUTO,
1320            // Flex defaults
1321            flex_grow: 0,
1322            flex_shrink: encode_flex_u16(1.0), // CSS default: flex-shrink: 1
1323
1324            // Gap defaults
1325            row_gap: 0,
1326            column_gap: 0,
1327        }
1328    }
1329}
1330
1331impl Default for CompactNodePropsCold {
1332    fn default() -> Self {
1333        Self {
1334            // Border colors default to 0 (sentinel/unset)
1335            border_top_color: 0,
1336            border_right_color: 0,
1337            border_bottom_color: 0,
1338            border_left_color: 0,
1339            // Border radii: I16_SENTINEL means "no rounded corner" (skip slow walk)
1340            border_top_left_radius: I16_SENTINEL,
1341            border_top_right_radius: I16_SENTINEL,
1342            border_bottom_left_radius: I16_SENTINEL,
1343            border_bottom_right_radius: I16_SENTINEL,
1344            // Other
1345            z_index: I16_AUTO,
1346            border_styles_packed: 0, // all BorderStyle::None
1347            border_spacing_h: 0,
1348            border_spacing_v: 0,
1349            tab_size: I16_SENTINEL, // default is 8em, needs resolution → sentinel
1350            grid_col_start: I16_AUTO,
1351            grid_col_end: I16_AUTO,
1352            grid_row_start: I16_AUTO,
1353            grid_row_end: I16_AUTO,
1354            opacity: OPACITY_SENTINEL,
1355            hot_flags: 0,
1356            extra_flags: 0,
1357        }
1358    }
1359}
1360
1361// =============================================================================
1362// Tier 2b: CompactTextProps — IFC/text properties (24 bytes/node)
1363// =============================================================================
1364
1365/// Compact text/IFC properties for a single node (24 bytes).
1366#[derive(Debug, Copy, Clone, PartialEq)]
1367#[repr(C)]
1368pub struct CompactTextProps {
1369    pub text_color: u32,       // RGBA as 0xRRGGBBAA (0 = transparent/unset)
1370    pub font_family_hash: u64, // FxHash of font-family list (0 = sentinel/unset)
1371    pub line_height: i16,      // px × 10, sentinel = I16_SENTINEL
1372    pub letter_spacing: i16,   // px × 10
1373    pub word_spacing: i16,     // px × 10
1374    pub text_indent: i16,      // px × 10
1375}
1376
1377impl Default for CompactTextProps {
1378    fn default() -> Self {
1379        Self {
1380            text_color: 0,
1381            font_family_hash: 0,
1382            line_height: I16_SENTINEL, // "normal" → sentinel
1383            letter_spacing: 0,
1384            word_spacing: 0,
1385            text_indent: 0,
1386        }
1387    }
1388}
1389
1390// =============================================================================
1391// Tier 3: Overflow map — rare/complex properties
1392// =============================================================================
1393
1394/// Overflow properties that couldn't fit in Tier 1/2 encoding.
1395/// Contains the original `CssProperty` values for properties that:
1396/// - Have calc() expressions
1397/// - Exceed the numeric range of compact encoding
1398/// - Are rare CSS properties (grid, transforms, etc.)
1399// =============================================================================
1400// CompactLayoutCache — the top-level container
1401// =============================================================================
1402
1403/// Three-tier compact layout property cache.
1404///
1405/// Allocated once per restyle, indexed by node index (same as NodeId).
1406/// Provides O(1) array-indexed access to all layout properties.
1407///
1408/// Non-compact properties (background, box-shadow, transform, etc.) are
1409/// resolved via the slow cascade path in `CssPropertyCache::get_property_slow()`.
1410#[derive(Debug, Clone, PartialEq)]
1411pub struct CompactLayoutCache {
1412    /// Tier 1: ALL enum properties bitpacked into u64 per node (8 B/node)
1413    pub tier1_enums: Vec<u64>,
1414    /// Tier 2 hot: Layout-critical numeric dimensions per node (68 B/node)
1415    pub tier2_dims: Vec<CompactNodeProps>,
1416    /// Tier 2 cold: Paint-only properties per node (28 B/node)
1417    pub tier2_cold: Vec<CompactNodePropsCold>,
1418    /// Tier 2b: Text/IFC properties per node (24 B/node)
1419    pub tier2b_text: Vec<CompactTextProps>,
1420    /// Indices of nodes whose `font_family_hash` changed since the last frame.
1421    ///
1422    /// Enables **per-node** font dirty tracking instead of the global all-or-nothing
1423    /// `font_stacks_hash` XOR approach. When this list is non-empty, only the
1424    /// font chains for these specific nodes need to be re-resolved, avoiding O(N)
1425    /// re-resolution when a single node's `font-family` changes.
1426    ///
1427    /// Populated during `build_compact_cache()` by comparing each node's
1428    /// `font_family_hash` against `prev_font_hashes`.
1429    pub font_dirty_nodes: Vec<usize>,
1430    /// Previous frame's per-node `font_family_hash` values.
1431    ///
1432    /// Stored after each compact cache build so that the next build can detect
1433    /// which specific nodes' font-family changed (rather than relying on a
1434    /// collision-prone global XOR hash).
1435    pub prev_font_hashes: Vec<u64>,
1436    /// Reverse map: `font_family_hash` (u64) → actual `StyleFontFamilyVec`.
1437    ///
1438    /// Populated during `build_compact_cache()` as a byproduct of hash computation.
1439    /// Consumers use this to look up font family names from the compact cache hash
1440    /// without going through `get_property_slow()` (which fails for inherited values
1441    /// on text nodes).
1442    pub font_hash_to_families: alloc::collections::BTreeMap<u64, crate::props::basic::font::StyleFontFamilyVec>,
1443    /// Bitfield tracking which rare text props are declared *anywhere* in the DOM.
1444    /// Built once during `build_compact_cache_with_inheritance`. When a bit is
1445    /// clear, callers (e.g. `translate_to_text3_constraints`) can skip the
1446    /// cascade walk for that property — its slow path would always return
1447    /// `None` and fall back to the default. See `DOM_HAS_*` constants.
1448    pub dom_declared_flags: u32,
1449}
1450
1451impl CompactLayoutCache {
1452    /// Create an empty cache (no nodes).
1453    pub fn empty() -> Self {
1454        Self {
1455            tier1_enums: Vec::new(),
1456            tier2_dims: Vec::new(),
1457            tier2_cold: Vec::new(),
1458            tier2b_text: Vec::new(),
1459            font_dirty_nodes: Vec::new(),
1460            prev_font_hashes: Vec::new(),
1461            font_hash_to_families: alloc::collections::BTreeMap::new(),
1462            dom_declared_flags: 0,
1463        }
1464    }
1465
1466    /// Create a cache pre-allocated for `node_count` nodes, filled with defaults.
1467    pub fn with_capacity(node_count: usize) -> Self {
1468        Self {
1469            tier1_enums: vec![0u64; node_count],
1470            tier2_dims: vec![CompactNodeProps::default(); node_count],
1471            tier2_cold: vec![CompactNodePropsCold::default(); node_count],
1472            tier2b_text: vec![CompactTextProps::default(); node_count],
1473            font_dirty_nodes: Vec::new(),
1474            prev_font_hashes: vec![0u64; node_count],
1475            font_hash_to_families: alloc::collections::BTreeMap::new(),
1476            dom_declared_flags: 0,
1477        }
1478    }
1479
1480    /// Number of nodes in this cache.
1481    #[inline]
1482    pub fn node_count(&self) -> usize {
1483        self.tier1_enums.len()
1484    }
1485
1486    // -- Tier 1 getters (enum properties) --
1487
1488    #[inline(always)]
1489    pub fn get_display(&self, node_idx: usize) -> LayoutDisplay {
1490        decode_display(self.tier1_enums[node_idx])
1491    }
1492
1493    #[inline(always)]
1494    pub fn get_position(&self, node_idx: usize) -> LayoutPosition {
1495        decode_position(self.tier1_enums[node_idx])
1496    }
1497
1498    #[inline(always)]
1499    pub fn get_float(&self, node_idx: usize) -> LayoutFloat {
1500        decode_float(self.tier1_enums[node_idx])
1501    }
1502
1503    #[inline(always)]
1504    pub fn get_overflow_x(&self, node_idx: usize) -> LayoutOverflow {
1505        decode_overflow_x(self.tier1_enums[node_idx])
1506    }
1507
1508    #[inline(always)]
1509    pub fn get_overflow_y(&self, node_idx: usize) -> LayoutOverflow {
1510        decode_overflow_y(self.tier1_enums[node_idx])
1511    }
1512
1513    #[inline(always)]
1514    pub fn get_box_sizing(&self, node_idx: usize) -> LayoutBoxSizing {
1515        decode_box_sizing(self.tier1_enums[node_idx])
1516    }
1517
1518    #[inline(always)]
1519    pub fn get_flex_direction(&self, node_idx: usize) -> LayoutFlexDirection {
1520        decode_flex_direction(self.tier1_enums[node_idx])
1521    }
1522
1523    #[inline(always)]
1524    pub fn get_flex_wrap(&self, node_idx: usize) -> LayoutFlexWrap {
1525        decode_flex_wrap(self.tier1_enums[node_idx])
1526    }
1527
1528    #[inline(always)]
1529    pub fn get_justify_content(&self, node_idx: usize) -> LayoutJustifyContent {
1530        decode_justify_content(self.tier1_enums[node_idx])
1531    }
1532
1533    #[inline(always)]
1534    pub fn get_align_items(&self, node_idx: usize) -> LayoutAlignItems {
1535        decode_align_items(self.tier1_enums[node_idx])
1536    }
1537
1538    #[inline(always)]
1539    pub fn get_align_content(&self, node_idx: usize) -> LayoutAlignContent {
1540        decode_align_content(self.tier1_enums[node_idx])
1541    }
1542
1543    #[inline(always)]
1544    pub fn get_writing_mode(&self, node_idx: usize) -> LayoutWritingMode {
1545        decode_writing_mode(self.tier1_enums[node_idx])
1546    }
1547
1548    #[inline(always)]
1549    pub fn get_clear(&self, node_idx: usize) -> LayoutClear {
1550        decode_clear(self.tier1_enums[node_idx])
1551    }
1552
1553    #[inline(always)]
1554    pub fn get_font_weight(&self, node_idx: usize) -> StyleFontWeight {
1555        decode_font_weight(self.tier1_enums[node_idx])
1556    }
1557
1558    #[inline(always)]
1559    pub fn get_font_style(&self, node_idx: usize) -> StyleFontStyle {
1560        decode_font_style(self.tier1_enums[node_idx])
1561    }
1562
1563    #[inline(always)]
1564    pub fn get_text_align(&self, node_idx: usize) -> StyleTextAlign {
1565        decode_text_align(self.tier1_enums[node_idx])
1566    }
1567
1568    #[inline(always)]
1569    pub fn get_visibility(&self, node_idx: usize) -> StyleVisibility {
1570        decode_visibility(self.tier1_enums[node_idx])
1571    }
1572
1573    #[inline(always)]
1574    pub fn get_white_space(&self, node_idx: usize) -> StyleWhiteSpace {
1575        decode_white_space(self.tier1_enums[node_idx])
1576    }
1577
1578    #[inline(always)]
1579    pub fn get_direction(&self, node_idx: usize) -> StyleDirection {
1580        decode_direction(self.tier1_enums[node_idx])
1581    }
1582
1583    #[inline(always)]
1584    pub fn get_vertical_align(&self, node_idx: usize) -> StyleVerticalAlign {
1585        decode_vertical_align(self.tier1_enums[node_idx])
1586    }
1587
1588    #[inline(always)]
1589    pub fn get_border_collapse(&self, node_idx: usize) -> StyleBorderCollapse {
1590        decode_border_collapse(self.tier1_enums[node_idx])
1591    }
1592
1593    // -- Tier 2 getters (numeric dimensions) --
1594
1595    /// Get width as encoded u32 (use `decode_pixel_value_u32` or check sentinel).
1596    #[inline(always)]
1597    pub fn get_width_raw(&self, node_idx: usize) -> u32 {
1598        self.tier2_dims[node_idx].width
1599    }
1600
1601    #[inline(always)]
1602    pub fn get_height_raw(&self, node_idx: usize) -> u32 {
1603        self.tier2_dims[node_idx].height
1604    }
1605
1606    #[inline(always)]
1607    pub fn get_min_width_raw(&self, node_idx: usize) -> u32 {
1608        self.tier2_dims[node_idx].min_width
1609    }
1610
1611    #[inline(always)]
1612    pub fn get_max_width_raw(&self, node_idx: usize) -> u32 {
1613        self.tier2_dims[node_idx].max_width
1614    }
1615
1616    #[inline(always)]
1617    pub fn get_min_height_raw(&self, node_idx: usize) -> u32 {
1618        self.tier2_dims[node_idx].min_height
1619    }
1620
1621    #[inline(always)]
1622    pub fn get_max_height_raw(&self, node_idx: usize) -> u32 {
1623        self.tier2_dims[node_idx].max_height
1624    }
1625
1626    #[inline(always)]
1627    pub fn get_font_size_raw(&self, node_idx: usize) -> u32 {
1628        self.tier2_dims[node_idx].font_size
1629    }
1630
1631    #[inline(always)]
1632    pub fn get_flex_basis_raw(&self, node_idx: usize) -> u32 {
1633        self.tier2_dims[node_idx].flex_basis
1634    }
1635
1636    /// Get padding-top as resolved px. Returns None if sentinel (needs slow path).
1637    #[inline(always)]
1638    pub fn get_padding_top(&self, node_idx: usize) -> Option<f32> {
1639        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_top)
1640    }
1641
1642    #[inline(always)]
1643    pub fn get_padding_right(&self, node_idx: usize) -> Option<f32> {
1644        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_right)
1645    }
1646
1647    #[inline(always)]
1648    pub fn get_padding_bottom(&self, node_idx: usize) -> Option<f32> {
1649        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_bottom)
1650    }
1651
1652    #[inline(always)]
1653    pub fn get_padding_left(&self, node_idx: usize) -> Option<f32> {
1654        decode_resolved_px_i16(self.tier2_dims[node_idx].padding_left)
1655    }
1656
1657    #[inline(always)]
1658    pub fn get_margin_top(&self, node_idx: usize) -> Option<f32> {
1659        let v = self.tier2_dims[node_idx].margin_top;
1660        if v == I16_AUTO { return None; } // Auto for margin is special
1661        decode_resolved_px_i16(v)
1662    }
1663
1664    #[inline(always)]
1665    pub fn get_margin_right(&self, node_idx: usize) -> Option<f32> {
1666        let v = self.tier2_dims[node_idx].margin_right;
1667        if v == I16_AUTO { return None; }
1668        decode_resolved_px_i16(v)
1669    }
1670
1671    #[inline(always)]
1672    pub fn get_margin_bottom(&self, node_idx: usize) -> Option<f32> {
1673        let v = self.tier2_dims[node_idx].margin_bottom;
1674        if v == I16_AUTO { return None; }
1675        decode_resolved_px_i16(v)
1676    }
1677
1678    #[inline(always)]
1679    pub fn get_margin_left(&self, node_idx: usize) -> Option<f32> {
1680        let v = self.tier2_dims[node_idx].margin_left;
1681        if v == I16_AUTO { return None; }
1682        decode_resolved_px_i16(v)
1683    }
1684
1685    /// Check if margin is Auto (important for centering logic).
1686    #[inline(always)]
1687    pub fn is_margin_top_auto(&self, node_idx: usize) -> bool {
1688        self.tier2_dims[node_idx].margin_top == I16_AUTO
1689    }
1690
1691    #[inline(always)]
1692    pub fn is_margin_right_auto(&self, node_idx: usize) -> bool {
1693        self.tier2_dims[node_idx].margin_right == I16_AUTO
1694    }
1695
1696    #[inline(always)]
1697    pub fn is_margin_bottom_auto(&self, node_idx: usize) -> bool {
1698        self.tier2_dims[node_idx].margin_bottom == I16_AUTO
1699    }
1700
1701    #[inline(always)]
1702    pub fn is_margin_left_auto(&self, node_idx: usize) -> bool {
1703        self.tier2_dims[node_idx].margin_left == I16_AUTO
1704    }
1705
1706    #[inline(always)]
1707    pub fn get_border_top_width(&self, node_idx: usize) -> Option<f32> {
1708        decode_resolved_px_i16(self.tier2_dims[node_idx].border_top_width)
1709    }
1710
1711    #[inline(always)]
1712    pub fn get_border_right_width(&self, node_idx: usize) -> Option<f32> {
1713        decode_resolved_px_i16(self.tier2_dims[node_idx].border_right_width)
1714    }
1715
1716    #[inline(always)]
1717    pub fn get_border_bottom_width(&self, node_idx: usize) -> Option<f32> {
1718        decode_resolved_px_i16(self.tier2_dims[node_idx].border_bottom_width)
1719    }
1720
1721    #[inline(always)]
1722    pub fn get_border_left_width(&self, node_idx: usize) -> Option<f32> {
1723        decode_resolved_px_i16(self.tier2_dims[node_idx].border_left_width)
1724    }
1725
1726    // -- Raw i16 getters for macro fast paths --
1727
1728    #[inline(always)]
1729    pub fn get_padding_top_raw(&self, node_idx: usize) -> i16 {
1730        self.tier2_dims[node_idx].padding_top
1731    }
1732
1733    #[inline(always)]
1734    pub fn get_padding_right_raw(&self, node_idx: usize) -> i16 {
1735        self.tier2_dims[node_idx].padding_right
1736    }
1737
1738    #[inline(always)]
1739    pub fn get_padding_bottom_raw(&self, node_idx: usize) -> i16 {
1740        self.tier2_dims[node_idx].padding_bottom
1741    }
1742
1743    #[inline(always)]
1744    pub fn get_padding_left_raw(&self, node_idx: usize) -> i16 {
1745        self.tier2_dims[node_idx].padding_left
1746    }
1747
1748    #[inline(always)]
1749    pub fn get_margin_top_raw(&self, node_idx: usize) -> i16 {
1750        self.tier2_dims[node_idx].margin_top
1751    }
1752
1753    #[inline(always)]
1754    pub fn get_margin_right_raw(&self, node_idx: usize) -> i16 {
1755        self.tier2_dims[node_idx].margin_right
1756    }
1757
1758    #[inline(always)]
1759    pub fn get_margin_bottom_raw(&self, node_idx: usize) -> i16 {
1760        self.tier2_dims[node_idx].margin_bottom
1761    }
1762
1763    #[inline(always)]
1764    pub fn get_margin_left_raw(&self, node_idx: usize) -> i16 {
1765        self.tier2_dims[node_idx].margin_left
1766    }
1767
1768    #[inline(always)]
1769    pub fn get_border_top_width_raw(&self, node_idx: usize) -> i16 {
1770        self.tier2_dims[node_idx].border_top_width
1771    }
1772
1773    #[inline(always)]
1774    pub fn get_border_right_width_raw(&self, node_idx: usize) -> i16 {
1775        self.tier2_dims[node_idx].border_right_width
1776    }
1777
1778    #[inline(always)]
1779    pub fn get_border_bottom_width_raw(&self, node_idx: usize) -> i16 {
1780        self.tier2_dims[node_idx].border_bottom_width
1781    }
1782
1783    #[inline(always)]
1784    pub fn get_border_left_width_raw(&self, node_idx: usize) -> i16 {
1785        self.tier2_dims[node_idx].border_left_width
1786    }
1787
1788    #[inline(always)]
1789    pub fn get_top(&self, node_idx: usize) -> i16 {
1790        self.tier2_dims[node_idx].top
1791    }
1792
1793    #[inline(always)]
1794    pub fn get_right(&self, node_idx: usize) -> i16 {
1795        self.tier2_dims[node_idx].right
1796    }
1797
1798    #[inline(always)]
1799    pub fn get_bottom(&self, node_idx: usize) -> i16 {
1800        self.tier2_dims[node_idx].bottom
1801    }
1802
1803    #[inline(always)]
1804    pub fn get_left(&self, node_idx: usize) -> i16 {
1805        self.tier2_dims[node_idx].left
1806    }
1807
1808    #[inline(always)]
1809    pub fn get_flex_grow(&self, node_idx: usize) -> Option<f32> {
1810        decode_flex_u16(self.tier2_dims[node_idx].flex_grow)
1811    }
1812
1813    #[inline(always)]
1814    pub fn get_flex_shrink(&self, node_idx: usize) -> Option<f32> {
1815        decode_flex_u16(self.tier2_dims[node_idx].flex_shrink)
1816    }
1817
1818    #[inline(always)]
1819    pub fn get_z_index(&self, node_idx: usize) -> i16 {
1820        self.tier2_cold[node_idx].z_index
1821    }
1822
1823    // -- Border colors (u32 RGBA) — cold tier --
1824
1825    #[inline(always)]
1826    pub fn get_border_top_color_raw(&self, node_idx: usize) -> u32 {
1827        self.tier2_cold[node_idx].border_top_color
1828    }
1829
1830    #[inline(always)]
1831    pub fn get_border_right_color_raw(&self, node_idx: usize) -> u32 {
1832        self.tier2_cold[node_idx].border_right_color
1833    }
1834
1835    #[inline(always)]
1836    pub fn get_border_bottom_color_raw(&self, node_idx: usize) -> u32 {
1837        self.tier2_cold[node_idx].border_bottom_color
1838    }
1839
1840    #[inline(always)]
1841    pub fn get_border_left_color_raw(&self, node_idx: usize) -> u32 {
1842        self.tier2_cold[node_idx].border_left_color
1843    }
1844
1845    // -- Border styles (packed u16) — cold tier --
1846
1847    #[inline(always)]
1848    pub fn get_border_styles_packed(&self, node_idx: usize) -> u16 {
1849        self.tier2_cold[node_idx].border_styles_packed
1850    }
1851
1852    #[inline(always)]
1853    pub fn get_border_top_style(&self, node_idx: usize) -> BorderStyle {
1854        decode_border_top_style(self.tier2_cold[node_idx].border_styles_packed)
1855    }
1856
1857    #[inline(always)]
1858    pub fn get_border_right_style(&self, node_idx: usize) -> BorderStyle {
1859        decode_border_right_style(self.tier2_cold[node_idx].border_styles_packed)
1860    }
1861
1862    #[inline(always)]
1863    pub fn get_border_bottom_style(&self, node_idx: usize) -> BorderStyle {
1864        decode_border_bottom_style(self.tier2_cold[node_idx].border_styles_packed)
1865    }
1866
1867    #[inline(always)]
1868    pub fn get_border_left_style(&self, node_idx: usize) -> BorderStyle {
1869        decode_border_left_style(self.tier2_cold[node_idx].border_styles_packed)
1870    }
1871
1872    // -- Border spacing — cold tier --
1873
1874    #[inline(always)]
1875    pub fn get_border_spacing_h_raw(&self, node_idx: usize) -> i16 {
1876        self.tier2_cold[node_idx].border_spacing_h
1877    }
1878
1879    #[inline(always)]
1880    pub fn get_border_spacing_v_raw(&self, node_idx: usize) -> i16 {
1881        self.tier2_cold[node_idx].border_spacing_v
1882    }
1883
1884    // -- Tab size — cold tier --
1885
1886    #[inline(always)]
1887    pub fn get_tab_size_raw(&self, node_idx: usize) -> i16 {
1888        self.tier2_cold[node_idx].tab_size
1889    }
1890
1891    // -- Border radii — cold tier (i16 px × 10, I16_SENTINEL = unset = 0) --
1892
1893    #[inline(always)]
1894    pub fn get_border_top_left_radius_raw(&self, node_idx: usize) -> i16 {
1895        self.tier2_cold[node_idx].border_top_left_radius
1896    }
1897
1898    #[inline(always)]
1899    pub fn get_border_top_right_radius_raw(&self, node_idx: usize) -> i16 {
1900        self.tier2_cold[node_idx].border_top_right_radius
1901    }
1902
1903    #[inline(always)]
1904    pub fn get_border_bottom_left_radius_raw(&self, node_idx: usize) -> i16 {
1905        self.tier2_cold[node_idx].border_bottom_left_radius
1906    }
1907
1908    #[inline(always)]
1909    pub fn get_border_bottom_right_radius_raw(&self, node_idx: usize) -> i16 {
1910        self.tier2_cold[node_idx].border_bottom_right_radius
1911    }
1912
1913    // -- Opacity / transform / hot flags --
1914
1915    /// Raw opacity byte. `OPACITY_SENTINEL` (255) = unset (default = 1.0).
1916    /// Otherwise value / 254.0 yields the opacity in [0.0, 1.0].
1917    #[inline(always)]
1918    pub fn get_opacity_raw(&self, node_idx: usize) -> u8 {
1919        self.tier2_cold[node_idx].opacity
1920    }
1921
1922    #[inline(always)]
1923    pub fn get_hot_flags(&self, node_idx: usize) -> u8 {
1924        self.tier2_cold[node_idx].hot_flags
1925    }
1926
1927    #[inline(always)]
1928    pub fn has_transform(&self, node_idx: usize) -> bool {
1929        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_TRANSFORM != 0
1930    }
1931
1932    #[inline(always)]
1933    pub fn has_transform_origin(&self, node_idx: usize) -> bool {
1934        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_TRANSFORM_ORIGIN != 0
1935    }
1936
1937    #[inline(always)]
1938    pub fn has_box_shadow(&self, node_idx: usize) -> bool {
1939        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_BOX_SHADOW != 0
1940    }
1941
1942    #[inline(always)]
1943    pub fn has_text_decoration(&self, node_idx: usize) -> bool {
1944        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_TEXT_DECORATION != 0
1945    }
1946
1947    #[inline(always)]
1948    pub fn has_background(&self, node_idx: usize) -> bool {
1949        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0
1950    }
1951
1952    #[inline(always)]
1953    pub fn has_clip_path(&self, node_idx: usize) -> bool {
1954        self.tier2_cold[node_idx].hot_flags & HOT_FLAG_HAS_CLIP_PATH != 0
1955    }
1956
1957    #[inline(always)]
1958    pub fn has_scrollbar_css(&self, node_idx: usize) -> bool {
1959        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_SCROLLBAR_CSS != 0
1960    }
1961
1962    #[inline(always)]
1963    pub fn has_counter(&self, node_idx: usize) -> bool {
1964        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_COUNTER != 0
1965    }
1966
1967    #[inline(always)]
1968    pub fn has_break(&self, node_idx: usize) -> bool {
1969        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_BREAK != 0
1970    }
1971
1972    #[inline(always)]
1973    pub fn has_text_orientation(&self, node_idx: usize) -> bool {
1974        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_TEXT_ORIENTATION != 0
1975    }
1976
1977    #[inline(always)]
1978    pub fn has_text_shadow(&self, node_idx: usize) -> bool {
1979        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_TEXT_SHADOW != 0
1980    }
1981
1982    #[inline(always)]
1983    pub fn has_backdrop_filter(&self, node_idx: usize) -> bool {
1984        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_BACKDROP_FILTER != 0
1985    }
1986
1987    #[inline(always)]
1988    pub fn has_filter(&self, node_idx: usize) -> bool {
1989        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_FILTER != 0
1990    }
1991
1992    #[inline(always)]
1993    pub fn has_mix_blend_mode(&self, node_idx: usize) -> bool {
1994        self.tier2_cold[node_idx].extra_flags & EXTRA_FLAG_HAS_MIX_BLEND_MODE != 0
1995    }
1996
1997    /// DOM-level fast-path check: returns `true` if the given flag bit is set
1998    /// (some node in this DOM declared the corresponding property).
1999    #[inline(always)]
2000    pub fn dom_declared(&self, flag: u32) -> bool {
2001        self.dom_declared_flags & flag != 0
2002    }
2003
2004    /// Scrollbar-gutter: 0 = auto (default), 1 = stable, 2 = both-edges, 3 = mirror.
2005    #[inline(always)]
2006    pub fn get_scrollbar_gutter_bits(&self, node_idx: usize) -> u8 {
2007        (self.tier2_cold[node_idx].hot_flags & HOT_FLAG_SCROLLBAR_GUTTER_MASK)
2008            >> HOT_FLAG_SCROLLBAR_GUTTER_SHIFT
2009    }
2010
2011    // -- Tier 2b getters (text props) --
2012
2013    #[inline(always)]
2014    pub fn get_text_color_raw(&self, node_idx: usize) -> u32 {
2015        self.tier2b_text[node_idx].text_color
2016    }
2017
2018    #[inline(always)]
2019    pub fn get_font_family_hash(&self, node_idx: usize) -> u64 {
2020        self.tier2b_text[node_idx].font_family_hash
2021    }
2022
2023    #[inline(always)]
2024    pub fn get_line_height(&self, node_idx: usize) -> Option<f32> {
2025        decode_resolved_px_i16(self.tier2b_text[node_idx].line_height)
2026    }
2027
2028    #[inline(always)]
2029    pub fn get_letter_spacing(&self, node_idx: usize) -> Option<f32> {
2030        decode_resolved_px_i16(self.tier2b_text[node_idx].letter_spacing)
2031    }
2032
2033    #[inline(always)]
2034    pub fn get_word_spacing(&self, node_idx: usize) -> Option<f32> {
2035        decode_resolved_px_i16(self.tier2b_text[node_idx].word_spacing)
2036    }
2037
2038    #[inline(always)]
2039    pub fn get_text_indent(&self, node_idx: usize) -> Option<f32> {
2040        decode_resolved_px_i16(self.tier2b_text[node_idx].text_indent)
2041    }
2042
2043}
2044
2045// =============================================================================
2046// Helper: encode a CssPropertyValue<PixelValue> into i16 resolved-px
2047// =============================================================================
2048
2049/// Resolve a CssPropertyValue<PixelValue> to an i16 ×10 encoding.
2050/// Only handles `Exact(px(...))` values. Everything else → sentinel.
2051/// For the compact cache builder, we only pre-resolve absolute pixel values.
2052/// Relative units (em, %, etc.) get sentinel and fall back to the slow path.
2053#[inline]
2054pub fn encode_css_pixel_as_i16(prop: &CssPropertyValue<PixelValue>) -> i16 {
2055    match prop {
2056        CssPropertyValue::Exact(pv) => {
2057            if pv.metric == SizeMetric::Px {
2058                encode_resolved_px_i16(pv.number.get())
2059            } else {
2060                I16_SENTINEL // non-px units need resolution context → slow path
2061            }
2062        }
2063        CssPropertyValue::Auto => I16_AUTO,
2064        CssPropertyValue::Initial => I16_INITIAL,
2065        CssPropertyValue::Inherit => I16_INHERIT,
2066        _ => I16_SENTINEL,
2067    }
2068}
2069
2070#[cfg(test)]
2071mod tests {
2072    use super::*;
2073
2074    #[test]
2075    fn test_tier1_roundtrip() {
2076        let t1 = encode_tier1(
2077            LayoutDisplay::Flex,
2078            LayoutPosition::Relative,
2079            LayoutFloat::Left,
2080            LayoutOverflow::Hidden,
2081            LayoutOverflow::Scroll,
2082            LayoutBoxSizing::BorderBox,
2083            LayoutFlexDirection::Column,
2084            LayoutFlexWrap::Wrap,
2085            LayoutJustifyContent::SpaceBetween,
2086            LayoutAlignItems::Center,
2087            LayoutAlignContent::End,
2088            LayoutWritingMode::VerticalRl,
2089            LayoutClear::Both,
2090            StyleFontWeight::Bold,
2091            StyleFontStyle::Italic,
2092            StyleTextAlign::Center,
2093            StyleVisibility::Hidden,
2094            StyleWhiteSpace::Pre,
2095            StyleDirection::Rtl,
2096            StyleVerticalAlign::Middle,
2097            StyleBorderCollapse::Collapse,
2098        );
2099
2100        assert!(tier1_is_populated(t1));
2101        assert_eq!(decode_display(t1), LayoutDisplay::Flex);
2102        assert_eq!(decode_position(t1), LayoutPosition::Relative);
2103        assert_eq!(decode_float(t1), LayoutFloat::Left);
2104        assert_eq!(decode_overflow_x(t1), LayoutOverflow::Hidden);
2105        assert_eq!(decode_overflow_y(t1), LayoutOverflow::Scroll);
2106        assert_eq!(decode_box_sizing(t1), LayoutBoxSizing::BorderBox);
2107        assert_eq!(decode_flex_direction(t1), LayoutFlexDirection::Column);
2108        assert_eq!(decode_flex_wrap(t1), LayoutFlexWrap::Wrap);
2109        assert_eq!(decode_justify_content(t1), LayoutJustifyContent::SpaceBetween);
2110        assert_eq!(decode_align_items(t1), LayoutAlignItems::Center);
2111        assert_eq!(decode_align_content(t1), LayoutAlignContent::End);
2112        assert_eq!(decode_writing_mode(t1), LayoutWritingMode::VerticalRl);
2113        assert_eq!(decode_clear(t1), LayoutClear::Both);
2114        assert_eq!(decode_font_weight(t1), StyleFontWeight::Bold);
2115        assert_eq!(decode_font_style(t1), StyleFontStyle::Italic);
2116        assert_eq!(decode_text_align(t1), StyleTextAlign::Center);
2117        assert_eq!(decode_visibility(t1), StyleVisibility::Hidden);
2118        assert_eq!(decode_white_space(t1), StyleWhiteSpace::Pre);
2119        assert_eq!(decode_direction(t1), StyleDirection::Rtl);
2120        assert_eq!(decode_vertical_align(t1), StyleVerticalAlign::Middle);
2121        assert_eq!(decode_border_collapse(t1), StyleBorderCollapse::Collapse);
2122    }
2123
2124    #[test]
2125    fn test_tier1_defaults() {
2126        let t1 = encode_tier1(
2127            LayoutDisplay::Block,
2128            LayoutPosition::Static,
2129            LayoutFloat::None,
2130            LayoutOverflow::Visible,
2131            LayoutOverflow::Visible,
2132            LayoutBoxSizing::ContentBox,
2133            LayoutFlexDirection::Row,
2134            LayoutFlexWrap::NoWrap,
2135            LayoutJustifyContent::FlexStart,
2136            LayoutAlignItems::Stretch,
2137            LayoutAlignContent::Stretch,
2138            LayoutWritingMode::HorizontalTb,
2139            LayoutClear::None,
2140            StyleFontWeight::Normal,
2141            StyleFontStyle::Normal,
2142            StyleTextAlign::Left,
2143            StyleVisibility::Visible,
2144            StyleWhiteSpace::Normal,
2145            StyleDirection::Ltr,
2146            StyleVerticalAlign::Baseline,
2147            StyleBorderCollapse::Separate,
2148        );
2149
2150        assert!(tier1_is_populated(t1));
2151        assert_eq!(decode_display(t1), LayoutDisplay::Block);
2152        assert_eq!(decode_position(t1), LayoutPosition::Static);
2153    }
2154
2155    #[test]
2156    fn test_pixel_value_u32_roundtrip() {
2157        let pv = PixelValue::px(123.456);
2158        let encoded = encode_pixel_value_u32(&pv);
2159        assert!(encoded < U32_SENTINEL_THRESHOLD);
2160        let decoded = decode_pixel_value_u32(encoded).unwrap();
2161        assert_eq!(decoded.metric, SizeMetric::Px);
2162        // Check within precision (×1000)
2163        assert!((decoded.number.get() - 123.456).abs() < 0.002);
2164    }
2165
2166    #[test]
2167    fn test_pixel_value_u32_percent() {
2168        let pv = PixelValue {
2169            metric: SizeMetric::Percent,
2170            number: FloatValue::new(50.0),
2171        };
2172        let encoded = encode_pixel_value_u32(&pv);
2173        let decoded = decode_pixel_value_u32(encoded).unwrap();
2174        assert_eq!(decoded.metric, SizeMetric::Percent);
2175        assert!((decoded.number.get() - 50.0).abs() < 0.002);
2176    }
2177
2178    #[test]
2179    fn test_sentinel_values() {
2180        assert_eq!(decode_pixel_value_u32(U32_SENTINEL), None);
2181        assert_eq!(decode_pixel_value_u32(U32_AUTO), None);
2182        assert_eq!(decode_pixel_value_u32(U32_MIN_CONTENT), None);
2183        assert_eq!(decode_resolved_px_i16(I16_SENTINEL), None);
2184        assert_eq!(decode_resolved_px_i16(I16_AUTO), None);
2185    }
2186
2187    #[test]
2188    fn test_resolved_px_i16_roundtrip() {
2189        let px = 123.4f32;
2190        let encoded = encode_resolved_px_i16(px);
2191        let decoded = decode_resolved_px_i16(encoded).unwrap();
2192        assert!((decoded - 123.4).abs() < 0.11);
2193
2194        // Negative values
2195        let px = -50.7f32;
2196        let encoded = encode_resolved_px_i16(px);
2197        let decoded = decode_resolved_px_i16(encoded).unwrap();
2198        assert!((decoded - (-50.7)).abs() < 0.11);
2199    }
2200
2201    #[test]
2202    fn test_flex_u16_roundtrip() {
2203        let v = 2.5f32;
2204        let encoded = encode_flex_u16(v);
2205        let decoded = decode_flex_u16(encoded).unwrap();
2206        assert!((decoded - 2.5).abs() < 0.011);
2207    }
2208
2209    #[test]
2210    fn test_compact_node_props_size() {
2211        // 72B hot props: 8×u32 dimensions (32B) + 16×i16 box model (32B)
2212        // + 2×u16 flex (4B) + 1×i16 order + align/pos tier1 bits (4B).
2213        assert_eq!(core::mem::size_of::<CompactNodeProps>(), 72);
2214    }
2215
2216    #[test]
2217    fn test_compact_node_props_cold_size() {
2218        // 48B cold props: 4×u32 border colors (16B) + 4×i16 border radii (8B)
2219        // + 1×i16 z_index + 1×u16 border_styles_packed + 2×i16 border_spacing
2220        // + 1×i16 tab_size + 4×i16 grid placement (8B) + 3×u8 (opacity,
2221        // hot_flags, extra_flags) = 45B, padded to 48B for u32 alignment.
2222        assert_eq!(core::mem::size_of::<CompactNodePropsCold>(), 48);
2223    }
2224
2225    #[test]
2226    fn test_compact_text_props_size() {
2227        assert_eq!(core::mem::size_of::<CompactTextProps>(), 24);
2228    }
2229
2230    // ========================================================================
2231    // Tier1 enum 0-sentinel contract
2232    //
2233    // Tier1 packs 21 enums into a single u64. A bit run that is all zeros
2234    // must decode to the CSS initial value of that property, because an
2235    // unpopulated tier1 field is all zeros. If any encoder shifts so that
2236    // `0 -> something-other-than-initial`, every node that didn't explicitly
2237    // set the property silently gets the wrong default — which is exactly
2238    // how the calc.c grid stretch regression shipped (Start encoded as 0,
2239    // so every grid container reported justify-items: Start instead of
2240    // the CSS default Stretch-for-grid, collapsing the calc button grid).
2241    //
2242    // Test invariant: decoding a u8 of 0 for every enum yields the CSS
2243    // initial value of that property.
2244    // ========================================================================
2245
2246    #[test]
2247    fn test_justify_items_zero_is_stretch() {
2248        // CSS initial for justify-items is `normal`, which on a grid item
2249        // behaves as `stretch`. The tier1 bit pattern 0 must round-trip to
2250        // Stretch so unset grid containers don't collapse their items.
2251        assert_eq!(layout_justify_items_from_u8(0), LayoutJustifyItems::Stretch);
2252        assert_eq!(layout_justify_items_to_u8(LayoutJustifyItems::Stretch), 0);
2253    }
2254
2255    #[test]
2256    fn test_tier1_enum_zero_sentinel_is_css_initial() {
2257        assert_eq!(layout_display_from_u8(0), LayoutDisplay::Block);
2258        assert_eq!(layout_position_from_u8(0), LayoutPosition::Static);
2259        assert_eq!(layout_float_from_u8(0), LayoutFloat::None);
2260        assert_eq!(layout_overflow_from_u8(0), LayoutOverflow::Visible);
2261        assert_eq!(layout_box_sizing_from_u8(0), LayoutBoxSizing::ContentBox);
2262        assert_eq!(layout_flex_direction_from_u8(0), LayoutFlexDirection::Row);
2263        assert_eq!(layout_flex_wrap_from_u8(0), LayoutFlexWrap::NoWrap);
2264        assert_eq!(layout_justify_content_from_u8(0), LayoutJustifyContent::FlexStart);
2265        assert_eq!(layout_align_items_from_u8(0), LayoutAlignItems::Stretch);
2266        assert_eq!(layout_align_content_from_u8(0), LayoutAlignContent::Stretch);
2267        assert_eq!(layout_align_self_from_u8(0), LayoutAlignSelf::Auto);
2268        assert_eq!(layout_justify_self_from_u8(0), LayoutJustifySelf::Auto);
2269        assert_eq!(layout_justify_items_from_u8(0), LayoutJustifyItems::Stretch);
2270        assert_eq!(layout_grid_auto_flow_from_u8(0), LayoutGridAutoFlow::Row);
2271        assert_eq!(layout_writing_mode_from_u8(0), LayoutWritingMode::HorizontalTb);
2272        assert_eq!(layout_clear_from_u8(0), LayoutClear::None);
2273        assert_eq!(style_font_weight_from_u8(0), StyleFontWeight::Normal);
2274        assert_eq!(style_font_style_from_u8(0), StyleFontStyle::Normal);
2275        // text-align initial is `start`; we collapse `start` → `left` on
2276        // LTR runs at encode time, so the 0 slot decodes to Left.
2277        assert_eq!(style_text_align_from_u8(0), StyleTextAlign::Left);
2278        assert_eq!(style_visibility_from_u8(0), StyleVisibility::Visible);
2279        assert_eq!(style_white_space_from_u8(0), StyleWhiteSpace::Normal);
2280        assert_eq!(style_direction_from_u8(0), StyleDirection::Ltr);
2281        assert_eq!(style_vertical_align_from_u8(0), StyleVerticalAlign::Baseline);
2282        assert_eq!(border_collapse_from_u8(0), StyleBorderCollapse::Separate);
2283    }
2284
2285    #[test]
2286    fn test_tier1_enum_initial_encodes_to_zero() {
2287        // Mirror of the above — encoding the CSS initial value must
2288        // produce 0, otherwise an `all-zeros` tier1 bit run would encode
2289        // a non-initial value and nodes without explicit properties would
2290        // silently inherit the wrong default.
2291        assert_eq!(layout_display_to_u8(LayoutDisplay::Block), 0);
2292        assert_eq!(layout_position_to_u8(LayoutPosition::Static), 0);
2293        assert_eq!(layout_float_to_u8(LayoutFloat::None), 0);
2294        assert_eq!(layout_overflow_to_u8(LayoutOverflow::Visible), 0);
2295        assert_eq!(layout_box_sizing_to_u8(LayoutBoxSizing::ContentBox), 0);
2296        assert_eq!(layout_flex_direction_to_u8(LayoutFlexDirection::Row), 0);
2297        assert_eq!(layout_flex_wrap_to_u8(LayoutFlexWrap::NoWrap), 0);
2298        assert_eq!(layout_justify_content_to_u8(LayoutJustifyContent::FlexStart), 0);
2299        assert_eq!(layout_align_items_to_u8(LayoutAlignItems::Stretch), 0);
2300        assert_eq!(layout_align_content_to_u8(LayoutAlignContent::Stretch), 0);
2301        assert_eq!(layout_align_self_to_u8(LayoutAlignSelf::Auto), 0);
2302        assert_eq!(layout_justify_self_to_u8(LayoutJustifySelf::Auto), 0);
2303        assert_eq!(layout_justify_items_to_u8(LayoutJustifyItems::Stretch), 0);
2304        assert_eq!(layout_grid_auto_flow_to_u8(LayoutGridAutoFlow::Row), 0);
2305        assert_eq!(layout_writing_mode_to_u8(LayoutWritingMode::HorizontalTb), 0);
2306        assert_eq!(layout_clear_to_u8(LayoutClear::None), 0);
2307        assert_eq!(style_font_weight_to_u8(StyleFontWeight::Normal), 0);
2308        assert_eq!(style_font_style_to_u8(StyleFontStyle::Normal), 0);
2309        assert_eq!(style_text_align_to_u8(StyleTextAlign::Left), 0);
2310        assert_eq!(style_visibility_to_u8(StyleVisibility::Visible), 0);
2311        assert_eq!(style_white_space_to_u8(StyleWhiteSpace::Normal), 0);
2312        assert_eq!(style_direction_to_u8(StyleDirection::Ltr), 0);
2313        assert_eq!(style_vertical_align_to_u8(StyleVerticalAlign::Baseline), 0);
2314        assert_eq!(border_collapse_to_u8(StyleBorderCollapse::Separate), 0);
2315    }
2316
2317    // ========================================================================
2318    // Exhaustive round-trip: every variant of every enum must survive
2319    // encode → decode unchanged. Catches any reordering that maps two
2320    // different variants to the same u8, or any mask-width mismatch.
2321    // ========================================================================
2322
2323    macro_rules! roundtrip_all {
2324        ($name:ident, $to:ident, $from:ident, [$($variant:expr),+ $(,)?]) => {
2325            #[test]
2326            fn $name() {
2327                for v in [$($variant),+] {
2328                    let u = $to(v);
2329                    let decoded = $from(u);
2330                    assert_eq!(decoded, v, "{:?} != {:?} (via u8 = {})", decoded, v, u);
2331                }
2332            }
2333        };
2334    }
2335
2336    roundtrip_all!(rt_display, layout_display_to_u8, layout_display_from_u8, [
2337        LayoutDisplay::Block, LayoutDisplay::Inline, LayoutDisplay::InlineBlock,
2338        LayoutDisplay::Flex, LayoutDisplay::None, LayoutDisplay::InlineFlex,
2339        LayoutDisplay::Table, LayoutDisplay::InlineTable, LayoutDisplay::TableRowGroup,
2340        LayoutDisplay::TableHeaderGroup, LayoutDisplay::TableFooterGroup,
2341        LayoutDisplay::TableRow, LayoutDisplay::TableColumnGroup,
2342        LayoutDisplay::TableColumn, LayoutDisplay::TableCell,
2343        LayoutDisplay::TableCaption, LayoutDisplay::FlowRoot,
2344        LayoutDisplay::ListItem, LayoutDisplay::RunIn, LayoutDisplay::Marker,
2345        LayoutDisplay::Grid, LayoutDisplay::InlineGrid, LayoutDisplay::Contents,
2346    ]);
2347
2348    roundtrip_all!(rt_position, layout_position_to_u8, layout_position_from_u8, [
2349        LayoutPosition::Static, LayoutPosition::Relative, LayoutPosition::Absolute,
2350        LayoutPosition::Fixed, LayoutPosition::Sticky,
2351    ]);
2352
2353    roundtrip_all!(rt_float, layout_float_to_u8, layout_float_from_u8, [
2354        LayoutFloat::None, LayoutFloat::Left, LayoutFloat::Right,
2355    ]);
2356
2357    roundtrip_all!(rt_overflow, layout_overflow_to_u8, layout_overflow_from_u8, [
2358        LayoutOverflow::Visible, LayoutOverflow::Hidden, LayoutOverflow::Scroll,
2359        LayoutOverflow::Auto, LayoutOverflow::Clip,
2360    ]);
2361
2362    roundtrip_all!(rt_box_sizing, layout_box_sizing_to_u8, layout_box_sizing_from_u8, [
2363        LayoutBoxSizing::ContentBox, LayoutBoxSizing::BorderBox,
2364    ]);
2365
2366    roundtrip_all!(rt_flex_direction, layout_flex_direction_to_u8, layout_flex_direction_from_u8, [
2367        LayoutFlexDirection::Row, LayoutFlexDirection::RowReverse,
2368        LayoutFlexDirection::Column, LayoutFlexDirection::ColumnReverse,
2369    ]);
2370
2371    roundtrip_all!(rt_flex_wrap, layout_flex_wrap_to_u8, layout_flex_wrap_from_u8, [
2372        LayoutFlexWrap::NoWrap, LayoutFlexWrap::Wrap, LayoutFlexWrap::WrapReverse,
2373    ]);
2374
2375    roundtrip_all!(rt_justify_content, layout_justify_content_to_u8, layout_justify_content_from_u8, [
2376        LayoutJustifyContent::FlexStart, LayoutJustifyContent::FlexEnd,
2377        LayoutJustifyContent::Start, LayoutJustifyContent::End,
2378        LayoutJustifyContent::Center, LayoutJustifyContent::SpaceBetween,
2379        LayoutJustifyContent::SpaceAround, LayoutJustifyContent::SpaceEvenly,
2380    ]);
2381
2382    roundtrip_all!(rt_align_items, layout_align_items_to_u8, layout_align_items_from_u8, [
2383        LayoutAlignItems::Stretch, LayoutAlignItems::Center, LayoutAlignItems::Start,
2384        LayoutAlignItems::End, LayoutAlignItems::Baseline,
2385    ]);
2386
2387    roundtrip_all!(rt_align_self, layout_align_self_to_u8, layout_align_self_from_u8, [
2388        LayoutAlignSelf::Auto, LayoutAlignSelf::Stretch, LayoutAlignSelf::Center,
2389        LayoutAlignSelf::Start, LayoutAlignSelf::End, LayoutAlignSelf::Baseline,
2390    ]);
2391
2392    roundtrip_all!(rt_justify_self, layout_justify_self_to_u8, layout_justify_self_from_u8, [
2393        LayoutJustifySelf::Auto, LayoutJustifySelf::Start, LayoutJustifySelf::End,
2394        LayoutJustifySelf::Center, LayoutJustifySelf::Stretch,
2395    ]);
2396
2397    roundtrip_all!(rt_justify_items, layout_justify_items_to_u8, layout_justify_items_from_u8, [
2398        LayoutJustifyItems::Stretch, LayoutJustifyItems::Start,
2399        LayoutJustifyItems::End, LayoutJustifyItems::Center,
2400    ]);
2401
2402    roundtrip_all!(rt_grid_auto_flow, layout_grid_auto_flow_to_u8, layout_grid_auto_flow_from_u8, [
2403        LayoutGridAutoFlow::Row, LayoutGridAutoFlow::Column,
2404        LayoutGridAutoFlow::RowDense, LayoutGridAutoFlow::ColumnDense,
2405    ]);
2406
2407    roundtrip_all!(rt_align_content, layout_align_content_to_u8, layout_align_content_from_u8, [
2408        LayoutAlignContent::Stretch, LayoutAlignContent::Center,
2409        LayoutAlignContent::Start, LayoutAlignContent::End,
2410        LayoutAlignContent::SpaceBetween, LayoutAlignContent::SpaceAround,
2411    ]);
2412
2413    roundtrip_all!(rt_writing_mode, layout_writing_mode_to_u8, layout_writing_mode_from_u8, [
2414        LayoutWritingMode::HorizontalTb, LayoutWritingMode::VerticalRl,
2415        LayoutWritingMode::VerticalLr,
2416    ]);
2417
2418    roundtrip_all!(rt_clear, layout_clear_to_u8, layout_clear_from_u8, [
2419        LayoutClear::None, LayoutClear::Left, LayoutClear::Right, LayoutClear::Both,
2420    ]);
2421
2422    roundtrip_all!(rt_font_weight, style_font_weight_to_u8, style_font_weight_from_u8, [
2423        StyleFontWeight::Normal, StyleFontWeight::W100, StyleFontWeight::W200,
2424        StyleFontWeight::W300, StyleFontWeight::W500, StyleFontWeight::W600,
2425        StyleFontWeight::Bold, StyleFontWeight::W800, StyleFontWeight::W900,
2426        StyleFontWeight::Lighter, StyleFontWeight::Bolder,
2427    ]);
2428
2429    roundtrip_all!(rt_font_style, style_font_style_to_u8, style_font_style_from_u8, [
2430        StyleFontStyle::Normal, StyleFontStyle::Italic, StyleFontStyle::Oblique,
2431    ]);
2432
2433    roundtrip_all!(rt_text_align, style_text_align_to_u8, style_text_align_from_u8, [
2434        StyleTextAlign::Left, StyleTextAlign::Center, StyleTextAlign::Right,
2435        StyleTextAlign::Justify, StyleTextAlign::Start, StyleTextAlign::End,
2436    ]);
2437
2438    roundtrip_all!(rt_visibility, style_visibility_to_u8, style_visibility_from_u8, [
2439        StyleVisibility::Visible, StyleVisibility::Hidden, StyleVisibility::Collapse,
2440    ]);
2441
2442    roundtrip_all!(rt_white_space, style_white_space_to_u8, style_white_space_from_u8, [
2443        StyleWhiteSpace::Normal, StyleWhiteSpace::Pre, StyleWhiteSpace::Nowrap,
2444        StyleWhiteSpace::PreWrap, StyleWhiteSpace::PreLine, StyleWhiteSpace::BreakSpaces,
2445    ]);
2446
2447    roundtrip_all!(rt_direction, style_direction_to_u8, style_direction_from_u8, [
2448        StyleDirection::Ltr, StyleDirection::Rtl,
2449    ]);
2450
2451    roundtrip_all!(rt_vertical_align, style_vertical_align_to_u8, style_vertical_align_from_u8, [
2452        StyleVerticalAlign::Baseline, StyleVerticalAlign::Top, StyleVerticalAlign::Middle,
2453        StyleVerticalAlign::Bottom, StyleVerticalAlign::Sub, StyleVerticalAlign::Superscript,
2454        StyleVerticalAlign::TextTop, StyleVerticalAlign::TextBottom,
2455    ]);
2456
2457    roundtrip_all!(rt_border_collapse, border_collapse_to_u8, border_collapse_from_u8, [
2458        StyleBorderCollapse::Separate, StyleBorderCollapse::Collapse,
2459    ]);
2460
2461    // ========================================================================
2462    // Bit-layout safety: every encoder must produce a u8 whose bits all
2463    // fit inside the mask allocated for that property in the tier1 u64.
2464    // If an enum grows a new variant that overflows its mask, the encoded
2465    // bits would leak into the next property's slot and silently corrupt
2466    // unrelated state.
2467    // ========================================================================
2468
2469    #[test]
2470    fn test_encoded_u8_fits_in_tier1_mask() {
2471        fn assert_fits(name: &str, val: u8, mask: u64) {
2472            assert!(
2473                (val as u64) & !mask == 0,
2474                "{}: encoded u8 {} overflows mask {:b}",
2475                name, val, mask,
2476            );
2477        }
2478
2479        assert_fits("display", layout_display_to_u8(LayoutDisplay::Contents), DISPLAY_MASK);
2480        assert_fits("position", layout_position_to_u8(LayoutPosition::Sticky), POSITION_MASK);
2481        assert_fits("float", layout_float_to_u8(LayoutFloat::Right), FLOAT_MASK);
2482        assert_fits("overflow", layout_overflow_to_u8(LayoutOverflow::Clip), OVERFLOW_MASK);
2483        assert_fits("box_sizing", layout_box_sizing_to_u8(LayoutBoxSizing::BorderBox), BOX_SIZING_MASK);
2484        assert_fits("flex_direction", layout_flex_direction_to_u8(LayoutFlexDirection::ColumnReverse), FLEX_DIR_MASK);
2485        assert_fits("flex_wrap", layout_flex_wrap_to_u8(LayoutFlexWrap::WrapReverse), FLEX_WRAP_MASK);
2486        assert_fits("justify_content", layout_justify_content_to_u8(LayoutJustifyContent::SpaceEvenly), JUSTIFY_MASK);
2487        assert_fits("align_items", layout_align_items_to_u8(LayoutAlignItems::Baseline), ALIGN_MASK);
2488        assert_fits("align_self", layout_align_self_to_u8(LayoutAlignSelf::Baseline), ALIGN_SELF_MASK);
2489        assert_fits("justify_self", layout_justify_self_to_u8(LayoutJustifySelf::Stretch), JUSTIFY_SELF_MASK);
2490        assert_fits("justify_items", layout_justify_items_to_u8(LayoutJustifyItems::Center), JUSTIFY_ITEMS_MASK);
2491        assert_fits("grid_auto_flow", layout_grid_auto_flow_to_u8(LayoutGridAutoFlow::ColumnDense), GRID_AUTO_FLOW_MASK);
2492        assert_fits("align_content", layout_align_content_to_u8(LayoutAlignContent::SpaceAround), ALIGN_MASK);
2493        assert_fits("writing_mode", layout_writing_mode_to_u8(LayoutWritingMode::VerticalLr), WRITING_MODE_MASK);
2494        assert_fits("clear", layout_clear_to_u8(LayoutClear::Both), CLEAR_MASK);
2495        assert_fits("font_weight", style_font_weight_to_u8(StyleFontWeight::Bolder), FONT_WEIGHT_MASK);
2496        assert_fits("font_style", style_font_style_to_u8(StyleFontStyle::Oblique), FONT_STYLE_MASK);
2497        assert_fits("text_align", style_text_align_to_u8(StyleTextAlign::End), TEXT_ALIGN_MASK);
2498        assert_fits("visibility", style_visibility_to_u8(StyleVisibility::Collapse), VISIBILITY_MASK);
2499        assert_fits("white_space", style_white_space_to_u8(StyleWhiteSpace::BreakSpaces), WHITE_SPACE_MASK);
2500        assert_fits("direction", style_direction_to_u8(StyleDirection::Rtl), DIRECTION_MASK);
2501        assert_fits("vertical_align", style_vertical_align_to_u8(StyleVerticalAlign::TextBottom), VERTICAL_ALIGN_MASK);
2502        assert_fits("border_collapse", border_collapse_to_u8(StyleBorderCollapse::Collapse), BORDER_COLLAPSE_MASK);
2503    }
2504
2505    // ========================================================================
2506    // Empty tier1 decodes to all-initial — this is the core contract that
2507    // was violated by the pre-fix justify_items encoding. An empty u64 with
2508    // only TIER1_POPULATED_BIT set must decode every property to its CSS
2509    // initial value; this is how `build_compact_cache` can leave unspecified
2510    // properties at 0 and still produce the correct cascade result.
2511    // ========================================================================
2512
2513    #[test]
2514    fn test_empty_tier1_decodes_to_initial_values() {
2515        let t1 = TIER1_POPULATED_BIT; // populated, but zero content
2516        assert!(tier1_is_populated(t1));
2517        assert_eq!(decode_display(t1), LayoutDisplay::Block);
2518        assert_eq!(decode_position(t1), LayoutPosition::Static);
2519        assert_eq!(decode_float(t1), LayoutFloat::None);
2520        assert_eq!(decode_overflow_x(t1), LayoutOverflow::Visible);
2521        assert_eq!(decode_overflow_y(t1), LayoutOverflow::Visible);
2522        assert_eq!(decode_box_sizing(t1), LayoutBoxSizing::ContentBox);
2523        assert_eq!(decode_flex_direction(t1), LayoutFlexDirection::Row);
2524        assert_eq!(decode_flex_wrap(t1), LayoutFlexWrap::NoWrap);
2525        assert_eq!(decode_justify_content(t1), LayoutJustifyContent::FlexStart);
2526        assert_eq!(decode_align_items(t1), LayoutAlignItems::Stretch);
2527        assert_eq!(decode_align_content(t1), LayoutAlignContent::Stretch);
2528        assert_eq!(decode_writing_mode(t1), LayoutWritingMode::HorizontalTb);
2529        assert_eq!(decode_clear(t1), LayoutClear::None);
2530        assert_eq!(decode_font_weight(t1), StyleFontWeight::Normal);
2531        assert_eq!(decode_font_style(t1), StyleFontStyle::Normal);
2532        assert_eq!(decode_text_align(t1), StyleTextAlign::Left);
2533        assert_eq!(decode_visibility(t1), StyleVisibility::Visible);
2534        assert_eq!(decode_white_space(t1), StyleWhiteSpace::Normal);
2535        assert_eq!(decode_direction(t1), StyleDirection::Ltr);
2536        assert_eq!(decode_vertical_align(t1), StyleVerticalAlign::Baseline);
2537        assert_eq!(decode_border_collapse(t1), StyleBorderCollapse::Separate);
2538    }
2539}