1
//! Box model geometry types and writing-mode support for the layout solver.
2
//!
3
//! Provides edge-size types (`EdgeSizes`, `ResolvedBoxProps`, `PackedBoxProps`),
4
//! CSS value resolution (`UnresolvedMargin`, `UnresolvedEdge`, `ResolutionParams`),
5
//! intrinsic sizing (`IntrinsicSizes`), and writing-mode context (`WritingModeContext`).
6

            
7
use azul_core::{
8
    geom::{LogicalPosition, LogicalRect, LogicalSize},
9
    ui_solver::ResolvedOffsets,
10
};
11
use azul_css::props::{
12
    basic::{pixel::PixelValue, PhysicalSize, PropertyContext, ResolutionContext, SizeMetric},
13
    layout::LayoutWritingMode,
14
    style::{StyleDirection, StyleTextOrientation},
15
};
16

            
17
#[derive(Copy, Debug, Clone, PartialEq, PartialOrd)]
18
pub struct PositionedRectangle {
19
    /// The outer bounds of the rectangle
20
    pub bounds: LogicalRect,
21
    /// Margin of the rectangle.
22
    pub margin: ResolvedOffsets,
23
    /// Border widths of the rectangle.
24
    pub border: ResolvedOffsets,
25
    /// Padding of the rectangle.
26
    pub padding: ResolvedOffsets,
27
}
28

            
29
// +spec:box-model:83b3b8 - Box dimensions: content area with optional padding, border, margin areas
30
/// Represents the four edges of a box for properties like margin, padding, border.
31
// +spec:box-model:3b155c - "4 values assigned to sides" pattern (top, right, bottom, left) matching margin/inset shorthands
32
// +spec:width-calculation:37f9e7 - CSS 2.2 §8.1 box dimensions: content, padding, border, margin areas with top/right/bottom/left segments
33
#[derive(Debug, Clone, Copy, Default)]
34
pub struct EdgeSizes {
35
    pub top: f32,
36
    pub right: f32,
37
    pub bottom: f32,
38
    pub left: f32,
39
}
40

            
41
impl EdgeSizes {
42
    /// Sum of horizontal edges (left + right).
43
88
    #[must_use] pub fn horizontal_sum(&self) -> f32 {
44
88
        self.left + self.right
45
88
    }
46

            
47
    /// Sum of vertical edges (top + bottom).
48
88
    #[must_use] pub fn vertical_sum(&self) -> f32 {
49
88
        self.top + self.bottom
50
88
    }
51

            
52
    // +spec:block-formatting-context:440282 - vertical writing modes use analogous layout via main/cross axis abstraction
53
    // +spec:block-formatting-context:a49f9e - line-relative directions mapped via writing mode
54
    // +spec:block-formatting-context:387117 - writing-mode property maps block flow to vertical/horizontal axes
55
    // +spec:box-model:4c01a3 - dimensional mapping: main=block axis, cross=inline axis per writing mode
56
    // +spec:box-model:4c1a9f - physical-to-logical mapping of margin/padding/border for vertical writing modes
57
    // +spec:box-model:9414ab - flow-relative mapping of box edges (margin/padding/border) per writing mode
58
    // +spec:inline-formatting-context:2de457 - block/inline dimension mapping via writing mode
59
    // +spec:inline-formatting-context:c6b91e - line-relative "over"/"under" mapped to physical top/bottom via writing mode
60
    // +spec:writing-modes:00a918 - Abstract-to-physical mappings for block/inline to top/right/bottom/left
61
    // +spec:writing-modes:14e6f0 - block-start/end depend only on writing-mode; inline-start/end also depend on direction (handled in positioning.rs)
62
    // +spec:writing-modes:1c2101 - Abstract directional terms (top/right/bottom/left) to logical axes (main/cross) based on writing-mode
63
    // +spec:writing-modes:1c5155 - line-relative mappings: over/under/line-left/line-right → top/bottom/left/right in horizontal-tb
64
    // +spec:writing-modes:70daf1 - block/inline axis mapping per writing-mode for edge sizes
65
    // +spec:writing-modes:f9af71 - flow-relative directions: block-start/end and inline-start/end mapped to physical edges
66
    // +spec:writing-modes:60b023 - abstract-to-physical mapping: block axis = main, inline axis = cross
67
    // +spec:writing-modes:829cd7 - flow-relative directions: block-start/end from writing-mode, inline-start/end from writing-mode+direction
68
    // +spec:writing-modes:a2113d - block/inline axis mapping for writing modes (block-axis, inline-axis, block-start/end, inline-start/end)
69
    // +spec:writing-modes:c0ae9c - abstract directional mappings from writing-mode/direction
70
    // +spec:writing-modes:c91130 - Abstract box terminology: block/inline axis mapping per writing-mode
71
    // +spec:writing-modes:cd31ce - flow-relative directions mapped to physical via writing mode
72
    // +spec:writing-modes:fd8c18 - block/inline axis mapping based on writing mode
73
    // +spec:writing-modes:0e549a - writing-mode computed value influences physical/logical axis mapping
74
    /// Returns the size of the edge at the start of the main/block axis.
75
1143849
    #[must_use] pub const fn main_start(&self, wm: LayoutWritingMode) -> f32 {
76
1143849
        match wm {
77
1143482
            LayoutWritingMode::HorizontalTb => self.top,
78
367
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.left,
79
        }
80
1143849
    }
81

            
82
    /// Returns the size of the edge at the end of the main/block axis.
83
1117537
    #[must_use] pub const fn main_end(&self, wm: LayoutWritingMode) -> f32 {
84
1117537
        match wm {
85
1117156
            LayoutWritingMode::HorizontalTb => self.bottom,
86
381
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.right,
87
        }
88
1117537
    }
89

            
90
    /// Returns the sum of the start and end sizes on the main/block axis.
91
798534
    #[must_use] pub fn main_sum(&self, wm: LayoutWritingMode) -> f32 {
92
798534
        self.main_start(wm) + self.main_end(wm)
93
798534
    }
94

            
95
    // +spec:block-formatting-context:6225cb - line-relative directions: vertical modes map line-over/under to top/bottom
96
    /// Returns the size of the edge at the start of the cross/inline axis.
97
831718
    #[must_use] pub const fn cross_start(&self, wm: LayoutWritingMode) -> f32 {
98
831718
        match wm {
99
831413
            LayoutWritingMode::HorizontalTb => self.left,
100
305
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.top,
101
        }
102
831718
    }
103

            
104
    /// Returns the size of the edge at the end of the cross/inline axis.
105
780400
    #[must_use] pub const fn cross_end(&self, wm: LayoutWritingMode) -> f32 {
106
780400
        match wm {
107
780097
            LayoutWritingMode::HorizontalTb => self.right,
108
303
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.bottom,
109
        }
110
780400
    }
111

            
112
    /// Returns the sum of the start and end sizes on the cross/inline axis.
113
740828
    #[must_use] pub fn cross_sum(&self, wm: LayoutWritingMode) -> f32 {
114
740828
        self.cross_start(wm) + self.cross_end(wm)
115
740828
    }
116

            
117
    // +spec:block-formatting-context:a49f9e - line-over/line-under are line-relative
118
    //   block-axis edges: top/bottom in horizontal-tb, right/left in vertical modes.
119
    // +spec:inline-formatting-context:c6b91e - line-relative "over"/"under" mapped to
120
    //   physical edges via writing mode.
121
    // +spec:writing-modes:1c5155 - line-relative mappings for over/under.
122
    /// Returns the line-over edge (line-relative block-axis start).
123
    ///
124
    /// Per CSS Writing Modes L4 §6.3, the line-over side is `top` in
125
    /// `horizontal-tb` and `right` in both vertical writing modes. Unlike
126
    /// [`Self::main_start`] (a physical block-axis accessor), this is the
127
    /// *line-relative* over side and coincides with `main_start` only in
128
    /// `horizontal-tb`.
129
8
    #[must_use] pub const fn line_over(&self, wm: LayoutWritingMode) -> f32 {
130
8
        match wm {
131
3
            LayoutWritingMode::HorizontalTb => self.top,
132
5
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.right,
133
        }
134
8
    }
135

            
136
    /// Returns the line-under edge (line-relative block-axis end).
137
    ///
138
    /// Per CSS Writing Modes L4 §6.3, the line-under side is `bottom` in
139
    /// `horizontal-tb` and `left` in both vertical writing modes.
140
5
    #[must_use] pub const fn line_under(&self, wm: LayoutWritingMode) -> f32 {
141
5
        match wm {
142
2
            LayoutWritingMode::HorizontalTb => self.bottom,
143
3
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.left,
144
        }
145
5
    }
146

            
147
    /// Returns the sum of the line-over and line-under edges.
148
2
    #[must_use] pub fn line_over_under_sum(&self, wm: LayoutWritingMode) -> f32 {
149
2
        self.line_over(wm) + self.line_under(wm)
150
2
    }
151

            
152
    // +spec:writing-modes:829cd7 - inline-start/end depend on writing-mode AND direction.
153
    // +spec:writing-modes:c0ae9c - flow-relative inline mappings derive from
154
    //   writing-mode + direction (RTL's inline-start = line-right).
155
    // +spec:writing-modes:14e6f0 - inline-start/end depend on direction as well as wm.
156
    /// Returns the flow-relative inline-start edge.
157
    ///
158
    /// The inline-start side is `line-left` (see [`Self::cross_start`]) when the
159
    /// inline base direction is LTR, and `line-right` (see [`Self::cross_end`])
160
    /// when it is RTL. This is the direction-aware counterpart that
161
    /// `cross_start`/`cross_end` (which are direction-blind line-left/line-right)
162
    /// cannot express on their own.
163
17
    #[must_use] pub const fn inline_start(&self, wm: LayoutWritingMode, dir: StyleDirection) -> f32 {
164
17
        match dir {
165
10
            StyleDirection::Ltr => self.cross_start(wm),
166
7
            StyleDirection::Rtl => self.cross_end(wm),
167
        }
168
17
    }
169

            
170
    /// Returns the flow-relative inline-end edge.
171
    ///
172
    /// The inline-end side is `line-right` when the inline base direction is
173
    /// LTR, and `line-left` when it is RTL.
174
15
    #[must_use] pub const fn inline_end(&self, wm: LayoutWritingMode, dir: StyleDirection) -> f32 {
175
15
        match dir {
176
9
            StyleDirection::Ltr => self.cross_end(wm),
177
6
            StyleDirection::Rtl => self.cross_start(wm),
178
        }
179
15
    }
180

            
181
    /// Returns the sum of the inline-start and inline-end edges.
182
    ///
183
    /// Direction-independent (start + end covers the whole inline axis), but
184
    /// takes `dir` for call-site symmetry with the individual accessors.
185
9
    #[must_use] pub fn inline_sum(&self, wm: LayoutWritingMode, dir: StyleDirection) -> f32 {
186
9
        self.inline_start(wm, dir) + self.inline_end(wm, dir)
187
9
    }
188
}
189

            
190
// ============================================================================
191
// UNRESOLVED VALUE TYPES (for lazy resolution during layout)
192
// ============================================================================
193

            
194
/// An unresolved CSS margin value.
195
// +spec:box-model:ff1730 - margin properties apply to both continuous and paged media
196
///
197
/// Margins can be `auto` (for centering) or a length value that needs
198
/// resolution against the containing block.
199
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
200
pub enum UnresolvedMargin {
201
    /// margin: 0 (default)
202
    #[default]
203
    Zero,
204
    /// margin: auto (for centering, CSS 2.2 § 10.3.3)
205
    Auto,
206
    /// A length value (px, %, em, vh, etc.)
207
    Length(PixelValue),
208
}
209

            
210
impl UnresolvedMargin {
211
    /// Returns true if this is an auto margin
212
887241
    #[must_use] pub const fn is_auto(&self) -> bool {
213
887241
        matches!(self, Self::Auto)
214
887241
    }
215

            
216
    /// Resolve this margin value to pixels.
217
    ///
218
    /// - `Auto` returns 0.0 (actual auto margin calculation happens in layout)
219
    /// - `Zero` returns 0.0
220
    /// - `Length` is resolved using the resolution context
221
    #[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
222
887244
    #[must_use] pub fn resolve(&self, ctx: &ResolutionContext) -> f32 {
223
887244
        match self {
224
115008
            Self::Zero => 0.0,
225
            // +spec:box-model:c921aa - auto margin-top/bottom used value is 0 for block-level non-replaced elements in normal flow
226
            // +spec:box-model:e25fdc - auto margins treated as zero for abspos size computation
227
76
            Self::Auto => 0.0, // Auto is handled separately in layout
228
772160
            Self::Length(pv) => pv.resolve_with_context(ctx, PropertyContext::Margin),
229
        }
230
887244
    }
231
}
232

            
233
/// Unresolved edge sizes for margin/padding/border.
234
///
235
/// This stores the raw CSS values before resolution, allowing us to
236
/// defer resolution until the containing block size is known.
237
#[derive(Debug, Clone, Copy, Default)]
238
pub struct UnresolvedEdge<T> {
239
    pub top: T,
240
    pub right: T,
241
    pub bottom: T,
242
    pub left: T,
243
}
244

            
245
impl<T> UnresolvedEdge<T> {
246
10
    pub const fn new(top: T, right: T, bottom: T, left: T) -> Self {
247
10
        Self { top, right, bottom, left }
248
10
    }
249
}
250

            
251
impl UnresolvedEdge<UnresolvedMargin> {
252
    /// Resolve all margin edges to pixel values.
253
221808
    #[must_use] pub fn resolve(&self, ctx: &ResolutionContext) -> EdgeSizes {
254
221808
        EdgeSizes {
255
221808
            top: self.top.resolve(ctx),
256
221808
            right: self.right.resolve(ctx),
257
221808
            bottom: self.bottom.resolve(ctx),
258
221808
            left: self.left.resolve(ctx),
259
221808
        }
260
221808
    }
261

            
262
    /// Extract which margins are set to `auto`.
263
221809
    #[must_use] pub const fn get_margin_auto(&self) -> MarginAuto {
264
221809
        MarginAuto {
265
221809
            top: self.top.is_auto(),
266
221809
            right: self.right.is_auto(),
267
221809
            bottom: self.bottom.is_auto(),
268
221809
            left: self.left.is_auto(),
269
221809
        }
270
221809
    }
271
}
272

            
273
impl UnresolvedEdge<PixelValue> {
274
    /// Resolve all edges to pixel values.
275
443617
    #[must_use] pub fn resolve(&self, ctx: &ResolutionContext, prop_ctx: PropertyContext) -> EdgeSizes {
276
443617
        EdgeSizes {
277
443617
            top: self.top.resolve_with_context(ctx, prop_ctx),
278
443617
            right: self.right.resolve_with_context(ctx, prop_ctx),
279
443617
            bottom: self.bottom.resolve_with_context(ctx, prop_ctx),
280
443617
            left: self.left.resolve_with_context(ctx, prop_ctx),
281
443617
        }
282
443617
    }
283
}
284

            
285
/// Parameters needed to resolve CSS values to pixels.
286
#[derive(Debug, Clone, Copy)]
287
pub struct ResolutionParams {
288
    // +spec:inline-formatting-context:26c933 - LogicalSize maps inline/block dimensions to physical width/height per writing mode
289
    /// The containing block size (for % resolution)
290
    pub containing_block: LogicalSize,
291
    /// The viewport size (for vh/vw resolution)
292
    pub viewport_size: LogicalSize,
293
    /// The element's computed font-size (for em resolution)
294
    pub element_font_size: f32,
295
    /// The root element's font-size (for rem resolution)
296
    pub root_font_size: f32,
297
}
298

            
299
impl ResolutionParams {
300
    /// Create a `ResolutionContext` from these parameters.
301
221821
    #[must_use] pub const fn to_resolution_context(&self) -> ResolutionContext {
302
221821
        ResolutionContext {
303
221821
            vertical_writing_mode: false,
304
221821
            element_font_size: self.element_font_size,
305
221821
            // For non-font properties, `em` resolves against the element's own
306
221821
            // computed font-size, so parent_font_size == element_font_size here.
307
221821
            // Do NOT use this context for font-size resolution itself.
308
221821
            parent_font_size: self.element_font_size,
309
221821
            root_font_size: self.root_font_size,
310
221821
            element_size: None,
311
221821
            containing_block_size: PhysicalSize::new(
312
221821
                self.containing_block.width,
313
221821
                self.containing_block.height,
314
221821
            ),
315
221821
            viewport_size: PhysicalSize::new(
316
221821
                self.viewport_size.width,
317
221821
                self.viewport_size.height,
318
221821
            ),
319
221821
        }
320
221821
    }
321
}
322

            
323
// ============================================================================
324
// UNRESOLVED BOX PROPS (new design)
325
// ============================================================================
326

            
327
/// Box properties with unresolved CSS values.
328
///
329
/// This stores the raw CSS values as parsed, deferring resolution until
330
/// layout time when the containing block size is known.
331
#[derive(Debug, Clone, Copy, Default)]
332
pub struct UnresolvedBoxProps {
333
    pub margin: UnresolvedEdge<UnresolvedMargin>,
334
    pub padding: UnresolvedEdge<PixelValue>,
335
    pub border: UnresolvedEdge<PixelValue>,
336
    /// css-writing-modes-4 §7.2: margin/padding percentages resolve against
337
    /// the containing block's INLINE size. Captured at collection time from
338
    /// the node's writing mode so late re-resolution (`resolve_box_props` with
339
    /// the real containing block) picks the right axis without re-reading
340
    /// the cascade.
341
    pub vertical_writing_mode: bool,
342
}
343

            
344
impl UnresolvedBoxProps {
345
    /// Resolve all box properties to pixel values.
346
221807
    #[must_use] pub fn resolve(&self, params: &ResolutionParams) -> ResolvedBoxProps {
347
221807
        let mut ctx = params.to_resolution_context();
348
221807
        ctx.vertical_writing_mode = self.vertical_writing_mode;
349
221807
        ResolvedBoxProps {
350
221807
            margin: self.margin.resolve(&ctx),
351
221807
            padding: self.padding.resolve(&ctx, PropertyContext::Padding),
352
221807
            border: self.border.resolve(&ctx, PropertyContext::BorderWidth),
353
221807
            margin_auto: self.margin.get_margin_auto(),
354
221807
        }
355
221807
    }
356
}
357

            
358
// ============================================================================
359
// RESOLVED BOX PROPS (legacy name: BoxProps)
360
// ============================================================================
361

            
362
/// Tracks which margins are set to `auto` (for centering calculations).
363
#[derive(Debug, Clone, Copy, Default)]
364
#[allow(clippy::struct_excessive_bools)] // one independent bool per margin edge (auto flags)
365
pub struct MarginAuto {
366
    pub left: bool,
367
    pub right: bool,
368
    pub top: bool,
369
    pub bottom: bool,
370
}
371

            
372
/// A fully resolved representation of a node's box model properties.
373
// +spec:box-model:3e083b - content/padding/border/margin box model layers
374
// +spec:box-model:a227ff - content/padding/border/margin edges defining box extents for overflow
375
// +spec:containing-block:bca691 - box model edges: padding/border/margin boxes with content-box, padding-box, margin-box methods
376
///
377
/// All values are in pixels. This is the result of resolving `UnresolvedBoxProps`
378
/// against a containing block.
379
#[derive(Debug, Clone, Copy, Default)]
380
pub struct ResolvedBoxProps {
381
    pub margin: EdgeSizes,
382
    pub padding: EdgeSizes,
383
    pub border: EdgeSizes,
384
    /// Tracks which margins are set to `auto`.
385
    /// CSS 2.2 § 10.3.3: If both margin-left and margin-right are auto,
386
    /// their used values are equal, centering the element within its container.
387
    pub margin_auto: MarginAuto,
388
}
389

            
390
impl ResolvedBoxProps {
391
    // +spec:box-model:be08c6 - inner size (content-box) from outer size minus border+padding, floored at zero
392
    // +spec:writing-modes:a58616 - abstract dimensions: inline size maps to physical width/height per writing-mode
393
    /// Calculates the inner content-box size from an outer border-box size,
394
    /// correctly accounting for the specified writing mode.
395
370405
    #[must_use] pub fn inner_size(&self, outer_size: LogicalSize, wm: LayoutWritingMode) -> LogicalSize {
396
370405
        let outer_main = outer_size.main(wm);
397
370405
        let outer_cross = outer_size.cross(wm);
398

            
399
        // The sum of padding and border along the cross (inline) axis.
400
370405
        let cross_axis_spacing = self.padding.cross_sum(wm) + self.border.cross_sum(wm);
401

            
402
        // The sum of padding and border along the main (block) axis.
403
370405
        let main_axis_spacing = self.padding.main_sum(wm) + self.border.main_sum(wm);
404

            
405
        // +spec:box-model:2589b1 - content size = border-box - border - padding, floored at zero
406
        // +spec:box-model:3ab53d - if padding+border > border-box, content floors at 0px
407
370405
        let inner_main = (outer_main - main_axis_spacing).max(0.0);
408
370405
        let inner_cross = (outer_cross - cross_axis_spacing).max(0.0);
409

            
410
370405
        LogicalSize::from_main_cross(inner_main, inner_cross, wm)
411
370405
    }
412

            
413
    // +spec:box-model:aa585e - Content/padding/border/margin edge relationships
414
    // +spec:height-calculation:6c9abb - box model edges: margin > border > padding > content
415
    /// Returns the content-box rect from a border-box rect.
416
    /// Shrinks inward by border + padding on each side.
417
    // +spec:box-model:1720a5 - content of a block box is confined to its content edges
418
10
    #[must_use] pub fn content_box(&self, border_box: LogicalRect) -> LogicalRect {
419
10
        let x = border_box.origin.x + self.border.left + self.padding.left;
420
10
        let y = border_box.origin.y + self.border.top + self.padding.top;
421
10
        let w = (border_box.size.width - self.border.horizontal_sum() - self.padding.horizontal_sum()).max(0.0);
422
10
        let h = (border_box.size.height - self.border.vertical_sum() - self.padding.vertical_sum()).max(0.0);
423
10
        LogicalRect { origin: LogicalPosition { x, y }, size: LogicalSize { width: w, height: h } }
424
10
    }
425

            
426
    /// Returns the padding-box rect from a border-box rect.
427
    /// Shrinks inward by border on each side.
428
10
    #[must_use] pub fn padding_box(&self, border_box: LogicalRect) -> LogicalRect {
429
10
        let x = border_box.origin.x + self.border.left;
430
10
        let y = border_box.origin.y + self.border.top;
431
10
        let w = (border_box.size.width - self.border.horizontal_sum()).max(0.0);
432
10
        let h = (border_box.size.height - self.border.vertical_sum()).max(0.0);
433
10
        LogicalRect { origin: LogicalPosition { x, y }, size: LogicalSize { width: w, height: h } }
434
10
    }
435

            
436
    /// Returns the margin-box rect from a border-box rect.
437
    /// Expands outward by margin on each side.
438
10
    #[must_use] pub fn margin_box(&self, border_box: LogicalRect) -> LogicalRect {
439
10
        let x = border_box.origin.x - self.margin.left;
440
10
        let y = border_box.origin.y - self.margin.top;
441
10
        let w = border_box.size.width + self.margin.horizontal_sum();
442
10
        let h = border_box.size.height + self.margin.vertical_sum();
443
10
        LogicalRect { origin: LogicalPosition { x, y }, size: LogicalSize { width: w, height: h } }
444
10
    }
445

            
446
    // +spec:box-model:0e75c1 - margin, padding, border contribute to layout bounds (default line-fit-edge: leading uses line-height model)
447
    /// Total horizontal space consumed by margin + border + padding.
448
8
    #[must_use] pub fn horizontal_mbp(&self) -> f32 {
449
8
        self.margin.horizontal_sum() + self.border.horizontal_sum() + self.padding.horizontal_sum()
450
8
    }
451

            
452
    /// Total vertical space consumed by margin + border + padding.
453
8
    #[must_use] pub fn vertical_mbp(&self) -> f32 {
454
8
        self.margin.vertical_sum() + self.border.vertical_sum() + self.padding.vertical_sum()
455
8
    }
456

            
457
    /// Total horizontal space consumed by border + padding only (no margin).
458
7
    #[must_use] pub fn horizontal_bp(&self) -> f32 {
459
7
        self.border.horizontal_sum() + self.padding.horizontal_sum()
460
7
    }
461

            
462
    /// Total vertical space consumed by border + padding only (no margin).
463
7
    #[must_use] pub fn vertical_bp(&self) -> f32 {
464
7
        self.border.vertical_sum() + self.padding.vertical_sum()
465
7
    }
466
}
467

            
468
/// Type alias for backwards compatibility.
469
/// TODO: Remove this once all code uses `ResolvedBoxProps` directly.
470
pub type BoxProps = ResolvedBoxProps;
471

            
472
/// Packed representation of box model properties using i16×10 encoding.
473
///
474
/// Stores margin/padding/border as i16 values scaled by 10 (0.1px precision),
475
/// reducing the hot struct from 52B to 26B. Range: ±3276.7px per edge.
476
///
477
/// Only used for storage in `LayoutNodeHot`. The layout solver unpacks to
478
/// `ResolvedBoxProps` (f32) for computation.
479
#[derive(Debug, Clone, Copy, Default)]
480
#[repr(C)]
481
pub struct PackedBoxProps {
482
    pub margin: [i16; 4],     // top, right, bottom, left — ×10
483
    pub padding: [i16; 4],    // ×10
484
    pub border: [i16; 4],     // ×10
485
    pub margin_auto: MarginAuto,
486
}
487

            
488
impl PackedBoxProps {
489
    /// Pack a `ResolvedBoxProps` into compact i16×10 encoding.
490
    #[inline]
491
263673
    #[must_use] pub fn pack(bp: &ResolvedBoxProps) -> Self {
492
263673
        Self {
493
263673
            margin: Self::pack_edge(&bp.margin),
494
263673
            padding: Self::pack_edge(&bp.padding),
495
263673
            border: Self::pack_edge(&bp.border),
496
263673
            margin_auto: bp.margin_auto,
497
263673
        }
498
263673
    }
499

            
500
    /// Unpack to full `ResolvedBoxProps` with f32 values.
501
    #[inline]
502
3637427
    #[must_use] pub fn unpack(&self) -> ResolvedBoxProps {
503
3637427
        ResolvedBoxProps {
504
3637427
            margin: Self::unpack_edge(&self.margin),
505
3637427
            padding: Self::unpack_edge(&self.padding),
506
3637427
            border: Self::unpack_edge(&self.border),
507
3637427
            margin_auto: self.margin_auto,
508
3637427
        }
509
3637427
    }
510

            
511
    /// Convenience: unpack and call `inner_size` on the result.
512
    #[inline]
513
115371
    #[must_use] pub fn inner_size(&self, outer_size: LogicalSize, wm: LayoutWritingMode) -> LogicalSize {
514
115371
        self.unpack().inner_size(outer_size, wm)
515
115371
    }
516

            
517
    /// Convenience: unpack and call `content_box` on the result.
518
    #[inline]
519
5
    #[must_use] pub fn content_box(&self, border_box: LogicalRect) -> LogicalRect {
520
5
        self.unpack().content_box(border_box)
521
5
    }
522

            
523
    /// Convenience: unpack and call `padding_box` on the result.
524
    #[inline]
525
5
    #[must_use] pub fn padding_box(&self, border_box: LogicalRect) -> LogicalRect {
526
5
        self.unpack().padding_box(border_box)
527
5
    }
528

            
529
    /// Convenience: unpack and call `margin_box` on the result.
530
    #[inline]
531
5
    #[must_use] pub fn margin_box(&self, border_box: LogicalRect) -> LogicalRect {
532
5
        self.unpack().margin_box(border_box)
533
5
    }
534

            
535
    /// Convenience: unpack and return horizontal MBP.
536
    #[inline]
537
2
    #[must_use] pub fn horizontal_mbp(&self) -> f32 {
538
2
        self.unpack().horizontal_mbp()
539
2
    }
540

            
541
    /// Convenience: unpack and return vertical MBP.
542
    #[inline]
543
2
    #[must_use] pub fn vertical_mbp(&self) -> f32 {
544
2
        self.unpack().vertical_mbp()
545
2
    }
546

            
547
    /// Convenience: unpack and return horizontal BP.
548
    #[inline]
549
2
    #[must_use] pub fn horizontal_bp(&self) -> f32 {
550
2
        self.unpack().horizontal_bp()
551
2
    }
552

            
553
    /// Convenience: unpack and return vertical BP.
554
    #[inline]
555
2
    #[must_use] pub fn vertical_bp(&self) -> f32 {
556
2
        self.unpack().vertical_bp()
557
2
    }
558

            
559
    #[inline]
560
    #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/counter/fixed-point cast
561
856561
    fn pack_edge(e: &EdgeSizes) -> [i16; 4] {
562
        const MIN: f32 = i16::MIN as f32;
563
        const MAX: f32 = i16::MAX as f32;
564
856561
        [
565
856561
            (e.top * 10.0).round().clamp(MIN, MAX) as i16,
566
856561
            (e.right * 10.0).round().clamp(MIN, MAX) as i16,
567
856561
            (e.bottom * 10.0).round().clamp(MIN, MAX) as i16,
568
856561
            (e.left * 10.0).round().clamp(MIN, MAX) as i16,
569
856561
        ]
570
856561
    }
571

            
572
    #[inline]
573
    #[allow(clippy::trivially_copy_pass_by_ref)] // <=8B Copy param kept by-ref intentionally (hot pixel/coord path or to avoid churning call sites for a perf-neutral change)
574
10977819
    fn unpack_edge(e: &[i16; 4]) -> EdgeSizes {
575
10977819
        EdgeSizes {
576
10977819
            top: f32::from(e[0]) * 0.1,
577
10977819
            right: f32::from(e[1]) * 0.1,
578
10977819
            bottom: f32::from(e[2]) * 0.1,
579
10977819
            left: f32::from(e[3]) * 0.1,
580
10977819
        }
581
10977819
    }
582
}
583

            
584
// Re-export float and clear types from azul_css
585
pub use azul_css::props::layout::{LayoutClear, LayoutFloat};
586

            
587
// +spec:intrinsic-sizing:af39b6 - min-content, max-content, and stretch fit size definitions
588
// min-content constraint, max-content constraint definitions
589
// and fit-content sizes for both inline and block axes
590
// +spec:height-calculation:e9ec84 - replaced elements have natural dimensions (width, height, ratio)
591
/// Represents the intrinsic sizing information for an element, calculated
592
/// without knowledge of the final containing block size.
593
// +spec:intrinsic-sizing:127a10 - min-content, max-content, fit-content size definitions (css-sizing-3 §2.1)
594
// +spec:intrinsic-sizing:21f2cb - defines min-content, max-content, and stretch-fit size terminology
595
// +spec:width-calculation:1583c4 - min-content, max-content, fit-content intrinsic size definitions (§2.1)
596
#[derive(Debug, Clone, Copy, Default)]
597
pub struct IntrinsicSizes {
598
    // +spec:width-calculation:b83d0a - min-content width ("preferred minimum width" in CSS2.1§10.3.5)
599
    // +spec:writing-modes:1583c4 - min-content size in inline axis = size fitting contents with all soft wraps taken
600
    /// §2.1 min-content inline size: inline size fitting contents if all soft wraps taken.
601
    pub min_content_width: f32,
602
    // +spec:width-calculation:0c74d3 - max-content width ("preferred width" in CSS2.1§10.3.5)
603
    // +spec:writing-modes:6e85d3 - max-content inline size is the "ideal" size in the inline axis (writing-mode-dependent)
604
    /// §2.1 max-content inline size: narrowest inline size if no soft wraps taken.
605
    pub max_content_width: f32,
606
    /// The width specified by CSS properties, if any.
607
    pub preferred_width: Option<f32>,
608
    /// §2.1 min-content block size: for block containers, tables, and inline boxes,
609
    /// equivalent to max-content block size.
610
    pub min_content_height: f32,
611
    // +spec:writing-modes:8c94e2 - max-content block size is the "ideal" block size after layout
612
    /// §2.1 max-content block size: "ideal" block size, usually content height after layout.
613
    pub max_content_height: f32,
614
    /// The height specified by CSS properties, if any.
615
    pub preferred_height: Option<f32>,
616
    // +spec:intrinsic-sizing:af39b6 - natural (intrinsic) aspect ratio of a replaced element
617
    // +spec:height-calculation:e9ec84 - replaced-element natural width/height/ratio
618
    /// The element's natural aspect ratio (inline / block), if it has one — e.g. a
619
    /// replaced element with intrinsic dimensions. `None` for non-replaced content.
620
    pub preferred_aspect_ratio: Option<f32>,
621
}
622

            
623
impl IntrinsicSizes {
624
    // +spec:intrinsic-sizing:127a10 - fit-content = clamp(min-content, stretch-fit, max-content)
625
    // +spec:intrinsic-sizing:21f2cb - stretch-fit size drawn from available (containing-block) space
626
    /// CSS Sizing §2.1 fit-content **inline** size:
627
    /// `clamp(min-content, stretch-fit, max-content)`, where the stretch-fit size is the
628
    /// `available_inline_size` (the space the containing block offers in the inline axis).
629
    /// Equal to `max(min_content, min(stretch_fit, max_content))`.
630
    #[must_use]
631
3
    pub const fn fit_content_width(&self, available_inline_size: f32) -> f32 {
632
3
        available_inline_size
633
3
            .min(self.max_content_width)
634
3
            .max(self.min_content_width)
635
3
    }
636

            
637
    /// CSS Sizing §2.1 fit-content **block** size:
638
    /// `clamp(min-content, stretch-fit, max-content)` in the block axis.
639
    #[must_use]
640
3
    pub const fn fit_content_height(&self, available_block_size: f32) -> f32 {
641
3
        available_block_size
642
3
            .min(self.max_content_height)
643
3
            .max(self.min_content_height)
644
3
    }
645
}
646

            
647
// ============================================================================
648
// WRITING MODE SUPPORT
649
// ============================================================================
650

            
651
/// Captures the resolved writing mode context for a node.
652
///
653
/// This struct bundles together all the CSS properties that affect how
654
/// logical directions (inline/block) map to physical directions (x/y).
655
/// Spec agents should use this struct to implement writing-mode-aware layout.
656
///
657
/// # CSS Writing Modes Level 4
658
///
659
/// - `writing-mode` determines the block flow direction and inline base direction
660
/// - `direction` determines the inline base direction (ltr or rtl)
661
/// - `text-orientation` determines glyph orientation in vertical writing modes
662
// +spec:block-formatting-context:333dcb - typographic mode captured by text_orientation field
663
// +spec:block-formatting-context:66eb6d - text-orientation property (mixed|upright|sideways) integrated into WritingModeContext
664
// +spec:block-formatting-context:8be1b0 - writing modes and vertical text orientation context (UTN#22)
665
// +spec:display-property:0a39dc - text-orientation affects inline-level alignment via WritingModeContext
666
// +spec:display-property:591355 - bidirectionality support via direction property in WritingModeContext
667
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
668
pub struct WritingModeContext {
669
    pub writing_mode: LayoutWritingMode,
670
    pub direction: StyleDirection,
671
    // +spec:block-formatting-context:925cfe - text-orientation mixed/upright for horizontal scripts in vertical mode
672
    pub text_orientation: StyleTextOrientation,
673
}
674

            
675
impl Default for WritingModeContext {
676
270015
    fn default() -> Self {
677
270015
        Self {
678
270015
            writing_mode: LayoutWritingMode::HorizontalTb,
679
270015
            direction: StyleDirection::Ltr,
680
270015
            text_orientation: StyleTextOrientation::Mixed,
681
270015
        }
682
270015
    }
683
}
684

            
685
impl WritingModeContext {
686
    /// Constructs a `WritingModeContext`, applying spec-mandated overrides.
687
    // +spec:writing-modes:8307e4 - text-orientation: upright forces used direction to ltr
688
81716
    #[must_use] pub fn new(
689
81716
        writing_mode: LayoutWritingMode,
690
81716
        direction: StyleDirection,
691
81716
        text_orientation: StyleTextOrientation,
692
81716
    ) -> Self {
693
        // CSS Writing Modes Level 4 §5.1: text-orientation: upright causes
694
        // the used value of direction to be ltr, and all characters to be
695
        // treated as strong LTR for bidi reordering purposes.
696
81716
        let used_direction = if text_orientation == StyleTextOrientation::Upright {
697
13
            StyleDirection::Ltr
698
        } else {
699
81703
            direction
700
        };
701
81716
        Self {
702
81716
            writing_mode,
703
81716
            direction: used_direction,
704
81716
            text_orientation,
705
81716
        }
706
81716
    }
707

            
708
    // +spec:writing-modes:458d31 - text-orientation:upright forces used direction to ltr
709
    /// Returns the used value of `direction`.
710
    ///
711
    /// The upright override is already applied in `new()`, so this just
712
    /// returns the stored direction.
713
14
    #[must_use] pub const fn used_direction(&self) -> StyleDirection {
714
14
        self.direction
715
14
    }
716

            
717
    // +spec:containing-block:c205e5 - orthogonal flow: child writing mode perpendicular to containing block's
718

            
719
    // +spec:block-formatting-context:6225cb - vertical writing modes: line-over is right, line-under is left
720
    // +spec:block-formatting-context:9a4269 - vertical vs horizontal script classification
721
    /// Returns true if the writing mode is horizontal (`HorizontalTb`).
722
    ///
723
    /// When true, the inline axis is horizontal and the block axis is vertical.
724
42388
    #[must_use] pub const fn is_horizontal(&self) -> bool {
725
42388
        matches!(self.writing_mode, LayoutWritingMode::HorizontalTb)
726
42388
    }
727

            
728
    /// Returns true if the inline size corresponds to the physical width.
729
    ///
730
    /// In horizontal writing modes, inline size = width.
731
    /// In vertical writing modes, inline size = height.
732
    // +spec:block-formatting-context:bb9845 - orthogonal flows: inline/block axis mapping
733
13
    #[must_use] pub const fn inline_size_is_width(&self) -> bool {
734
13
        self.is_horizontal()
735
13
    }
736

            
737
    /// Returns true if the block size corresponds to the physical height.
738
    ///
739
    /// In horizontal writing modes, block size = height.
740
    /// In vertical writing modes, block size = width.
741
13
    #[must_use] pub const fn block_size_is_height(&self) -> bool {
742
13
        self.is_horizontal()
743
13
    }
744

            
745
    // +spec:writing-modes:32541a - direction property controls inline text direction via stylesheet
746
    /// Returns true if the inline direction is reversed (RTL in horizontal,
747
    /// or bottom-to-top in certain vertical modes).
748
10
    #[must_use] pub fn is_inline_reversed(&self) -> bool {
749
10
        self.used_direction() == StyleDirection::Rtl
750
10
    }
751
}
752

            
753
#[cfg(test)]
754
#[allow(clippy::float_cmp, clippy::unreadable_literal)]
755
mod autotest_generated {
756
    use super::*;
757

            
758
    // ---------------------------------------------------------------- helpers
759

            
760
    /// All writing modes, so every mapping test can be run exhaustively.
761
    const ALL_WM: [LayoutWritingMode; 3] = [
762
        LayoutWritingMode::HorizontalTb,
763
        LayoutWritingMode::VerticalRl,
764
        LayoutWritingMode::VerticalLr,
765
    ];
766

            
767
    fn edges(top: f32, right: f32, bottom: f32, left: f32) -> EdgeSizes {
768
        EdgeSizes { top, right, bottom, left }
769
    }
770

            
771
    fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
772
        LogicalRect {
773
            origin: LogicalPosition { x, y },
774
            size: LogicalSize { width: w, height: h },
775
        }
776
    }
777

            
778
    /// `EdgeSizes`/`ResolvedBoxProps` carry no `PartialEq`, and the packed
779
    /// encoding is lossy by design, so float comparisons need a tolerance.
780
    fn close(a: f32, b: f32, eps: f32) -> bool {
781
        (a - b).abs() <= eps
782
    }
783

            
784
    fn params(cb: LogicalSize, vp: LogicalSize, font: f32, root: f32) -> ResolutionParams {
785
        ResolutionParams {
786
            containing_block: cb,
787
            viewport_size: vp,
788
            element_font_size: font,
789
            root_font_size: root,
790
        }
791
    }
792

            
793
    /// 800x600 containing block, 1000x500 viewport, 16px element / 10px root font.
794
    /// Every dimension is distinct so a transposed axis cannot pass by accident.
795
    fn distinct_params() -> ResolutionParams {
796
        params(
797
            LogicalSize::new(800.0, 600.0),
798
            LogicalSize::new(1000.0, 500.0),
799
            16.0,
800
            10.0,
801
        )
802
    }
803

            
804
    fn props(margin: EdgeSizes, padding: EdgeSizes, border: EdgeSizes) -> ResolvedBoxProps {
805
        ResolvedBoxProps { margin, padding, border, margin_auto: MarginAuto::default() }
806
    }
807

            
808
    // ================================================================
809
    // EdgeSizes: sums
810
    // ================================================================
811

            
812
    #[test]
813
    fn edge_sizes_sums_pick_the_right_pair_of_edges() {
814
        let e = edges(1.0, 2.0, 4.0, 8.0); // top, right, bottom, left
815
        assert_eq!(e.horizontal_sum(), 10.0, "horizontal = left + right");
816
        assert_eq!(e.vertical_sum(), 5.0, "vertical = top + bottom");
817
    }
818

            
819
    #[test]
820
    fn edge_sizes_default_is_all_zero() {
821
        let e = EdgeSizes::default();
822
        assert_eq!(e.horizontal_sum(), 0.0);
823
        assert_eq!(e.vertical_sum(), 0.0);
824
        for wm in ALL_WM {
825
            assert_eq!(e.main_sum(wm), 0.0);
826
            assert_eq!(e.cross_sum(wm), 0.0);
827
        }
828
    }
829

            
830
    #[test]
831
    fn edge_sizes_sums_saturate_to_infinity_instead_of_panicking() {
832
        // f32::MAX + f32::MAX overflows to +inf; it must not panic or wrap.
833
        let e = edges(f32::MAX, f32::MAX, f32::MAX, f32::MAX);
834
        assert!(e.horizontal_sum().is_infinite() && e.horizontal_sum() > 0.0);
835
        assert!(e.vertical_sum().is_infinite() && e.vertical_sum() > 0.0);
836
    }
837

            
838
    #[test]
839
    fn edge_sizes_opposing_infinities_produce_nan_not_a_panic() {
840
        // inf + (-inf) is NaN by IEEE-754. The point is that it is deterministic
841
        // and does not trap — nothing downstream may assume a finite sum.
842
        let e = edges(f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
843
        assert!(e.horizontal_sum().is_nan());
844
        assert!(e.vertical_sum().is_nan());
845
    }
846

            
847
    #[test]
848
    fn edge_sizes_nan_edges_propagate_without_panicking() {
849
        let e = edges(f32::NAN, 1.0, 2.0, 3.0);
850
        assert!(e.vertical_sum().is_nan());
851
        assert_eq!(e.horizontal_sum(), 4.0, "a NaN top must not poison left+right");
852
        for wm in ALL_WM {
853
            let _ = e.main_sum(wm);
854
            let _ = e.cross_sum(wm);
855
        }
856
    }
857

            
858
    // ================================================================
859
    // EdgeSizes: writing-mode axis mapping
860
    // ================================================================
861

            
862
    #[test]
863
    fn edge_sizes_axis_mapping_is_exact_for_every_writing_mode() {
864
        let e = edges(1.0, 2.0, 4.0, 8.0); // top, right, bottom, left
865

            
866
        // horizontal-tb: block (main) axis is vertical, inline (cross) axis is horizontal.
867
        let h = LayoutWritingMode::HorizontalTb;
868
        assert_eq!(e.main_start(h), 1.0, "main-start = top");
869
        assert_eq!(e.main_end(h), 4.0, "main-end = bottom");
870
        assert_eq!(e.cross_start(h), 8.0, "cross-start = left");
871
        assert_eq!(e.cross_end(h), 2.0, "cross-end = right");
872

            
873
        // vertical-*: block (main) axis is horizontal, inline (cross) axis is vertical.
874
        for wm in [LayoutWritingMode::VerticalRl, LayoutWritingMode::VerticalLr] {
875
            assert_eq!(e.main_start(wm), 8.0, "main-start = left");
876
            assert_eq!(e.main_end(wm), 2.0, "main-end = right");
877
            assert_eq!(e.cross_start(wm), 1.0, "cross-start = top");
878
            assert_eq!(e.cross_end(wm), 4.0, "cross-end = bottom");
879
        }
880
    }
881

            
882
    // +spec:block-formatting-context:a49f9e - line-over/line-under accessors.
883
    #[test]
884
    fn edge_sizes_line_over_under_is_line_relative_not_physical() {
885
        let e = edges(1.0, 2.0, 4.0, 8.0); // top, right, bottom, left
886

            
887
        // horizontal-tb: over = top, under = bottom (coincides with main_start/end).
888
        let h = LayoutWritingMode::HorizontalTb;
889
        assert_eq!(e.line_over(h), 1.0, "line-over = top in horizontal-tb");
890
        assert_eq!(e.line_under(h), 4.0, "line-under = bottom in horizontal-tb");
891
        assert_eq!(e.line_over(h), e.main_start(h), "over == main-start only in horizontal-tb");
892

            
893
        // vertical modes: over = right, under = left (CSS Writing Modes L4 §6.3).
894
        // Crucially this differs from main_start/main_end (left/right), proving the
895
        // line-relative accessor is not just an alias of the physical block axis.
896
        for wm in [LayoutWritingMode::VerticalRl, LayoutWritingMode::VerticalLr] {
897
            assert_eq!(e.line_over(wm), 2.0, "line-over = right in vertical modes");
898
            assert_eq!(e.line_under(wm), 8.0, "line-under = left in vertical modes");
899
            assert_ne!(e.line_over(wm), e.main_start(wm), "over != main-start in vertical modes");
900
        }
901

            
902
        // Sum picks the line-relative pair, independent of writing mode orientation.
903
        assert_eq!(e.line_over_under_sum(h), 5.0);
904
        assert_eq!(e.line_over_under_sum(LayoutWritingMode::VerticalRl), 10.0);
905
    }
906

            
907
    // +spec:writing-modes:829cd7 / c0ae9c - direction-aware inline-start/end accessors.
908
    #[test]
909
    fn edge_sizes_inline_start_end_depend_on_direction() {
910
        let e = edges(1.0, 2.0, 4.0, 8.0); // top, right, bottom, left
911

            
912
        for wm in ALL_WM {
913
            // LTR: inline-start = line-left = cross_start; inline-end = line-right = cross_end.
914
            assert_eq!(
915
                e.inline_start(wm, StyleDirection::Ltr),
916
                e.cross_start(wm),
917
                "LTR inline-start = cross-start (line-left)"
918
            );
919
            assert_eq!(
920
                e.inline_end(wm, StyleDirection::Ltr),
921
                e.cross_end(wm),
922
                "LTR inline-end = cross-end (line-right)"
923
            );
924

            
925
            // RTL: the two ends swap — inline-start moves to line-right.
926
            assert_eq!(
927
                e.inline_start(wm, StyleDirection::Rtl),
928
                e.cross_end(wm),
929
                "RTL inline-start = cross-end (line-right)"
930
            );
931
            assert_eq!(
932
                e.inline_end(wm, StyleDirection::Rtl),
933
                e.cross_start(wm),
934
                "RTL inline-end = cross-start (line-left)"
935
            );
936

            
937
            // The sum covers the whole inline axis regardless of direction.
938
            assert_eq!(
939
                e.inline_sum(wm, StyleDirection::Ltr),
940
                e.inline_sum(wm, StyleDirection::Rtl),
941
                "inline-sum is direction-independent"
942
            );
943
            assert_eq!(e.inline_sum(wm, StyleDirection::Ltr), e.cross_sum(wm));
944
        }
945

            
946
        // Concrete horizontal-tb values: LTR start=left(8), RTL start=right(2).
947
        let h = LayoutWritingMode::HorizontalTb;
948
        assert_eq!(e.inline_start(h, StyleDirection::Ltr), 8.0);
949
        assert_eq!(e.inline_start(h, StyleDirection::Rtl), 2.0);
950
    }
951

            
952
    #[test]
953
    fn edge_sizes_main_and_cross_sums_partition_the_four_edges() {
954
        // Whatever the writing mode, main+cross must account for each edge exactly
955
        // once — a transposition bug in one arm would break this identity.
956
        let e = edges(1.0, 2.0, 4.0, 8.0);
957
        for wm in ALL_WM {
958
            assert_eq!(e.main_start(wm) + e.main_end(wm), e.main_sum(wm));
959
            assert_eq!(e.cross_start(wm) + e.cross_end(wm), e.cross_sum(wm));
960
            assert_eq!(
961
                e.main_sum(wm) + e.cross_sum(wm),
962
                e.horizontal_sum() + e.vertical_sum(),
963
                "{wm:?}: main+cross must cover all four edges"
964
            );
965
        }
966
    }
967

            
968
    #[test]
969
    fn edge_sizes_axis_accessors_survive_extreme_values() {
970
        let e = edges(f32::MAX, f32::MIN, f32::INFINITY, f32::NEG_INFINITY);
971
        for wm in ALL_WM {
972
            let _ = e.main_start(wm);
973
            let _ = e.main_end(wm);
974
            let _ = e.cross_start(wm);
975
            let _ = e.cross_end(wm);
976
            let _ = e.main_sum(wm);
977
            let _ = e.cross_sum(wm);
978
        }
979
    }
980

            
981
    // ================================================================
982
    // UnresolvedMargin
983
    // ================================================================
984

            
985
    #[test]
986
    fn unresolved_margin_is_auto_only_for_the_auto_variant() {
987
        assert!(UnresolvedMargin::Auto.is_auto());
988
        assert!(!UnresolvedMargin::Zero.is_auto());
989
        assert!(!UnresolvedMargin::Length(PixelValue::const_px(10)).is_auto());
990
        // A zero-valued length is still *not* `auto`.
991
        assert!(!UnresolvedMargin::Length(PixelValue::zero()).is_auto());
992
        assert!(!UnresolvedMargin::default().is_auto(), "default is Zero, not Auto");
993
    }
994

            
995
    #[test]
996
    fn unresolved_margin_auto_and_zero_both_resolve_to_zero_px() {
997
        let ctx = distinct_params().to_resolution_context();
998
        assert_eq!(UnresolvedMargin::Zero.resolve(&ctx), 0.0);
999
        // `auto` is resolved later by the layout algorithm; here it must read as 0.
        assert_eq!(UnresolvedMargin::Auto.resolve(&ctx), 0.0);
    }
    #[test]
    fn unresolved_margin_length_resolves_by_metric() {
        let ctx = distinct_params().to_resolution_context(); // 800x600 cb, 16px font, 10px root
        let r = |pv: PixelValue| UnresolvedMargin::Length(pv).resolve(&ctx);
        assert_eq!(r(PixelValue::px(12.5)), 12.5);
        assert_eq!(r(PixelValue::em(2.0)), 32.0, "em = 2 x element font-size");
        assert_eq!(r(PixelValue::rem(2.0)), 20.0, "rem = 2 x root font-size");
    }
    #[test]
    fn unresolved_margin_percent_resolves_against_containing_block_width() {
        // CSS 2.1 §8.3: percentage margins ALWAYS refer to the containing block
        // *width* — never the height. With a 800x600 block, 50% must be 400, not 300.
        let ctx = distinct_params().to_resolution_context();
        let got = UnresolvedMargin::Length(PixelValue::percent(50.0)).resolve(&ctx);
        assert_eq!(got, 400.0);
        assert_ne!(got, 300.0, "percentage margin must not use the block height");
    }
    #[test]
    fn unresolved_margin_nan_length_resolves_to_zero_not_nan() {
        // PixelValue stores a fixed-point isize, and `f32 as isize` saturates NaN
        // to 0 — so a NaN can never escape into layout as a NaN margin.
        let ctx = distinct_params().to_resolution_context();
        let got = UnresolvedMargin::Length(PixelValue::px(f32::NAN)).resolve(&ctx);
        assert!(!got.is_nan(), "a NaN px value must not survive resolution");
        assert_eq!(got, 0.0);
    }
    #[test]
    fn unresolved_margin_huge_length_saturates_to_a_finite_value() {
        let ctx = distinct_params().to_resolution_context();
        for v in [f32::MAX, f32::MIN, f32::INFINITY, f32::NEG_INFINITY] {
            let got = UnresolvedMargin::Length(PixelValue::px(v)).resolve(&ctx);
            assert!(got.is_finite(), "px({v}) resolved to a non-finite {got}");
        }
    }
    #[test]
    fn unresolved_margin_percent_of_a_nan_containing_block_is_nan_not_a_panic() {
        // The containing block is a raw f32 and is NOT sanitized, so a NaN block
        // size *does* propagate. Documenting the real behaviour: deterministic NaN,
        // no panic — the guard has to live at the caller, not here.
        let p = params(
            LogicalSize::new(f32::NAN, f32::NAN),
            LogicalSize::new(1000.0, 500.0),
            16.0,
            10.0,
        );
        let got = UnresolvedMargin::Length(PixelValue::percent(50.0)).resolve(&p.to_resolution_context());
        assert!(got.is_nan());
    }
    // ================================================================
    // UnresolvedEdge
    // ================================================================
    #[test]
    fn unresolved_edge_new_assigns_fields_in_top_right_bottom_left_order() {
        // The classic CSS shorthand transposition bug: argument order is TRBL.
        let e = UnresolvedEdge::new(1_u8, 2, 4, 8);
        assert_eq!(e.top, 1);
        assert_eq!(e.right, 2);
        assert_eq!(e.bottom, 4);
        assert_eq!(e.left, 8);
    }
    #[test]
    fn unresolved_edge_new_accepts_extreme_payloads() {
        let e = UnresolvedEdge::new(f32::NAN, f32::INFINITY, f32::MAX, f32::MIN);
        assert!(e.top.is_nan());
        assert!(e.right.is_infinite());
        assert_eq!(e.bottom, f32::MAX);
        assert_eq!(e.left, f32::MIN);
    }
    #[test]
    fn get_margin_auto_flags_exactly_the_auto_sides() {
        let e = UnresolvedEdge::new(
            UnresolvedMargin::Zero,                                 // top
            UnresolvedMargin::Auto,                                 // right
            UnresolvedMargin::Length(PixelValue::const_px(5)),      // bottom
            UnresolvedMargin::Auto,                                 // left
        );
        let a = e.get_margin_auto();
        assert!(!a.top);
        assert!(a.right);
        assert!(!a.bottom);
        assert!(a.left);
    }
    #[test]
    fn get_margin_auto_on_default_edge_flags_nothing() {
        let a = UnresolvedEdge::<UnresolvedMargin>::default().get_margin_auto();
        assert!(!a.top && !a.right && !a.bottom && !a.left);
    }
    #[test]
    fn unresolved_margin_edge_resolve_keeps_each_side_separate() {
        let ctx = distinct_params().to_resolution_context();
        let e = UnresolvedEdge::new(
            UnresolvedMargin::Length(PixelValue::px(1.0)),   // top
            UnresolvedMargin::Length(PixelValue::px(2.0)),   // right
            UnresolvedMargin::Auto,                          // bottom -> 0
            UnresolvedMargin::Length(PixelValue::px(8.0)),   // left
        );
        let r = e.resolve(&ctx);
        assert_eq!(r.top, 1.0);
        assert_eq!(r.right, 2.0);
        assert_eq!(r.bottom, 0.0, "auto resolves to 0 px here");
        assert_eq!(r.left, 8.0);
    }
    #[test]
    fn pixel_edge_resolve_drops_percentages_on_border_width() {
        // CSS Backgrounds 3 §4.1: `%` is not a valid border-width. The resolver
        // must yield 0 rather than silently resolving against the containing block.
        let ctx = distinct_params().to_resolution_context();
        let e = UnresolvedEdge::new(
            PixelValue::percent(50.0),
            PixelValue::percent(50.0),
            PixelValue::percent(50.0),
            PixelValue::percent(50.0),
        );
        let r = e.resolve(&ctx, PropertyContext::BorderWidth);
        assert_eq!(r.top, 0.0);
        assert_eq!(r.right, 0.0);
        assert_eq!(r.bottom, 0.0);
        assert_eq!(r.left, 0.0);
    }
    #[test]
    fn pixel_edge_resolve_uses_block_width_for_vertical_padding_percentages() {
        // CSS 2.1 §8.4: padding-top/bottom percentages also refer to the containing
        // block WIDTH. With 800x600, every side must land on 80, never 60.
        let ctx = distinct_params().to_resolution_context();
        let e = UnresolvedEdge::new(
            PixelValue::percent(10.0),
            PixelValue::percent(10.0),
            PixelValue::percent(10.0),
            PixelValue::percent(10.0),
        );
        let r = e.resolve(&ctx, PropertyContext::Padding);
        assert_eq!(r.top, 80.0, "padding-top % must use the width");
        assert_eq!(r.bottom, 80.0, "padding-bottom % must use the width");
        assert_eq!(r.left, 80.0);
        assert_eq!(r.right, 80.0);
    }
    #[test]
    fn pixel_edge_resolve_of_viewport_units_uses_the_viewport_not_the_block() {
        let ctx = distinct_params().to_resolution_context(); // viewport 1000x500
        let e = UnresolvedEdge::new(
            PixelValue::from_metric(SizeMetric::Vw, 10.0),
            PixelValue::from_metric(SizeMetric::Vh, 10.0),
            PixelValue::from_metric(SizeMetric::Vmin, 10.0),
            PixelValue::from_metric(SizeMetric::Vmax, 10.0),
        );
        let r = e.resolve(&ctx, PropertyContext::Padding);
        assert_eq!(r.top, 100.0, "10vw of 1000");
        assert_eq!(r.right, 50.0, "10vh of 500");
        assert_eq!(r.bottom, 50.0, "10vmin of min(1000,500)");
        assert_eq!(r.left, 100.0, "10vmax of max(1000,500)");
    }
    // ================================================================
    // ResolutionParams
    // ================================================================
    #[test]
    fn to_resolution_context_maps_every_field() {
        let ctx = distinct_params().to_resolution_context();
        assert_eq!(ctx.element_font_size, 16.0);
        assert_eq!(ctx.root_font_size, 10.0);
        assert_eq!(ctx.containing_block_size.width, 800.0);
        assert_eq!(ctx.containing_block_size.height, 600.0);
        assert_eq!(ctx.viewport_size.width, 1000.0);
        assert_eq!(ctx.viewport_size.height, 500.0);
        assert!(ctx.element_size.is_none(), "element size is unknown pre-layout");
    }
    #[test]
    fn to_resolution_context_aliases_parent_font_size_onto_the_element_font_size() {
        // Documented quirk: for non-font properties `em` resolves against the
        // element's OWN font-size, so the context deliberately reports
        // parent == element. Using it for font-size resolution would be wrong.
        let ctx = distinct_params().to_resolution_context();
        assert_eq!(ctx.parent_font_size, ctx.element_font_size);
        assert_eq!(ctx.parent_font_size, 16.0);
    }
    #[test]
    fn to_resolution_context_passes_extreme_values_through_unchanged() {
        let p = params(
            LogicalSize::new(f32::MAX, f32::NAN),
            LogicalSize::new(f32::INFINITY, 0.0),
            0.0,
            f32::MIN,
        );
        let ctx = p.to_resolution_context();
        assert_eq!(ctx.containing_block_size.width, f32::MAX);
        assert!(ctx.containing_block_size.height.is_nan());
        assert!(ctx.viewport_size.width.is_infinite());
        assert_eq!(ctx.element_font_size, 0.0);
        assert_eq!(ctx.root_font_size, f32::MIN);
    }
    #[test]
    fn zero_font_size_context_resolves_em_to_zero_without_dividing_by_it() {
        let p = params(LogicalSize::zero(), LogicalSize::zero(), 0.0, 0.0);
        let ctx = p.to_resolution_context();
        assert_eq!(PixelValue::em(10.0).resolve_with_context(&ctx, PropertyContext::Margin), 0.0);
        assert_eq!(PixelValue::rem(10.0).resolve_with_context(&ctx, PropertyContext::Margin), 0.0);
        // A zero-sized viewport must not turn vmin/vmax into NaN.
        let vmin = PixelValue::from_metric(SizeMetric::Vmin, 50.0);
        assert_eq!(vmin.resolve_with_context(&ctx, PropertyContext::Padding), 0.0);
    }
    // ================================================================
    // UnresolvedBoxProps
    // ================================================================
    #[test]
    fn unresolved_box_props_default_resolves_to_an_all_zero_box() {
        let r = UnresolvedBoxProps::default().resolve(&distinct_params());
        assert_eq!(r.horizontal_mbp(), 0.0);
        assert_eq!(r.vertical_mbp(), 0.0);
        assert!(!r.margin_auto.left && !r.margin_auto.right);
    }
    #[test]
    fn unresolved_box_props_resolve_applies_the_right_property_context_per_edge() {
        let p = distinct_params(); // 800x600 block
        let b = UnresolvedBoxProps {
            vertical_writing_mode: false,
            margin: UnresolvedEdge::new(
                UnresolvedMargin::Auto,
                UnresolvedMargin::Length(PixelValue::percent(10.0)), // -> 80 (width)
                UnresolvedMargin::Zero,
                UnresolvedMargin::Auto,
            ),
            padding: UnresolvedEdge::new(
                PixelValue::percent(10.0), // -> 80 (width, even on the top edge)
                PixelValue::px(4.0),
                PixelValue::px(4.0),
                PixelValue::px(4.0),
            ),
            border: UnresolvedEdge::new(
                PixelValue::percent(10.0), // -> 0 (percent is invalid on border-width)
                PixelValue::px(2.0),
                PixelValue::px(2.0),
                PixelValue::px(2.0),
            ),
        };
        let r = b.resolve(&p);
        assert_eq!(r.margin.top, 0.0, "auto margin resolves to 0 px");
        assert_eq!(r.margin.right, 80.0);
        assert_eq!(r.padding.top, 80.0);
        assert_eq!(r.border.top, 0.0, "percent border-width must collapse to 0");
        assert_eq!(r.border.left, 2.0);
        // The auto flags must survive resolution — they are what drives centering.
        assert!(r.margin_auto.top);
        assert!(r.margin_auto.left);
        assert!(!r.margin_auto.right);
        assert!(!r.margin_auto.bottom);
    }
    // ================================================================
    // ResolvedBoxProps::inner_size
    // ================================================================
    #[test]
    fn inner_size_of_a_zero_box_is_zero() {
        let bp = ResolvedBoxProps::default();
        for wm in ALL_WM {
            let s = bp.inner_size(LogicalSize::zero(), wm);
            assert_eq!(s.width, 0.0);
            assert_eq!(s.height, 0.0);
        }
    }
    #[test]
    fn inner_size_subtracts_border_and_padding_but_not_margin() {
        let bp = props(
            edges(100.0, 100.0, 100.0, 100.0), // margin — must be ignored
            edges(1.0, 2.0, 4.0, 8.0),         // padding
            edges(10.0, 20.0, 30.0, 40.0),     // border
        );
        // width  loses left+right: (8+2) + (40+20) = 70
        // height loses top+bottom: (1+4) + (10+30) = 45
        let s = bp.inner_size(LogicalSize::new(200.0, 100.0), LayoutWritingMode::HorizontalTb);
        assert_eq!(s.width, 130.0);
        assert_eq!(s.height, 55.0);
    }
    #[test]
    fn inner_size_is_identical_in_every_writing_mode() {
        // Physically, content-box = border-box minus the same four edges regardless
        // of writing mode. The main/cross indirection must cancel out exactly; a
        // transposed arm in one mode would show up right here.
        let bp = props(
            EdgeSizes::default(),
            edges(1.0, 2.0, 4.0, 8.0),
            edges(10.0, 20.0, 30.0, 40.0),
        );
        let outer = LogicalSize::new(200.0, 100.0);
        let base = bp.inner_size(outer, LayoutWritingMode::HorizontalTb);
        for wm in ALL_WM {
            let s = bp.inner_size(outer, wm);
            assert_eq!(s.width, base.width, "{wm:?} width diverged");
            assert_eq!(s.height, base.height, "{wm:?} height diverged");
        }
    }
    #[test]
    fn inner_size_floors_at_zero_when_border_and_padding_exceed_the_box() {
        // CSS: if padding+border overflow the border-box, content is 0 — never negative.
        let bp = props(
            EdgeSizes::default(),
            edges(100.0, 100.0, 100.0, 100.0),
            edges(100.0, 100.0, 100.0, 100.0),
        );
        for wm in ALL_WM {
            let s = bp.inner_size(LogicalSize::new(10.0, 10.0), wm);
            assert_eq!(s.width, 0.0, "{wm:?}");
            assert_eq!(s.height, 0.0, "{wm:?}");
            assert!(s.width >= 0.0 && s.height >= 0.0);
        }
    }
    #[test]
    fn inner_size_never_returns_nan() {
        // `.max(0.0)` discards a NaN operand, so NaN can only ever collapse to 0.
        let nan_bp = props(EdgeSizes::default(), edges(f32::NAN, 0.0, 0.0, 0.0), EdgeSizes::default());
        let cases = [
            (ResolvedBoxProps::default(), LogicalSize::new(f32::NAN, f32::NAN)),
            (nan_bp, LogicalSize::new(100.0, 100.0)),
            (
                // inf - inf = NaN, which must still floor to 0 rather than escape.
                props(EdgeSizes::default(), edges(f32::INFINITY, 0.0, f32::INFINITY, 0.0), EdgeSizes::default()),
                LogicalSize::new(f32::INFINITY, f32::INFINITY),
            ),
        ];
        for (bp, outer) in cases {
            for wm in ALL_WM {
                let s = bp.inner_size(outer, wm);
                assert!(!s.width.is_nan(), "{wm:?}: NaN width escaped inner_size");
                assert!(!s.height.is_nan(), "{wm:?}: NaN height escaped inner_size");
                assert!(s.width >= 0.0 && s.height >= 0.0);
            }
        }
    }
    #[test]
    fn inner_size_at_f32_max_stays_finite_and_non_negative() {
        let bp = props(EdgeSizes::default(), edges(1.0, 2.0, 4.0, 8.0), edges(1.0, 1.0, 1.0, 1.0));
        for wm in ALL_WM {
            let s = bp.inner_size(LogicalSize::new(f32::MAX, f32::MAX), wm);
            assert!(s.width.is_finite() && s.height.is_finite());
            assert!(s.width > 0.0 && s.height > 0.0);
        }
    }
    #[test]
    fn inner_size_with_negative_outer_size_floors_to_zero() {
        let bp = ResolvedBoxProps::default();
        for wm in ALL_WM {
            let s = bp.inner_size(LogicalSize::new(-100.0, -50.0), wm);
            assert_eq!(s.width, 0.0, "{wm:?}");
            assert_eq!(s.height, 0.0, "{wm:?}");
        }
    }
    #[test]
    fn inner_size_with_negative_padding_grows_the_content_box() {
        // Negative border/padding is not reachable from CSS, but the struct permits
        // it — assert the arithmetic is plain subtraction rather than something that
        // silently clamps mid-way.
        let bp = props(EdgeSizes::default(), edges(-5.0, -5.0, -5.0, -5.0), EdgeSizes::default());
        let s = bp.inner_size(LogicalSize::new(100.0, 100.0), LayoutWritingMode::HorizontalTb);
        assert_eq!(s.width, 110.0);
        assert_eq!(s.height, 110.0);
    }
    // ================================================================
    // ResolvedBoxProps: box rects
    // ================================================================
    #[test]
    fn content_box_shrinks_by_border_plus_padding() {
        let bp = props(
            edges(100.0, 100.0, 100.0, 100.0), // margin is irrelevant here
            edges(1.0, 2.0, 4.0, 8.0),         // padding TRBL
            edges(10.0, 20.0, 30.0, 40.0),     // border TRBL
        );
        let got = bp.content_box(rect(1000.0, 2000.0, 200.0, 100.0));
        // origin moves in by border+padding on the start edges: left 40+8, top 10+1
        // size shrinks by both sides: width 200-(40+20)-(8+2)=130, height 100-(10+30)-(1+4)=55
        assert_eq!(got, rect(1048.0, 2011.0, 130.0, 55.0));
    }
    #[test]
    fn padding_box_shrinks_by_border_only() {
        let bp = props(
            EdgeSizes::default(),
            edges(1.0, 2.0, 4.0, 8.0),
            edges(10.0, 20.0, 30.0, 40.0),
        );
        let got = bp.padding_box(rect(1000.0, 2000.0, 200.0, 100.0));
        assert_eq!(got, rect(1040.0, 2010.0, 140.0, 60.0));
    }
    #[test]
    fn margin_box_expands_by_margin_only() {
        let bp = props(
            edges(1.0, 2.0, 4.0, 8.0), // margin TRBL
            edges(99.0, 99.0, 99.0, 99.0),
            edges(99.0, 99.0, 99.0, 99.0),
        );
        let got = bp.margin_box(rect(1000.0, 2000.0, 200.0, 100.0));
        // origin moves OUT by left/top margin; size grows by both sides.
        assert_eq!(got, rect(992.0, 1999.0, 210.0, 105.0));
    }
    #[test]
    fn box_rects_nest_content_inside_padding_inside_border_inside_margin() {
        let bp = props(
            edges(5.0, 6.0, 7.0, 8.0),
            edges(1.0, 2.0, 3.0, 4.0),
            edges(9.0, 10.0, 11.0, 12.0),
        );
        let border_box = rect(50.0, 60.0, 400.0, 300.0);
        let content = bp.content_box(border_box);
        let padding = bp.padding_box(border_box);
        let margin = bp.margin_box(border_box);
        // Each layer must sit strictly inside the next one out.
        assert!(margin.origin.x <= border_box.origin.x);
        assert!(border_box.origin.x <= padding.origin.x);
        assert!(padding.origin.x <= content.origin.x);
        assert!(margin.origin.y <= border_box.origin.y);
        assert!(border_box.origin.y <= padding.origin.y);
        assert!(padding.origin.y <= content.origin.y);
        assert!(content.size.width <= padding.size.width);
        assert!(padding.size.width <= border_box.size.width);
        assert!(border_box.size.width <= margin.size.width);
        assert!(content.size.height <= padding.size.height);
        assert!(padding.size.height <= border_box.size.height);
        assert!(border_box.size.height <= margin.size.height);
    }
    #[test]
    fn content_box_size_floors_at_zero_but_the_origin_still_moves_in() {
        let bp = props(
            EdgeSizes::default(),
            edges(500.0, 500.0, 500.0, 500.0),
            edges(500.0, 500.0, 500.0, 500.0),
        );
        let got = bp.content_box(rect(0.0, 0.0, 10.0, 10.0));
        assert_eq!(got.size.width, 0.0, "size must clamp, not go negative");
        assert_eq!(got.size.height, 0.0);
        assert_eq!(got.origin.x, 1000.0, "origin is not clamped");
        assert_eq!(got.origin.y, 1000.0);
    }
    #[test]
    fn padding_box_size_floors_at_zero_for_an_oversized_border() {
        let bp = props(EdgeSizes::default(), EdgeSizes::default(), edges(99.0, 99.0, 99.0, 99.0));
        let got = bp.padding_box(rect(0.0, 0.0, 10.0, 10.0));
        assert_eq!(got.size.width, 0.0);
        assert_eq!(got.size.height, 0.0);
    }
    #[test]
    fn margin_box_with_negative_margins_can_shrink_below_zero() {
        // Unlike content_box/padding_box, margin_box does NOT floor at 0 — negative
        // margins are legal CSS and the box genuinely inverts. Pinning the real
        // behaviour so a later "helpful" clamp cannot land unnoticed.
        let bp = props(edges(-10.0, -10.0, -10.0, -10.0), EdgeSizes::default(), EdgeSizes::default());
        let got = bp.margin_box(rect(0.0, 0.0, 10.0, 10.0));
        assert_eq!(got.size.width, -10.0);
        assert_eq!(got.size.height, -10.0);
        assert_eq!(got.origin.x, 10.0, "a negative left margin pushes the origin right");
        assert_eq!(got.origin.y, 10.0);
    }
    #[test]
    fn box_rects_do_not_panic_on_nan_or_infinite_geometry() {
        let bp = props(
            edges(f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX),
            edges(f32::NAN, f32::MAX, 0.0, f32::MIN),
            edges(f32::INFINITY, 0.0, f32::NAN, 1.0),
        );
        let r = rect(f32::NAN, f32::INFINITY, f32::MAX, f32::MIN);
        let c = bp.content_box(r);
        let p = bp.padding_box(r);
        let m = bp.margin_box(r);
        // The clamped sizes are the only guaranteed invariant: never negative, never NaN.
        for s in [c.size, p.size] {
            assert!(!s.width.is_nan() && !s.height.is_nan());
            assert!(s.width >= 0.0 && s.height >= 0.0);
        }
        let _ = m;
    }
    // ================================================================
    // ResolvedBoxProps: mbp / bp getters
    // ================================================================
    #[test]
    fn mbp_and_bp_getters_sum_the_expected_layers() {
        let bp = props(
            edges(1.0, 2.0, 4.0, 8.0),      // margin: h=10, v=5
            edges(10.0, 20.0, 40.0, 80.0),  // padding: h=100, v=50
            edges(100.0, 200.0, 400.0, 800.0), // border: h=1000, v=500
        );
        assert_eq!(bp.horizontal_bp(), 1100.0);
        assert_eq!(bp.vertical_bp(), 550.0);
        assert_eq!(bp.horizontal_mbp(), 1110.0);
        assert_eq!(bp.vertical_mbp(), 555.0);
        // mbp is exactly bp plus the margins — the two must never drift apart.
        assert_eq!(bp.horizontal_mbp(), bp.horizontal_bp() + bp.margin.horizontal_sum());
        assert_eq!(bp.vertical_mbp(), bp.vertical_bp() + bp.margin.vertical_sum());
    }
    #[test]
    fn mbp_and_bp_getters_are_zero_on_a_default_box() {
        let bp = ResolvedBoxProps::default();
        assert_eq!(bp.horizontal_mbp(), 0.0);
        assert_eq!(bp.vertical_mbp(), 0.0);
        assert_eq!(bp.horizontal_bp(), 0.0);
        assert_eq!(bp.vertical_bp(), 0.0);
    }
    #[test]
    fn mbp_getters_do_not_panic_on_extreme_boxes() {
        let bp = props(
            edges(f32::MAX, f32::MAX, f32::MAX, f32::MAX),
            edges(f32::NAN, 0.0, 0.0, 0.0),
            edges(f32::NEG_INFINITY, 0.0, 0.0, 0.0),
        );
        let _ = bp.horizontal_mbp();
        let _ = bp.vertical_mbp();
        let _ = bp.horizontal_bp();
        let _ = bp.vertical_bp();
    }
    // ================================================================
    // PackedBoxProps: encoding
    // ================================================================
    #[test]
    fn pack_edge_encodes_tenths_of_a_pixel() {
        let e = edges(0.0, 1.0, 2.5, 3276.7);
        let p = PackedBoxProps::pack_edge(&e);
        assert_eq!(p[0], 0);
        assert_eq!(p[1], 10);
        assert_eq!(p[2], 25);
        assert_eq!(p[3], 32767, "the documented +3276.7px maximum");
    }
    #[test]
    fn pack_edge_rounds_to_the_nearest_tenth_rather_than_truncating() {
        let p = PackedBoxProps::pack_edge(&edges(1.04, 1.06, -1.04, -1.06));
        assert_eq!(p[0], 10, "1.04 rounds down");
        assert_eq!(p[1], 11, "1.06 rounds up — truncation would give 10");
        assert_eq!(p[2], -10);
        assert_eq!(p[3], -11);
    }
    #[test]
    fn pack_edge_saturates_out_of_range_values_instead_of_wrapping() {
        // Wrapping would turn a huge positive margin into a huge NEGATIVE one —
        // the single nastiest failure mode of this encoding.
        let p = PackedBoxProps::pack_edge(&edges(10_000.0, -10_000.0, 1e30, -1e30));
        assert_eq!(p[0], i16::MAX);
        assert_eq!(p[1], i16::MIN);
        assert_eq!(p[2], i16::MAX);
        assert_eq!(p[3], i16::MIN);
        assert!(p.iter().all(|v| *v == i16::MAX || *v == i16::MIN));
    }
    #[test]
    fn pack_edge_clamps_infinities_to_the_i16_bounds() {
        let p = PackedBoxProps::pack_edge(&edges(f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN));
        assert_eq!(p[0], i16::MAX);
        assert_eq!(p[1], i16::MIN);
        assert_eq!(p[2], i16::MAX);
        assert_eq!(p[3], i16::MIN);
    }
    #[test]
    fn pack_edge_maps_nan_to_zero_without_panicking() {
        // `f32::clamp` passes NaN through (it only panics on a NaN *bound*), and the
        // subsequent `as i16` saturates NaN to 0. So a NaN edge encodes as 0px.
        let p = PackedBoxProps::pack_edge(&edges(f32::NAN, f32::NAN, f32::NAN, f32::NAN));
        assert_eq!(p, [0, 0, 0, 0]);
    }
    #[test]
    fn pack_edge_maps_negative_zero_to_zero() {
        let p = PackedBoxProps::pack_edge(&edges(-0.0, -0.0, -0.0, -0.0));
        assert_eq!(p, [0, 0, 0, 0]);
    }
    #[test]
    fn unpack_edge_decodes_tenths_and_preserves_the_trbl_order() {
        let e = PackedBoxProps::unpack_edge(&[10, 25, -10, 32767]);
        assert!(close(e.top, 1.0, 1e-4), "top was {}", e.top);
        assert!(close(e.right, 2.5, 1e-4), "right was {}", e.right);
        assert!(close(e.bottom, -1.0, 1e-4), "bottom was {}", e.bottom);
        assert!(close(e.left, 3276.7, 1e-2), "left was {}", e.left);
    }
    #[test]
    fn unpack_edge_at_the_i16_extremes_stays_finite() {
        let e = PackedBoxProps::unpack_edge(&[i16::MAX, i16::MIN, 0, 1]);
        assert!(e.top.is_finite() && e.right.is_finite());
        assert!(close(e.top, 3276.7, 1e-2));
        assert!(close(e.right, -3276.8, 1e-2));
        assert_eq!(e.bottom, 0.0);
        assert!(close(e.left, 0.1, 1e-6));
    }
    // ================================================================
    // PackedBoxProps: round-trip
    // ================================================================
    #[test]
    fn every_i16_encoding_survives_unpack_then_pack_unchanged() {
        // Exhaustive decode->encode identity over the WHOLE i16 domain: if the
        // f32 round-off in `unpack_edge` ever drifted by more than half a tenth,
        // `pack_edge` would land on a neighbouring code and this would catch it.
        for n in i16::MIN..=i16::MAX {
            let encoded = [n; 4];
            let round_tripped = PackedBoxProps::pack_edge(&PackedBoxProps::unpack_edge(&encoded));
            assert_eq!(round_tripped, encoded, "i16 code {n} did not round-trip");
        }
    }
    #[test]
    fn pack_then_unpack_preserves_values_to_a_tenth_of_a_pixel() {
        let bp = props(
            edges(1.0, 2.5, 0.1, 12.3),
            edges(0.0, 100.25, 3276.7, -3276.8),
            edges(0.5, 0.05, 7.0, 0.0),
        );
        let out = PackedBoxProps::pack(&bp).unpack();
        for (got, want) in [
            (out.margin.top, bp.margin.top),
            (out.margin.right, bp.margin.right),
            (out.margin.bottom, bp.margin.bottom),
            (out.margin.left, bp.margin.left),
            (out.padding.top, bp.padding.top),
            (out.padding.right, bp.padding.right),
            (out.padding.bottom, bp.padding.bottom),
            (out.padding.left, bp.padding.left),
            (out.border.top, bp.border.top),
            (out.border.right, bp.border.right),
            (out.border.bottom, bp.border.bottom),
            (out.border.left, bp.border.left),
        ] {
            // Half a quantum (0.05) plus a little float slack at the 3276.x extreme.
            assert!(close(got, want, 0.051), "{want} round-tripped to {got}");
        }
    }
    #[test]
    fn pack_carries_the_margin_auto_flags_through_verbatim() {
        // margin_auto is NOT part of the lossy i16 encoding, so it must come back bit-exact.
        let bp = ResolvedBoxProps {
            margin_auto: MarginAuto { left: true, right: false, top: true, bottom: false },
            ..ResolvedBoxProps::default()
        };
        let out = PackedBoxProps::pack(&bp).unpack();
        assert!(out.margin_auto.left);
        assert!(!out.margin_auto.right);
        assert!(out.margin_auto.top);
        assert!(!out.margin_auto.bottom);
    }
    #[test]
    fn pack_keeps_the_three_edge_groups_apart() {
        // A copy-paste slip in `pack` would splice margin into padding or border.
        let bp = props(
            edges(1.0, 1.0, 1.0, 1.0),
            edges(2.0, 2.0, 2.0, 2.0),
            edges(3.0, 3.0, 3.0, 3.0),
        );
        let p = PackedBoxProps::pack(&bp);
        assert_eq!(p.margin, [10; 4]);
        assert_eq!(p.padding, [20; 4]);
        assert_eq!(p.border, [30; 4]);
    }
    #[test]
    fn pack_of_an_out_of_range_box_clamps_rather_than_flipping_sign() {
        let bp = props(
            edges(5000.0, 5000.0, 5000.0, 5000.0), // beyond +3276.7
            EdgeSizes::default(),
            EdgeSizes::default(),
        );
        let out = PackedBoxProps::pack(&bp).unpack();
        assert!(out.margin.top > 0.0, "a huge margin must not decode as negative");
        assert!(close(out.margin.top, 3276.7, 1e-2));
    }
    #[test]
    fn packed_default_is_an_all_zero_box() {
        let p = PackedBoxProps::default();
        assert_eq!(p.margin, [0; 4]);
        assert_eq!(p.horizontal_mbp(), 0.0);
        assert_eq!(p.vertical_mbp(), 0.0);
        assert_eq!(p.horizontal_bp(), 0.0);
        assert_eq!(p.vertical_bp(), 0.0);
        let s = p.inner_size(LogicalSize::new(10.0, 10.0), LayoutWritingMode::HorizontalTb);
        assert_eq!(s.width, 10.0);
        assert_eq!(s.height, 10.0);
    }
    // ================================================================
    // PackedBoxProps: convenience methods must agree with unpack()
    // ================================================================
    #[test]
    fn packed_convenience_methods_match_the_unpacked_equivalents() {
        let bp = props(
            edges(1.0, 2.5, 4.0, 8.5),
            edges(0.5, 1.5, 2.5, 3.5),
            edges(2.0, 4.0, 6.0, 8.0),
        );
        let packed = PackedBoxProps::pack(&bp);
        let unpacked = packed.unpack();
        let r = rect(10.0, 20.0, 300.0, 200.0);
        assert_eq!(packed.content_box(r), unpacked.content_box(r));
        assert_eq!(packed.padding_box(r), unpacked.padding_box(r));
        assert_eq!(packed.margin_box(r), unpacked.margin_box(r));
        assert_eq!(packed.horizontal_mbp(), unpacked.horizontal_mbp());
        assert_eq!(packed.vertical_mbp(), unpacked.vertical_mbp());
        assert_eq!(packed.horizontal_bp(), unpacked.horizontal_bp());
        assert_eq!(packed.vertical_bp(), unpacked.vertical_bp());
        for wm in ALL_WM {
            let a = packed.inner_size(r.size, wm);
            let b = unpacked.inner_size(r.size, wm);
            assert_eq!(a.width, b.width, "{wm:?}");
            assert_eq!(a.height, b.height, "{wm:?}");
        }
    }
    #[test]
    fn packed_inner_size_floors_at_zero_for_a_tiny_box() {
        let bp = props(
            EdgeSizes::default(),
            edges(50.0, 50.0, 50.0, 50.0),
            edges(50.0, 50.0, 50.0, 50.0),
        );
        let packed = PackedBoxProps::pack(&bp);
        for wm in ALL_WM {
            let s = packed.inner_size(LogicalSize::new(1.0, 1.0), wm);
            assert_eq!(s.width, 0.0, "{wm:?}");
            assert_eq!(s.height, 0.0, "{wm:?}");
        }
    }
    #[test]
    fn packed_box_rects_do_not_panic_on_extreme_input_rects() {
        let packed = PackedBoxProps::pack(&props(
            edges(3276.7, 3276.7, 3276.7, 3276.7),
            edges(3276.7, 3276.7, 3276.7, 3276.7),
            edges(3276.7, 3276.7, 3276.7, 3276.7),
        ));
        for r in [
            rect(0.0, 0.0, 0.0, 0.0),
            rect(f32::MAX, f32::MIN, f32::MAX, f32::MAX),
            rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
            rect(-1e30, -1e30, -1e30, -1e30),
        ] {
            let c = packed.content_box(r);
            let p = packed.padding_box(r);
            let _ = packed.margin_box(r);
            assert!(c.size.width >= 0.0 && !c.size.width.is_nan());
            assert!(p.size.height >= 0.0 && !p.size.height.is_nan());
        }
    }
    // ================================================================
    // WritingModeContext
    // ================================================================
    #[test]
    fn writing_mode_context_new_stores_what_it_was_given() {
        let c = WritingModeContext::new(
            LayoutWritingMode::VerticalRl,
            StyleDirection::Rtl,
            StyleTextOrientation::Mixed,
        );
        assert_eq!(c.writing_mode, LayoutWritingMode::VerticalRl);
        assert_eq!(c.direction, StyleDirection::Rtl);
        assert_eq!(c.text_orientation, StyleTextOrientation::Mixed);
    }
    #[test]
    fn text_orientation_upright_forces_the_used_direction_to_ltr() {
        // CSS Writing Modes 4 §5.1: `text-orientation: upright` makes the USED value
        // of `direction` ltr, even when the author wrote `direction: rtl`.
        let c = WritingModeContext::new(
            LayoutWritingMode::VerticalRl,
            StyleDirection::Rtl,
            StyleTextOrientation::Upright,
        );
        assert_eq!(c.used_direction(), StyleDirection::Ltr);
        assert!(!c.is_inline_reversed(), "upright must cancel the RTL reversal");
    }
    #[test]
    fn only_upright_overrides_the_direction() {
        for orientation in [StyleTextOrientation::Mixed, StyleTextOrientation::Sideways] {
            let c = WritingModeContext::new(
                LayoutWritingMode::VerticalRl,
                StyleDirection::Rtl,
                orientation,
            );
            assert_eq!(
                c.used_direction(),
                StyleDirection::Rtl,
                "{orientation:?} must not touch the direction"
            );
            assert!(c.is_inline_reversed());
        }
    }
    #[test]
    fn writing_mode_context_new_is_idempotent() {
        // Re-feeding a context's own fields back into `new()` must be a fixed point,
        // otherwise the upright override would compound across re-resolutions.
        for wm in ALL_WM {
            for dir in [StyleDirection::Ltr, StyleDirection::Rtl] {
                for or in [
                    StyleTextOrientation::Mixed,
                    StyleTextOrientation::Upright,
                    StyleTextOrientation::Sideways,
                ] {
                    let once = WritingModeContext::new(wm, dir, or);
                    let twice = WritingModeContext::new(
                        once.writing_mode,
                        once.direction,
                        once.text_orientation,
                    );
                    assert_eq!(once, twice, "{wm:?}/{dir:?}/{or:?} is not a fixed point");
                }
            }
        }
    }
    #[test]
    fn is_horizontal_is_true_only_for_horizontal_tb() {
        let mk = |wm| WritingModeContext::new(wm, StyleDirection::Ltr, StyleTextOrientation::Mixed);
        assert!(mk(LayoutWritingMode::HorizontalTb).is_horizontal());
        assert!(!mk(LayoutWritingMode::VerticalRl).is_horizontal());
        assert!(!mk(LayoutWritingMode::VerticalLr).is_horizontal());
    }
    #[test]
    fn inline_and_block_axis_predicates_track_is_horizontal() {
        // In horizontal-tb, inline size is the width and block size is the height;
        // both flip together in vertical modes. They may never disagree.
        for wm in ALL_WM {
            for dir in [StyleDirection::Ltr, StyleDirection::Rtl] {
                let c = WritingModeContext::new(wm, dir, StyleTextOrientation::Mixed);
                assert_eq!(c.inline_size_is_width(), c.is_horizontal(), "{wm:?}");
                assert_eq!(c.block_size_is_height(), c.is_horizontal(), "{wm:?}");
                assert_eq!(
                    c.inline_size_is_width(),
                    c.block_size_is_height(),
                    "{wm:?}: the two axes must flip together"
                );
            }
        }
    }
    #[test]
    fn is_inline_reversed_follows_the_used_direction_not_the_writing_mode() {
        for wm in ALL_WM {
            let ltr = WritingModeContext::new(wm, StyleDirection::Ltr, StyleTextOrientation::Mixed);
            let rtl = WritingModeContext::new(wm, StyleDirection::Rtl, StyleTextOrientation::Mixed);
            assert!(!ltr.is_inline_reversed(), "{wm:?} ltr");
            assert!(rtl.is_inline_reversed(), "{wm:?} rtl");
        }
    }
    #[test]
    fn default_writing_mode_context_is_horizontal_ltr_mixed() {
        let c = WritingModeContext::default();
        assert_eq!(c.writing_mode, LayoutWritingMode::HorizontalTb);
        assert_eq!(c.used_direction(), StyleDirection::Ltr);
        assert_eq!(c.text_orientation, StyleTextOrientation::Mixed);
        assert!(c.is_horizontal());
        assert!(c.inline_size_is_width());
        assert!(c.block_size_is_height());
        assert!(!c.is_inline_reversed());
    }
    #[test]
    fn default_context_matches_new_with_the_same_arguments() {
        let built = WritingModeContext::new(
            LayoutWritingMode::HorizontalTb,
            StyleDirection::Ltr,
            StyleTextOrientation::Mixed,
        );
        assert_eq!(WritingModeContext::default(), built);
    }
    #[test]
    fn fit_content_clamps_stretch_fit_between_min_and_max_content() {
        let is = IntrinsicSizes {
            min_content_width: 30.0,
            max_content_width: 100.0,
            min_content_height: 10.0,
            max_content_height: 40.0,
            ..Default::default()
        };
        // stretch-fit within [min, max] → returned as-is.
        assert!((is.fit_content_width(60.0) - 60.0).abs() < f32::EPSILON);
        assert!((is.fit_content_height(25.0) - 25.0).abs() < f32::EPSILON);
        // stretch-fit above max-content → clamped to max-content.
        assert!((is.fit_content_width(500.0) - 100.0).abs() < f32::EPSILON);
        assert!((is.fit_content_height(500.0) - 40.0).abs() < f32::EPSILON);
        // stretch-fit below min-content → clamped up to min-content (min wins over max).
        assert!((is.fit_content_width(0.0) - 30.0).abs() < f32::EPSILON);
        assert!((is.fit_content_height(0.0) - 10.0).abs() < f32::EPSILON);
    }
    #[test]
    fn intrinsic_sizes_default_has_no_preferred_aspect_ratio() {
        assert_eq!(IntrinsicSizes::default().preferred_aspect_ratio, None);
    }
}