1
//! CSS length and pixel value types, parsing, and unit resolution.
2
//!
3
//! Defines `PixelValue` (a numeric value + CSS unit like px, em, rem, %),
4
//! `ResolutionContext` (contextual information for resolving relative units),
5
//! and `PropertyContext` (which property is being resolved, affecting % and em semantics).
6
//!
7
//! **Resolution paths:**
8
//! - `resolve_with_context()` — the correct method for new code; properly distinguishes em vs rem,
9
//!   and resolves % based on property type per the CSS spec.
10
//! - `to_pixels_internal()` — legacy fallback used by `prop_cache.rs`; does not distinguish rem
11
//!   from em. Marked `#[doc(hidden)]`.
12

            
13
use core::fmt;
14
use std::num::ParseFloatError;
15

            
16
use crate::{
17
    corety::{AzString, OptionF32},
18
    props::{
19
        basic::{error::ParseFloatErrorWithInput, FloatValue, SizeMetric},
20
        formatter::FormatAsCssValue,
21
    },
22
};
23

            
24
/// Default font size in pixels (16px), matching the CSS "medium" keyword
25
/// and all major browser defaults (CSS 2.1 §15.7).
26
pub const DEFAULT_FONT_SIZE: f32 = 16.0;
27

            
28
/// Conversion factor from points to pixels (1pt = 1/72 inch, 1in = 96px, therefore 1pt = 96/72 px)
29
pub const PT_TO_PX: f32 = 96.0 / 72.0;
30

            
31
/// A normalized percentage value (0.0 = 0%, 1.0 = 100%)
32
///
33
/// This type prevents double-division bugs by making it explicit that the value
34
/// is already normalized to the 0.0-1.0 range. When you have a `NormalizedPercentage`,
35
/// you should multiply it directly with the containing block size, NOT divide by 100 again.
36
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
37
#[repr(transparent)]
38
pub struct NormalizedPercentage(f32);
39

            
40
impl NormalizedPercentage {
41
    /// Create a new percentage value from a normalized float (0.0-1.0)
42
    ///
43
    /// # Arguments
44
    /// * `value` - A normalized percentage where 0.0 = 0% and 1.0 = 100%
45
    #[inline]
46
    #[must_use]
47
13
    pub const fn new(value: f32) -> Self {
48
13
        Self(value)
49
13
    }
50

            
51
    /// Create a percentage from an unnormalized value (0-100 scale)
52
    ///
53
    /// This divides by 100 internally, so you should use this when converting
54
    /// from CSS percentage syntax like "50%" which is stored as 50.0.
55
    #[inline]
56
    #[must_use]
57
25062
    pub fn from_unnormalized(value: f32) -> Self {
58
25062
        Self(value / 100.0)
59
25062
    }
60

            
61
    /// Get the raw normalized value (0.0-1.0)
62
    #[inline]
63
    #[must_use]
64
10704
    pub const fn get(self) -> f32 {
65
10704
        self.0
66
10704
    }
67

            
68
    /// Resolve this percentage against a containing block size
69
    ///
70
    /// This multiplies the normalized percentage by the containing block size.
71
    /// For example, 50% (0.5) of 640px = 320px.
72
    #[inline]
73
    #[must_use]
74
13229
    pub fn resolve(self, containing_block_size: f32) -> f32 {
75
13229
        self.0 * containing_block_size
76
13229
    }
77
}
78

            
79
impl fmt::Display for NormalizedPercentage {
80
4
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81
4
        write!(f, "{}%", self.0 * 100.0)
82
4
    }
83
}
84

            
85
/// Logical size in CSS logical coordinate system
86
#[derive(Debug, Copy, Clone, PartialEq)]
87
#[repr(C)]
88
pub struct CssLogicalSize {
89
    /// Inline-axis size (width in horizontal writing mode)
90
    pub inline_size: f32,
91
    /// Block-axis size (height in horizontal writing mode)
92
    pub block_size: f32,
93
}
94

            
95
impl CssLogicalSize {
96
    #[inline]
97
    #[must_use]
98
3
    pub const fn new(inline_size: f32, block_size: f32) -> Self {
99
3
        Self {
100
3
            inline_size,
101
3
            block_size,
102
3
        }
103
3
    }
104

            
105
    /// Convert to physical size (width, height) in horizontal writing mode
106
    #[inline]
107
    #[must_use]
108
4
    pub const fn to_physical(self) -> PhysicalSize {
109
4
        PhysicalSize {
110
4
            width: self.inline_size,
111
4
            height: self.block_size,
112
4
        }
113
4
    }
114
}
115

            
116
/// Physical size (always width x height, regardless of writing mode)
117
#[derive(Debug, Copy, Clone, PartialEq)]
118
#[repr(C)]
119
pub struct PhysicalSize {
120
    pub width: f32,
121
    pub height: f32,
122
}
123

            
124
impl PhysicalSize {
125
    #[inline]
126
    #[must_use]
127
2549270
    pub const fn new(width: f32, height: f32) -> Self {
128
2549270
        Self { width, height }
129
2549270
    }
130

            
131
    /// Convert to logical size in horizontal writing mode
132
    #[inline]
133
    #[must_use]
134
6
    pub const fn to_logical(self) -> CssLogicalSize {
135
6
        CssLogicalSize {
136
6
            inline_size: self.width,
137
6
            block_size: self.height,
138
6
        }
139
6
    }
140
}
141

            
142
/// Context information needed to properly resolve CSS units (em, rem, %) to pixels.
143
///
144
/// This struct contains all the contextual information that `PixelValue::resolve()`
145
/// needs to correctly convert relative units according to the CSS specification:
146
///
147
/// - **em** units: For most properties, em refers to the element's own computed font-size. For the
148
///   font-size property itself, em refers to the parent's computed font-size.
149
///
150
/// - **rem** units: Always refer to the root element's computed font-size.
151
///
152
/// - **%** units: Percentage resolution depends on the property:
153
///   - Width/height: relative to containing block dimensions
154
///   - Margin/padding: relative to containing block width (even top/bottom!)
155
///   - Border-radius: relative to element's own border box dimensions
156
///   - Font-size: relative to parent's font-size
157
#[derive(Debug, Copy, Clone)]
158
pub struct ResolutionContext {
159
    /// The computed font-size of the current element (for em in non-font properties)
160
    pub element_font_size: f32,
161

            
162
    /// The computed font-size of the parent element (for em in font-size property)
163
    pub parent_font_size: f32,
164

            
165
    /// The computed font-size of the root element (for rem units)
166
    pub root_font_size: f32,
167

            
168
    /// The containing block dimensions (for % in width/height/margins/padding)
169
    pub containing_block_size: PhysicalSize,
170

            
171
    /// The element's own border box size (for % in border-radius, transforms)
172
    /// May be None during first layout pass before size is determined
173
    pub element_size: Option<PhysicalSize>,
174

            
175
    /// Is the element in a VERTICAL writing mode (`vertical-rl`/`vertical-lr`)?
176
    /// css-writing-modes-4 §7.2: margin/padding percentages resolve against
177
    /// the containing block's INLINE size - the physical HEIGHT in vertical
178
    /// modes. Physical width/height percentages are unaffected.
179
    pub vertical_writing_mode: bool,
180

            
181
    /// The viewport size in CSS pixels (for vw, vh, vmin, vmax units)
182
    /// This is the layout viewport size, not physical screen size
183
    pub viewport_size: PhysicalSize,
184
}
185

            
186
impl Default for ResolutionContext {
187
3
    fn default() -> Self {
188
3
        Self {
189
3
            element_font_size: 16.0,
190
3
            parent_font_size: 16.0,
191
3
            root_font_size: 16.0,
192
3
            containing_block_size: PhysicalSize::new(0.0, 0.0),
193
3
            element_size: None,
194
3
            viewport_size: PhysicalSize::new(0.0, 0.0),
195
3
            vertical_writing_mode: false,
196
3
        }
197
3
    }
198
}
199

            
200
impl ResolutionContext {
201
    /// Create a minimal context for testing or default resolution
202
    #[inline]
203
    #[must_use]
204
2
    pub const fn default_const() -> Self {
205
2
        Self {
206
2
            element_font_size: 16.0,
207
2
            parent_font_size: 16.0,
208
2
            root_font_size: 16.0,
209
2
            containing_block_size: PhysicalSize {
210
2
                width: 0.0,
211
2
                height: 0.0,
212
2
            },
213
2
            element_size: None,
214
2
            viewport_size: PhysicalSize {
215
2
                width: 0.0,
216
2
                height: 0.0,
217
2
            },
218
2
            vertical_writing_mode: false,
219
2
        }
220
2
    }
221
}
222

            
223
/// Specifies which property context we're resolving for, to determine correct reference values
224
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
225
pub enum PropertyContext {
226
    /// Resolving for the font-size property itself (em refers to parent)
227
    FontSize,
228
    /// Resolving for margin properties (% refers to containing block width)
229
    Margin,
230
    /// Resolving for padding properties (% refers to containing block width)
231
    Padding,
232
    /// Resolving for width or horizontal properties (% refers to containing block width)
233
    Width,
234
    /// Resolving for height or vertical properties (% refers to containing block height)
235
    Height,
236
    /// Resolving for border-width properties (only absolute lengths + em/rem, no % support)
237
    BorderWidth,
238
    /// Resolving for border-radius (% refers to element's own dimensions)
239
    BorderRadius,
240
    /// Resolving for transforms (% refers to element's own dimensions)
241
    Transform,
242
    /// Resolving for other properties (em refers to element font-size)
243
    Other,
244
}
245

            
246
/// A CSS length value consisting of a numeric value and a unit (px, em, rem, %, etc.).
247
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
248
#[repr(C)]
249
pub struct PixelValue {
250
    pub metric: SizeMetric,
251
    pub number: FloatValue,
252
}
253

            
254
impl PixelValue {
255
175
    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
256
175
        self.number = FloatValue::new(self.number.get() * scale_factor);
257
175
    }
258

            
259
    /// Whether this value resolves to pixels with NO context.
260
    ///
261
    /// True for `px`, `pt`, `in`, `cm`, `mm`. False for `em`, `rem`, `%` and
262
    /// the viewport units, all of which need something the value itself does
263
    /// not carry.
264
    #[must_use]
265
200
    pub const fn is_absolute(&self) -> bool {
266
22
        matches!(
267
200
            self.metric,
268
            SizeMetric::Px | SizeMetric::Pt | SizeMetric::In | SizeMetric::Cm | SizeMetric::Mm
269
        )
270
200
    }
271

            
272
    /// Resolve to pixels WITHOUT a context, or `None` if this value needs one.
273
    ///
274
    /// The honest form for the many values that are absolute by construction -
275
    /// a safe-area inset, a scrollbar width, a border reported by the system
276
    /// theme. `CallbackInfo::get_safe_area_insets()` hands back
277
    /// `OptionPixelValue`, and before this the only way out was
278
    /// `p.number.get()`, which silently returns `24` for `24em` as readily as
279
    /// for `24px`: correct only because the caller happened to know the value
280
    /// was absolute.
281
    ///
282
    /// Returning `None` rather than a number is the point. A relative unit has
283
    /// no pixel value until something supplies the reference, so answering
284
    /// with one would be inventing it - which is exactly how
285
    /// `to_pixels_internal` reports viewport units as `0.0`.
286
    #[must_use]
287
188
    pub fn to_pixels_absolute(&self) -> OptionF32 {
288
188
        if self.is_absolute() {
289
            // The resolves are unused for absolute metrics; pass zeroes rather
290
            // than inventing a context.
291
173
            OptionF32::Some(self.to_pixels_internal(0.0, 0.0, 0.0))
292
        } else {
293
15
            OptionF32::None
294
        }
295
188
    }
296

            
297
    /// Resolve to pixels, supplying the reference each relative unit needs.
298
    ///
299
    /// - `percent_resolve` - the 100% reference for `%`
300
    /// - `em_resolve` - the element's own font size, for `em`
301
    /// - `rem_resolve` - the root font size, for `rem`
302
    ///
303
    /// Absolute units ignore all three, so an absolute value can be resolved
304
    /// with zeroes - though [`Self::to_pixels_absolute`] says that more
305
    /// clearly.
306
    ///
307
    /// # Viewport units
308
    ///
309
    /// `vw`/`vh`/`vmin`/`vmax` resolve to `0.0` here, because this signature
310
    /// carries no viewport. That is a documented limitation of this entry
311
    /// point rather than a correct answer; the engine's own resolution path
312
    /// handles them.
313
    #[must_use]
314
82
    pub fn to_pixels(&self, percent_resolve: f32, em_resolve: f32, rem_resolve: f32) -> f32 {
315
82
        self.to_pixels_internal(percent_resolve, em_resolve, rem_resolve)
316
82
    }
317
}
318

            
319
impl FormatAsCssValue for PixelValue {
320
193
    fn format_as_css_value(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321
193
        write!(f, "{}{}", self.number, self.metric)
322
193
    }
323
}
324

            
325
impl crate::css::PrintAsCssValue for PixelValue {
326
292
    fn print_as_css_value(&self) -> String {
327
292
        format!("{}{}", self.number, self.metric)
328
292
    }
329
}
330

            
331
impl crate::codegen::format::FormatAsRustCode for PixelValue {
332
4
    fn format_as_rust_code(&self, _tabs: usize) -> String {
333
4
        format!(
334
4
            "PixelValue {{ metric: {:?}, number: FloatValue::new({}) }}",
335
            self.metric,
336
4
            self.number.get()
337
        )
338
4
    }
339
}
340

            
341
// Manual Debug implementation, because the auto-generated one is nearly unreadable
342
impl fmt::Debug for PixelValue {
343
317739
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344
317739
        write!(f, "{}{}", self.number, self.metric)
345
317739
    }
346
}
347

            
348
impl fmt::Display for PixelValue {
349
9278
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350
9278
        write!(f, "{}{}", self.number, self.metric)
351
9278
    }
352
}
353

            
354
impl PixelValue {
355
    #[inline]
356
    #[must_use]
357
23752
    pub const fn zero() -> Self {
358
        const ZERO_PX: PixelValue = PixelValue::const_px(0);
359
23752
        ZERO_PX
360
23752
    }
361

            
362
    /// Same as `PixelValue::px()`, but only accepts whole numbers,
363
    /// since using `f32` in const fn is not yet stabilized.
364
    #[inline]
365
    #[must_use]
366
5889036
    pub const fn const_px(value: isize) -> Self {
367
5889036
        Self::const_from_metric(SizeMetric::Px, value)
368
5889036
    }
369

            
370
    /// Same as `PixelValue::em()`, but only accepts whole numbers,
371
    /// since using `f32` in const fn is not yet stabilized.
372
    #[inline]
373
    #[must_use]
374
13
    pub const fn const_em(value: isize) -> Self {
375
13
        Self::const_from_metric(SizeMetric::Em, value)
376
13
    }
377

            
378
    /// Creates an em value from a fractional number in const context.
379
    ///
380
    /// # Arguments
381
    /// * `pre_comma` - The integer part (e.g., 1 for 1.5em)
382
    /// * `post_comma` - The fractional part as digits (e.g., 5 for 0.5em, 83 for 0.83em)
383
    ///
384
    /// # Examples
385
    /// ```
386
    /// // 1.5em = const_em_fractional(1, 5)
387
    /// // 0.83em = const_em_fractional(0, 83)
388
    /// // 1.17em = const_em_fractional(1, 17)
389
    /// ```
390
    #[inline]
391
    #[must_use]
392
4
    pub const fn const_em_fractional(pre_comma: isize, post_comma: isize) -> Self {
393
4
        Self::const_from_metric_fractional(SizeMetric::Em, pre_comma, post_comma)
394
4
    }
395

            
396
    /// Same as `PixelValue::pt()`, but only accepts whole numbers,
397
    /// since using `f32` in const fn is not yet stabilized.
398
    #[inline]
399
    #[must_use]
400
16
    pub const fn const_pt(value: isize) -> Self {
401
16
        Self::const_from_metric(SizeMetric::Pt, value)
402
16
    }
403

            
404
    /// Creates a pt value from a fractional number in const context.
405
    #[inline]
406
    #[must_use]
407
2
    pub const fn const_pt_fractional(pre_comma: isize, post_comma: isize) -> Self {
408
2
        Self::const_from_metric_fractional(SizeMetric::Pt, pre_comma, post_comma)
409
2
    }
410

            
411
    /// Same as `PixelValue::percent()`, but only accepts whole numbers,
412
    /// since using `f32` in const fn is not yet stabilized.
413
    #[inline]
414
    #[must_use]
415
18354
    pub const fn const_percent(value: isize) -> Self {
416
18354
        Self::const_from_metric(SizeMetric::Percent, value)
417
18354
    }
418

            
419
    /// Same as `PixelValue::in()`, but only accepts whole numbers,
420
    /// since using `f32` in const fn is not yet stabilized.
421
    #[inline]
422
    #[must_use]
423
7
    pub const fn const_in(value: isize) -> Self {
424
7
        Self::const_from_metric(SizeMetric::In, value)
425
7
    }
426

            
427
    /// Same as `PixelValue::cm()`, but only accepts whole numbers,
428
    /// since using `f32` in const fn is not yet stabilized.
429
    #[inline]
430
    #[must_use]
431
8
    pub const fn const_cm(value: isize) -> Self {
432
8
        Self::const_from_metric(SizeMetric::Cm, value)
433
8
    }
434

            
435
    /// Same as `PixelValue::mm()`, but only accepts whole numbers,
436
    /// since using `f32` in const fn is not yet stabilized.
437
    #[inline]
438
    #[must_use]
439
8
    pub const fn const_mm(value: isize) -> Self {
440
8
        Self::const_from_metric(SizeMetric::Mm, value)
441
8
    }
442

            
443
    #[inline]
444
    #[must_use]
445
5907527
    pub const fn const_from_metric(metric: SizeMetric, value: isize) -> Self {
446
5907527
        Self {
447
5907527
            metric,
448
5907527
            number: FloatValue::const_new(value),
449
5907527
        }
450
5907527
    }
451

            
452
    /// Creates a `PixelValue` from a fractional number in const context.
453
    ///
454
    /// # Arguments
455
    /// * `metric` - The size metric (Px, Em, Pt, etc.)
456
    /// * `pre_comma` - The integer part
457
    /// * `post_comma` - The fractional part as digits
458
    #[inline]
459
    #[must_use]
460
10
    pub const fn const_from_metric_fractional(
461
10
        metric: SizeMetric,
462
10
        pre_comma: isize,
463
10
        post_comma: isize,
464
10
    ) -> Self {
465
10
        Self {
466
10
            metric,
467
10
            number: FloatValue::const_new_fractional(pre_comma, post_comma),
468
10
        }
469
10
    }
470

            
471
    #[inline]
472
    #[must_use]
473
17228985
    pub fn px(value: f32) -> Self {
474
17228985
        Self::from_metric(SizeMetric::Px, value)
475
17228985
    }
476

            
477
    #[inline]
478
    #[must_use]
479
1533
    pub fn em(value: f32) -> Self {
480
1533
        Self::from_metric(SizeMetric::Em, value)
481
1533
    }
482

            
483
    #[inline]
484
    #[must_use]
485
28
    pub fn inch(value: f32) -> Self {
486
28
        Self::from_metric(SizeMetric::In, value)
487
28
    }
488

            
489
    #[inline]
490
    #[must_use]
491
28
    pub fn cm(value: f32) -> Self {
492
28
        Self::from_metric(SizeMetric::Cm, value)
493
28
    }
494

            
495
    #[inline]
496
    #[must_use]
497
28
    pub fn mm(value: f32) -> Self {
498
28
        Self::from_metric(SizeMetric::Mm, value)
499
28
    }
500

            
501
    #[inline]
502
    #[must_use]
503
61
    pub fn pt(value: f32) -> Self {
504
61
        Self::from_metric(SizeMetric::Pt, value)
505
61
    }
506

            
507
    #[inline]
508
    #[must_use]
509
30961
    pub fn percent(value: f32) -> Self {
510
30961
        Self::from_metric(SizeMetric::Percent, value)
511
30961
    }
512

            
513
    #[inline]
514
    #[must_use]
515
72
    pub fn rem(value: f32) -> Self {
516
72
        Self::from_metric(SizeMetric::Rem, value)
517
72
    }
518

            
519
    #[inline]
520
    #[must_use]
521
18173885
    pub fn from_metric(metric: SizeMetric, value: f32) -> Self {
522
18173885
        Self {
523
18173885
            metric,
524
18173885
            number: FloatValue::new(value),
525
18173885
        }
526
18173885
    }
527

            
528
    #[inline]
529
    #[allow(clippy::suboptimal_flops)] // explicit FP; mul_add slower without +fma
530
    #[must_use]
531
2832
    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
532
2832
        if self.metric == other.metric {
533
2787
            Self {
534
2787
                metric: self.metric,
535
2787
                number: self.number.interpolate(&other.number, t),
536
2787
            }
537
        } else {
538
            // Interpolate between different metrics by converting to px
539
            // Note: Uses DEFAULT_FONT_SIZE for em/rem - acceptable for animation fallback
540
45
            let self_px_interp = self.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
541
45
            let other_px_interp =
542
45
                other.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
543
45
            Self::from_metric(
544
45
                SizeMetric::Px,
545
45
                self_px_interp + (other_px_interp - self_px_interp) * t,
546
            )
547
        }
548
2832
    }
549

            
550
    /// Returns the value of the `SizeMetric` as a normalized percentage (0.0 = 0%, 1.0 = 100%)
551
    ///
552
    /// Returns `Some(NormalizedPercentage)` if this is a percentage value, `None` otherwise.
553
    /// The returned `NormalizedPercentage` is already normalized to 0.0-1.0 range,
554
    /// so you should multiply it directly with the containing block size.
555
    #[inline]
556
    #[must_use]
557
11201
    pub fn to_percent(&self) -> Option<NormalizedPercentage> {
558
11201
        match self.metric {
559
10926
            SizeMetric::Percent => Some(NormalizedPercentage::from_unnormalized(self.number.get())),
560
275
            _ => None,
561
        }
562
11201
    }
563

            
564
    /// Internal fallback method for converting to pixels with manual % resolution.
565
    ///
566
    /// Used internally by prop_cache.rs resolve_property_dependency().
567
    ///
568
    /// **DO NOT USE directly!** Use `resolve_with_context()` instead for new code.
569
    #[doc(hidden)]
570
    #[inline]
571
    #[must_use]
572
2347226
    pub fn to_pixels_internal(
573
2347226
        &self,
574
2347226
        percent_resolve: f32,
575
2347226
        em_resolve: f32,
576
2347226
        rem_resolve: f32,
577
2347226
    ) -> f32 {
578
2347226
        match self.metric {
579
2334760
            SizeMetric::Px => self.number.get(),
580
29
            SizeMetric::Pt => self.number.get() * PT_TO_PX,
581
11
            SizeMetric::In => self.number.get() * 96.0,
582
11
            SizeMetric::Cm => self.number.get() * 96.0 / 2.54,
583
11
            SizeMetric::Mm => self.number.get() * 96.0 / 25.4,
584
200
            SizeMetric::Em => self.number.get() * em_resolve,
585
26
            SizeMetric::Rem => self.number.get() * rem_resolve,
586
            SizeMetric::Percent => {
587
12104
                NormalizedPercentage::from_unnormalized(self.number.get()).resolve(percent_resolve)
588
            }
589
            // Viewport units: Cannot resolve without viewport context, return 0
590
            // These should use resolve_with_context() instead
591
74
            SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => 0.0,
592
        }
593
2347226
    }
594

            
595
    /// Resolve this value to pixels using proper CSS context.
596
    ///
597
    /// This is the **CORRECT** way to resolve CSS units. It properly handles:
598
    /// - em units: Uses element's own font-size (or parent's for font-size property)
599
    /// - rem units: Uses root element's font-size
600
    /// - % units: Uses property-appropriate reference (containing block width/height, element size,
601
    ///   etc.)
602
    /// - Absolute units: px, pt, in, cm, mm (already correct)
603
    ///
604
    /// # Arguments
605
    /// * `context` - Resolution context with font sizes and dimensions
606
    /// * `property_context` - Which property we're resolving for (affects % and em resolution)
607
    #[inline]
608
    #[must_use]
609
4278576
    pub fn resolve_with_context(
610
4278576
        &self,
611
4278576
        context: &ResolutionContext,
612
4278576
        property_context: PropertyContext,
613
4278576
    ) -> f32 {
614
4278576
        match self.metric {
615
            // Absolute units - already correct
616
4245016
            SizeMetric::Px => self.number.get(),
617
136
            SizeMetric::Pt => self.number.get() * PT_TO_PX,
618
136
            SizeMetric::In => self.number.get() * 96.0,
619
136
            SizeMetric::Cm => self.number.get() * 96.0 / 2.54,
620
136
            SizeMetric::Mm => self.number.get() * 96.0 / 25.4,
621

            
622
            // Em units - CRITICAL: different resolution for font-size vs other properties
623
            SizeMetric::Em => {
624
30697
                let reference_font_size = if property_context == PropertyContext::FontSize {
625
                    // Em on font-size refers to parent's font-size (CSS 2.1 §15.7)
626
111
                    context.parent_font_size
627
                } else {
628
                    // Em on other properties refers to element's own font-size (CSS 2.1 §10.5)
629
30586
                    context.element_font_size
630
                };
631
30697
                self.number.get() * reference_font_size
632
            }
633

            
634
            // Rem units - ALWAYS refer to root font-size (CSS Values 3)
635
188
            SizeMetric::Rem => self.number.get() * context.root_font_size,
636

            
637
            // Viewport units - refer to viewport dimensions (CSS Values 3 §6.2)
638
            // 1vw = 1% of viewport width, 1vh = 1% of viewport height
639
384
            SizeMetric::Vw => self.number.get() * context.viewport_size.width / 100.0,
640
431
            SizeMetric::Vh => self.number.get() * context.viewport_size.height / 100.0,
641
            // vmin = smaller of vw or vh
642
            SizeMetric::Vmin => {
643
144
                let min_dimension = context
644
144
                    .viewport_size
645
144
                    .width
646
144
                    .min(context.viewport_size.height);
647
144
                self.number.get() * min_dimension / 100.0
648
            }
649
            // vmax = larger of vw or vh
650
            SizeMetric::Vmax => {
651
131
                let max_dimension = context
652
131
                    .viewport_size
653
131
                    .width
654
131
                    .max(context.viewport_size.height);
655
131
                self.number.get() * max_dimension / 100.0
656
            }
657

            
658
            // Percent units - reference depends on property type
659
            SizeMetric::Percent => {
660
                // Width and Other deliberately both resolve to containing-block width but are
661
                // kept as separate arms for documentation / likely future divergence.
662
                #[allow(clippy::match_same_arms)]
663
1041
                let reference = match property_context {
664
                    // Font-size %: refers to parent's font-size (CSS 2.1 §15.7)
665
14
                    PropertyContext::FontSize => context.parent_font_size,
666

            
667
                    // Width and horizontal properties: containing block width (CSS 2.1 §10.3)
668
110
                    PropertyContext::Width => context.containing_block_size.width,
669

            
670
                    // Height and vertical properties: containing block height (CSS 2.1 §10.5)
671
110
                    PropertyContext::Height => context.containing_block_size.height,
672

            
673
                    // +spec:box-model:66e123 - margin/padding % resolved against inline size (=
674
                    // width in horizontal-tb) +spec:width-calculation:bef810 -
675
                    // margin percentages refer to containing block width (even top/bottom)
676
                    // Margins: ALWAYS containing block WIDTH, even for top/bottom! (CSS 2.1 §8.3)
677
                    // +spec:width-calculation:d78514 - margin percentages refer to width of
678
                    // containing block Padding: ALWAYS containing block WIDTH,
679
                    // even for top/bottom! (CSS 2.1 §8.4)
680
                    PropertyContext::Margin | PropertyContext::Padding => {
681
                        // CSS3 (writing-modes-4 §7.2) upgrades CSS 2.1's
682
                        // "always width" to "the INLINE size": physical width
683
                        // in horizontal-tb, physical HEIGHT in vertical-rl/lr.
684
497
                        if context.vertical_writing_mode {
685
24
                            context.containing_block_size.height
686
                        } else {
687
473
                            context.containing_block_size.width
688
                        }
689
                    }
690

            
691
                    // Border-width: % is NOT valid per CSS spec (CSS Backgrounds 3 §4.1)
692
                    // Return 0.0 if someone tries to use % on border-width
693
74
                    PropertyContext::BorderWidth => 0.0,
694

            
695
                    // Border-radius: element's own dimensions (CSS Backgrounds 3 §5.1)
696
                    // Note: More complex - horizontal % uses width, vertical % uses height
697
                    // For now, use width as default
698
207
                    PropertyContext::BorderRadius => context.element_size.map_or(0.0, |s| s.width),
699

            
700
                    // Transforms: element's own dimensions (CSS Transforms §20.1)
701
15
                    PropertyContext::Transform => context.element_size.map_or(0.0, |s| s.width),
702

            
703
                    // Other properties: default to containing block width
704
14
                    PropertyContext::Other => context.containing_block_size.width,
705
                };
706

            
707
1041
                NormalizedPercentage::from_unnormalized(self.number.get()).resolve(reference)
708
            }
709
        }
710
4278576
    }
711
}
712

            
713
// border-width: thin / medium / thick keyword values
714
// These are the canonical CSS definitions and should be used consistently
715
// across parsing and resolution.
716

            
717
/// border-width: thin = 1px (per CSS spec)
718
pub const THIN_BORDER_THICKNESS: PixelValue = PixelValue {
719
    metric: SizeMetric::Px,
720
    number: FloatValue { number: 1000 },
721
};
722

            
723
/// border-width: medium = 3px (per CSS spec, default)
724
pub const MEDIUM_BORDER_THICKNESS: PixelValue = PixelValue {
725
    metric: SizeMetric::Px,
726
    number: FloatValue { number: 3000 },
727
};
728

            
729
/// border-width: thick = 5px (per CSS spec)
730
pub const THICK_BORDER_THICKNESS: PixelValue = PixelValue {
731
    metric: SizeMetric::Px,
732
    number: FloatValue { number: 5000 },
733
};
734

            
735
/// Same as `PixelValue`, but doesn't allow a "%" sign
736
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
737
#[repr(C)]
738
pub struct PixelValueNoPercent {
739
    pub inner: PixelValue,
740
}
741

            
742
impl PixelValueNoPercent {
743
146
    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
744
146
        self.inner.scale_for_dpi(scale_factor);
745
146
    }
746
}
747

            
748
impl_option!(
749
    PixelValueNoPercent,
750
    OptionPixelValueNoPercent,
751
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
752
);
753

            
754
impl_option!(
755
    PixelValue,
756
    OptionPixelValue,
757
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
758
);
759

            
760
impl fmt::Display for PixelValueNoPercent {
761
2618
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
762
2618
        write!(f, "{}", self.inner)
763
2618
    }
764
}
765

            
766
impl ::core::fmt::Debug for PixelValueNoPercent {
767
2497
    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
768
2497
        write!(f, "{self}")
769
2497
    }
770
}
771

            
772
impl PixelValueNoPercent {
773
    /// Internal conversion to pixels (no percent support).
774
    ///
775
    /// Used internally by prop_cache.rs.
776
    ///
777
    /// **DO NOT USE directly!** Use `resolve_with_context()` on inner value instead.
778
    #[doc(hidden)]
779
    #[inline]
780
    #[must_use]
781
533
    pub fn to_pixels_internal(&self, em_resolve: f32, rem_resolve: f32) -> f32 {
782
533
        self.inner.to_pixels_internal(0.0, em_resolve, rem_resolve)
783
533
    }
784

            
785
    #[inline]
786
    #[must_use]
787
6
    pub const fn zero() -> Self {
788
        const ZERO_PXNP: PixelValueNoPercent = PixelValueNoPercent {
789
            inner: PixelValue::zero(),
790
        };
791
6
        ZERO_PXNP
792
6
    }
793
}
794
impl From<PixelValue> for PixelValueNoPercent {
795
17
    fn from(e: PixelValue) -> Self {
796
17
        Self { inner: e }
797
17
    }
798
}
799

            
800
#[derive(Clone, PartialEq, Eq)]
801
pub enum CssPixelValueParseError<'a> {
802
    EmptyString,
803
    NoValueGiven(&'a str, SizeMetric),
804
    ValueParseErr(ParseFloatError, &'a str),
805
    InvalidPixelValue(&'a str),
806
}
807

            
808
impl_debug_as_display!(CssPixelValueParseError<'a>);
809

            
810
impl_display! { CssPixelValueParseError<'a>, {
811
    EmptyString => format!("Missing [px / pt / em / %] value"),
812
    NoValueGiven(input, metric) => format!("Expected floating-point pixel value, got: \"{}{}\"", input, metric),
813
    ValueParseErr(err, number_str) => format!("Could not parse \"{}\" as floating-point value: \"{}\"", number_str, err),
814
    InvalidPixelValue(s) => format!("Invalid pixel value: \"{}\"", s),
815
}}
816

            
817
/// Wrapper for `NoValueGiven` error in pixel value parsing.
818
#[derive(Debug, Clone, PartialEq, Eq)]
819
#[repr(C)]
820
pub struct PixelNoValueGivenError {
821
    pub value: AzString,
822
    pub metric: SizeMetric,
823
}
824

            
825
/// Owned version of `CssPixelValueParseError`.
826
#[derive(Debug, Clone, PartialEq, Eq)]
827
#[repr(C, u8)]
828
pub enum CssPixelValueParseErrorOwned {
829
    EmptyString,
830
    NoValueGiven(PixelNoValueGivenError),
831
    ValueParseErr(ParseFloatErrorWithInput),
832
    InvalidPixelValue(AzString),
833
}
834

            
835
impl CssPixelValueParseError<'_> {
836
    #[must_use]
837
143
    pub fn to_contained(&self) -> CssPixelValueParseErrorOwned {
838
143
        match self {
839
29
            CssPixelValueParseError::EmptyString => CssPixelValueParseErrorOwned::EmptyString,
840
21
            CssPixelValueParseError::NoValueGiven(s, metric) => {
841
21
                CssPixelValueParseErrorOwned::NoValueGiven(PixelNoValueGivenError {
842
21
                    value: (*s).to_string().into(),
843
21
                    metric: *metric,
844
21
                })
845
            }
846
23
            CssPixelValueParseError::ValueParseErr(err, s) => {
847
23
                CssPixelValueParseErrorOwned::ValueParseErr(ParseFloatErrorWithInput {
848
23
                    error: err.clone().into(),
849
23
                    input: (*s).to_string().into(),
850
23
                })
851
            }
852
70
            CssPixelValueParseError::InvalidPixelValue(s) => {
853
70
                CssPixelValueParseErrorOwned::InvalidPixelValue((*s).to_string().into())
854
            }
855
        }
856
143
    }
857
}
858

            
859
impl CssPixelValueParseErrorOwned {
860
    #[must_use]
861
131
    pub fn to_shared(&self) -> CssPixelValueParseError<'_> {
862
131
        match self {
863
26
            Self::EmptyString => CssPixelValueParseError::EmptyString,
864
20
            Self::NoValueGiven(e) => {
865
20
                CssPixelValueParseError::NoValueGiven(e.value.as_str(), e.metric)
866
            }
867
20
            Self::ValueParseErr(e) => {
868
20
                CssPixelValueParseError::ValueParseErr(e.error.to_std(), e.input.as_str())
869
            }
870
65
            Self::InvalidPixelValue(s) => CssPixelValueParseError::InvalidPixelValue(s.as_str()),
871
        }
872
131
    }
873
}
874

            
875
/// parses an angle value like `30deg`, `1.64rad`, `100%`, etc.
876
916281
fn parse_pixel_value_inner<'a>(
877
916281
    input: &'a str,
878
916281
    match_values: &[(&'static str, SizeMetric)],
879
916281
) -> Result<PixelValue, CssPixelValueParseError<'a>> {
880
916281
    let input = input.trim();
881

            
882
916281
    if input.is_empty() {
883
207
        return Err(CssPixelValueParseError::EmptyString);
884
916074
    }
885

            
886
1563744
    for (match_val, metric) in match_values {
887
1538586
        if let Some(value) = input.strip_suffix(match_val) {
888
890916
            let value = value.trim();
889
890916
            if value.is_empty() {
890
429
                return Err(CssPixelValueParseError::NoValueGiven(input, *metric));
891
890487
            }
892
890487
            match value.parse::<f32>() {
893
890184
                Ok(o) => {
894
890184
                    return Ok(PixelValue::from_metric(*metric, o));
895
                }
896
303
                Err(e) => {
897
303
                    return Err(CssPixelValueParseError::ValueParseErr(e, value));
898
                }
899
            }
900
647670
        }
901
    }
902

            
903
25158
    input.trim().parse::<f32>().map_or_else(
904
3378
        |_| Err(CssPixelValueParseError::InvalidPixelValue(input)),
905
21780
        |o| Ok(PixelValue::px(o)),
906
    )
907
916281
}
908

            
909
/// # Errors
910
///
911
/// Returns an error if `input` is not a valid CSS `pixel-value` value.
912
915414
pub fn parse_pixel_value(input: &str) -> Result<PixelValue, CssPixelValueParseError<'_>> {
913
915414
    parse_pixel_value_inner(
914
915414
        input,
915
915414
        &[
916
915414
            // ORDER IS LOAD-BEARING: matching is by `strip_suffix`, first hit wins, so
917
915414
            // any unit that is a SUFFIX of another must come after it.
918
915414
            ("px", SizeMetric::Px),
919
915414
            ("rem", SizeMetric::Rem), // before "em" ("rem" ends with "em")
920
915414
            ("em", SizeMetric::Em),
921
915414
            ("pt", SizeMetric::Pt),
922
915414
            ("vmax", SizeMetric::Vmax),
923
915414
            ("vmin", SizeMetric::Vmin), // before "in" -- "vmin" ends with "in"!
924
915414
            ("vw", SizeMetric::Vw),
925
915414
            ("vh", SizeMetric::Vh),
926
915414
            ("in", SizeMetric::In),
927
915414
            ("mm", SizeMetric::Mm),
928
915414
            ("cm", SizeMetric::Cm),
929
915414
            ("%", SizeMetric::Percent),
930
915414
        ],
931
    )
932
915414
}
933

            
934
/// # Errors
935
///
936
/// Returns an error if `input` is not a valid CSS `pixel-value-no-percent` value.
937
861
pub fn parse_pixel_value_no_percent(
938
861
    input: &str,
939
861
) -> Result<PixelValueNoPercent, CssPixelValueParseError<'_>> {
940
    Ok(PixelValueNoPercent {
941
861
        inner: parse_pixel_value_inner(
942
861
            input,
943
861
            &[
944
861
                // ORDER IS LOAD-BEARING -- see parse_pixel_value above.
945
861
                ("px", SizeMetric::Px),
946
861
                ("rem", SizeMetric::Rem), // before "em" ("rem" ends with "em")
947
861
                ("em", SizeMetric::Em),
948
861
                ("pt", SizeMetric::Pt),
949
861
                ("vmax", SizeMetric::Vmax),
950
861
                ("vmin", SizeMetric::Vmin), // before "in" -- "vmin" ends with "in"!
951
861
                ("vw", SizeMetric::Vw),
952
861
                ("vh", SizeMetric::Vh),
953
861
                ("in", SizeMetric::In),
954
861
                ("mm", SizeMetric::Mm),
955
861
                ("cm", SizeMetric::Cm),
956
861
            ],
957
35
        )?,
958
    })
959
861
}
960

            
961
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
962
pub enum PixelValueWithAuto {
963
    None,
964
    Initial,
965
    Inherit,
966
    Auto,
967
    Exact(PixelValue),
968
}
969

            
970
/// Parses a pixel value, but also tries values like "auto", "initial", "inherit" and "none"
971
/// # Errors
972
///
973
/// Returns an error if `input` is not a valid CSS `pixel-value-with-auto` value.
974
445492
pub fn parse_pixel_value_with_auto(
975
445492
    input: &str,
976
445492
) -> Result<PixelValueWithAuto, CssPixelValueParseError<'_>> {
977
445492
    let input = input.trim();
978
445492
    match input {
979
445492
        "none" => Ok(PixelValueWithAuto::None),
980
445488
        "initial" => Ok(PixelValueWithAuto::Initial),
981
445484
        "inherit" => Ok(PixelValueWithAuto::Inherit),
982
445478
        "auto" => Ok(PixelValueWithAuto::Auto),
983
445442
        e => Ok(PixelValueWithAuto::Exact(parse_pixel_value(e)?)),
984
    }
985
445492
}
986

            
987
// ============================================================================
988
// System Metric References (system:button-padding, system:button-radius, etc.)
989
// ============================================================================
990

            
991
/// Reference to a specific system metric value.
992
/// These are resolved at runtime based on the user's system preferences.
993
///
994
/// CSS syntax: `system:button-padding`, `system:button-radius`, `system:titlebar-height`, etc.
995
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
996
#[repr(C)]
997
#[derive(Default)]
998
pub enum SystemMetricRef {
999
    /// Button corner radius (system:button-radius)
    #[default]
    ButtonRadius,
    /// Button horizontal padding (system:button-padding-horizontal)
    ButtonPaddingHorizontal,
    /// Button vertical padding (system:button-padding-vertical)
    ButtonPaddingVertical,
    /// Button border width (system:button-border-width)
    ButtonBorderWidth,
    /// Titlebar height (system:titlebar-height)
    TitlebarHeight,
    /// Titlebar button area width (system:titlebar-button-width)
    TitlebarButtonWidth,
    /// Titlebar horizontal padding (system:titlebar-padding)
    TitlebarPadding,
    /// Safe area top inset for notched devices (system:safe-area-top)
    SafeAreaTop,
    /// Safe area bottom inset (system:safe-area-bottom)
    SafeAreaBottom,
    /// Safe area left inset (system:safe-area-left)
    SafeAreaLeft,
    /// Safe area right inset (system:safe-area-right)
    SafeAreaRight,
}
impl SystemMetricRef {
    /// Resolve this system metric reference against actual system metrics.
    #[must_use]
35
    pub const fn resolve(&self, metrics: &crate::system::SystemMetrics) -> Option<PixelValue> {
35
        match self {
5
            Self::ButtonRadius => metrics.corner_radius.as_option().copied(),
3
            Self::ButtonPaddingHorizontal => metrics.button_padding_horizontal.as_option().copied(),
3
            Self::ButtonPaddingVertical => metrics.button_padding_vertical.as_option().copied(),
3
            Self::ButtonBorderWidth => metrics.border_width.as_option().copied(),
3
            Self::TitlebarHeight => metrics.titlebar.height.as_option().copied(),
3
            Self::TitlebarButtonWidth => metrics.titlebar.button_area_width.as_option().copied(),
3
            Self::TitlebarPadding => metrics.titlebar.padding_horizontal.as_option().copied(),
3
            Self::SafeAreaTop => metrics.titlebar.safe_area.top.as_option().copied(),
3
            Self::SafeAreaBottom => metrics.titlebar.safe_area.bottom.as_option().copied(),
3
            Self::SafeAreaLeft => metrics.titlebar.safe_area.left.as_option().copied(),
3
            Self::SafeAreaRight => metrics.titlebar.safe_area.right.as_option().copied(),
        }
35
    }
    /// Returns the CSS string representation of this system metric reference.
    #[must_use]
57
    pub const fn as_css_str(&self) -> &'static str {
57
        match self {
5
            Self::ButtonRadius => "system:button-radius",
5
            Self::ButtonPaddingHorizontal => "system:button-padding-horizontal",
5
            Self::ButtonPaddingVertical => "system:button-padding-vertical",
5
            Self::ButtonBorderWidth => "system:button-border-width",
7
            Self::TitlebarHeight => "system:titlebar-height",
5
            Self::TitlebarButtonWidth => "system:titlebar-button-width",
5
            Self::TitlebarPadding => "system:titlebar-padding",
5
            Self::SafeAreaTop => "system:safe-area-top",
5
            Self::SafeAreaBottom => "system:safe-area-bottom",
5
            Self::SafeAreaLeft => "system:safe-area-left",
5
            Self::SafeAreaRight => "system:safe-area-right",
        }
57
    }
    /// Parse a system metric reference from a CSS string (without the "system:" prefix).
    #[must_use]
75
    pub fn from_css_str(s: &str) -> Option<Self> {
75
        match s {
75
            "button-radius" => Some(Self::ButtonRadius),
71
            "button-padding-horizontal" => Some(Self::ButtonPaddingHorizontal),
67
            "button-padding-vertical" => Some(Self::ButtonPaddingVertical),
63
            "button-border-width" => Some(Self::ButtonBorderWidth),
59
            "titlebar-height" => Some(Self::TitlebarHeight),
55
            "titlebar-button-width" => Some(Self::TitlebarButtonWidth),
51
            "titlebar-padding" => Some(Self::TitlebarPadding),
47
            "safe-area-top" => Some(Self::SafeAreaTop),
43
            "safe-area-bottom" => Some(Self::SafeAreaBottom),
39
            "safe-area-left" => Some(Self::SafeAreaLeft),
35
            "safe-area-right" => Some(Self::SafeAreaRight),
31
            _ => None,
        }
75
    }
}
impl fmt::Display for SystemMetricRef {
12
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12
        write!(f, "{}", self.as_css_str())
12
    }
}
impl FormatAsCssValue for SystemMetricRef {
12
    fn format_as_css_value(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12
        write!(f, "{}", self.as_css_str())
12
    }
}
/// A pixel value reference that can be either a concrete value or a system metric.
/// System metrics are lazily evaluated at runtime based on the user's system theme.
///
/// CSS syntax: `10px`, `1.5em`, `system:button-padding`, etc.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
#[repr(C, u8)]
pub enum PixelValueOrSystem {
    /// A concrete pixel value.
    Value(PixelValue),
    /// A reference to a system metric, resolved at runtime.
    System(SystemMetricRef),
}
impl Default for PixelValueOrSystem {
3
    fn default() -> Self {
3
        Self::Value(PixelValue::zero())
3
    }
}
impl From<PixelValue> for PixelValueOrSystem {
1
    fn from(value: PixelValue) -> Self {
1
        Self::Value(value)
1
    }
}
impl PixelValueOrSystem {
    /// Create a new `PixelValueOrSystem` from a concrete value.
    #[must_use]
15
    pub const fn value(v: PixelValue) -> Self {
15
        Self::Value(v)
15
    }
    /// Create a new `PixelValueOrSystem` from a system metric reference.
    #[must_use]
13
    pub const fn system(s: SystemMetricRef) -> Self {
13
        Self::System(s)
13
    }
    /// Resolve the pixel value against a `SystemMetrics` struct.
    /// Returns the system metric if available, or falls back to the provided default.
    #[must_use]
16
    pub fn resolve(
16
        &self,
16
        system_metrics: &crate::system::SystemMetrics,
16
        fallback: PixelValue,
16
    ) -> PixelValue {
16
        match self {
3
            Self::Value(v) => *v,
13
            Self::System(ref_type) => ref_type.resolve(system_metrics).unwrap_or(fallback),
        }
16
    }
}
impl fmt::Display for PixelValueOrSystem {
16
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16
        match self {
15
            Self::Value(v) => write!(f, "{v}"),
1
            Self::System(s) => write!(f, "{s}"),
        }
16
    }
}
impl FormatAsCssValue for PixelValueOrSystem {
2
    fn format_as_css_value(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2
        match self {
1
            Self::Value(v) => v.format_as_css_value(f),
1
            Self::System(s) => s.format_as_css_value(f),
        }
2
    }
}
/// Parse a pixel value that may include system metric references.
///
/// Accepts: `10px`, `1.5em`, `system:button-padding`, etc.
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `pixel-value-or-system` value.
35
pub fn parse_pixel_value_or_system(
35
    input: &str,
35
) -> Result<PixelValueOrSystem, CssPixelValueParseError<'_>> {
35
    let input = input.trim();
    // Check for system metric reference
35
    if let Some(metric_name) = input.strip_prefix("system:") {
30
        if let Some(metric_ref) = SystemMetricRef::from_css_str(metric_name) {
22
            return Ok(PixelValueOrSystem::System(metric_ref));
8
        }
8
        return Err(CssPixelValueParseError::InvalidPixelValue(input));
5
    }
    // Parse as regular pixel value
5
    Ok(PixelValueOrSystem::Value(parse_pixel_value(input)?))
35
}
#[cfg(all(test, feature = "parser"))]
mod tests {
    // Tests assert that parsed values equal the exact source literals.
    #![allow(clippy::float_cmp)]
    use super::*;
    #[test]
1
    fn test_parse_pixel_value() {
1
        assert_eq!(parse_pixel_value("10px").unwrap(), PixelValue::px(10.0));
1
        assert_eq!(parse_pixel_value("1.5em").unwrap(), PixelValue::em(1.5));
1
        assert_eq!(parse_pixel_value("2rem").unwrap(), PixelValue::rem(2.0));
1
        assert_eq!(parse_pixel_value("-20pt").unwrap(), PixelValue::pt(-20.0));
1
        assert_eq!(parse_pixel_value("50%").unwrap(), PixelValue::percent(50.0));
1
        assert_eq!(parse_pixel_value("1in").unwrap(), PixelValue::inch(1.0));
1
        assert_eq!(parse_pixel_value("2.54cm").unwrap(), PixelValue::cm(2.54));
1
        assert_eq!(parse_pixel_value("10mm").unwrap(), PixelValue::mm(10.0));
1
        assert_eq!(parse_pixel_value("  0  ").unwrap(), PixelValue::px(0.0));
1
    }
    #[test]
1
    fn test_resolve_with_context_em() {
        // Element has font-size: 32px, margin: 0.67em
1
        let context = ResolutionContext {
1
            vertical_writing_mode: false,
1
            element_font_size: 32.0,
1
            parent_font_size: 16.0,
1
            ..Default::default()
1
        };
        // Margin em uses element's own font-size
1
        let margin = PixelValue::em(0.67);
1
        assert!(
1
            (margin.resolve_with_context(&context, PropertyContext::Margin) - 21.44).abs() < 0.01
        );
        // Font-size em uses parent's font-size
1
        let font_size = PixelValue::em(2.0);
1
        assert_eq!(
1
            font_size.resolve_with_context(&context, PropertyContext::FontSize),
            32.0
        );
1
    }
    #[test]
1
    fn test_resolve_with_context_rem() {
        // Root has font-size: 18px
1
        let context = ResolutionContext {
1
            vertical_writing_mode: false,
1
            element_font_size: 32.0,
1
            parent_font_size: 16.0,
1
            root_font_size: 18.0,
1
            ..Default::default()
1
        };
        // Rem always uses root font-size, regardless of property
1
        let margin = PixelValue::rem(2.0);
1
        assert_eq!(
1
            margin.resolve_with_context(&context, PropertyContext::Margin),
            36.0
        );
1
        let font_size = PixelValue::rem(1.5);
1
        assert_eq!(
1
            font_size.resolve_with_context(&context, PropertyContext::FontSize),
            27.0
        );
1
    }
    #[test]
1
    fn test_resolve_with_context_percent_margin() {
        // Margin % uses containing block WIDTH (even for top/bottom!)
1
        let context = ResolutionContext {
1
            vertical_writing_mode: false,
1
            element_font_size: 16.0,
1
            parent_font_size: 16.0,
1
            root_font_size: 16.0,
1
            containing_block_size: PhysicalSize::new(800.0, 600.0),
1
            element_size: None,
1
            viewport_size: PhysicalSize::new(1920.0, 1080.0),
1
        };
1
        let margin = PixelValue::percent(10.0); // 10%
1
        assert_eq!(
1
            margin.resolve_with_context(&context, PropertyContext::Margin),
            80.0
        ); // 10% of 800
1
    }
    #[test]
1
    fn test_parse_pixel_value_no_percent() {
1
        assert_eq!(
1
            parse_pixel_value_no_percent("10px").unwrap().inner,
1
            PixelValue::px(10.0)
        );
1
        assert!(parse_pixel_value_no_percent("50%").is_err());
1
    }
    #[test]
1
    fn test_parse_pixel_value_with_auto() {
1
        assert_eq!(
1
            parse_pixel_value_with_auto("10px").unwrap(),
1
            PixelValueWithAuto::Exact(PixelValue::px(10.0))
        );
1
        assert_eq!(
1
            parse_pixel_value_with_auto("auto").unwrap(),
            PixelValueWithAuto::Auto
        );
1
        assert_eq!(
1
            parse_pixel_value_with_auto("initial").unwrap(),
            PixelValueWithAuto::Initial
        );
1
        assert_eq!(
1
            parse_pixel_value_with_auto("inherit").unwrap(),
            PixelValueWithAuto::Inherit
        );
1
        assert_eq!(
1
            parse_pixel_value_with_auto("none").unwrap(),
            PixelValueWithAuto::None
        );
1
    }
    #[test]
1
    fn test_parse_pixel_value_errors() {
1
        assert!(parse_pixel_value("").is_err());
        // Modern CSS parsers can be liberal - unitless numbers treated as px
1
        assert!(parse_pixel_value("10").is_ok()); // Parsed as 10px
                                                  // This parser is liberal and trims whitespace, so
                                                  // "10 px" is accepted
1
        assert!(parse_pixel_value("10 px").is_ok()); // Liberal parsing accepts this
1
        assert!(parse_pixel_value("px").is_err());
1
        assert!(parse_pixel_value("ten-px").is_err());
1
    }
}
#[cfg(test)]
#[allow(
    clippy::float_cmp,
    clippy::unreadable_literal,
    clippy::cast_precision_loss,
    clippy::too_many_lines,
    clippy::excessive_precision
)]
mod autotest_generated {
    use std::{
        collections::hash_map::DefaultHasher,
        hash::{Hash, Hasher},
    };
    use super::*;
    use crate::{
        codegen::format::FormatAsRustCode,
        css::PrintAsCssValue,
        props::{
            basic::length::{FloatValue, SizeMetric},
            formatter::FormatAsCssValue,
        },
        system::{SafeAreaInsets, SystemMetrics, TitlebarMetrics},
    };
    /// `FloatValue` stores `f32 * 1000` truncated into an `isize`, so every value
    /// is quantized to 1/1000 and every `get()` is finite by construction.
    const MULT: f32 = 1000.0;
    /// `const_new` multiplies by 1000 in `isize` space, so anything beyond this
    /// overflows the multiply (debug-panics / release-wraps). The `const_*`
    /// constructors are only usable up to here.
    const MAX_SAFE_CONST: isize = isize::MAX / 1000;
    const MIN_SAFE_CONST: isize = isize::MIN / 1000;
    const ALL_METRICS: [SizeMetric; 12] = [
        SizeMetric::Px,
        SizeMetric::Pt,
        SizeMetric::Em,
        SizeMetric::Rem,
        SizeMetric::In,
        SizeMetric::Cm,
        SizeMetric::Mm,
        SizeMetric::Percent,
        SizeMetric::Vw,
        SizeMetric::Vh,
        SizeMetric::Vmin,
        SizeMetric::Vmax,
    ];
    const ALL_PROPERTY_CONTEXTS: [PropertyContext; 9] = [
        PropertyContext::FontSize,
        PropertyContext::Margin,
        PropertyContext::Padding,
        PropertyContext::Width,
        PropertyContext::Height,
        PropertyContext::BorderWidth,
        PropertyContext::BorderRadius,
        PropertyContext::Transform,
        PropertyContext::Other,
    ];
    const ALL_SYSTEM_REFS: [SystemMetricRef; 11] = [
        SystemMetricRef::ButtonRadius,
        SystemMetricRef::ButtonPaddingHorizontal,
        SystemMetricRef::ButtonPaddingVertical,
        SystemMetricRef::ButtonBorderWidth,
        SystemMetricRef::TitlebarHeight,
        SystemMetricRef::TitlebarButtonWidth,
        SystemMetricRef::TitlebarPadding,
        SystemMetricRef::SafeAreaTop,
        SystemMetricRef::SafeAreaBottom,
        SystemMetricRef::SafeAreaLeft,
        SystemMetricRef::SafeAreaRight,
    ];
    /// The values that historically break fixed-point encoders.
    const EXTREME_F32: [f32; 13] = [
        0.0,
        -0.0,
        1.0,
        -1.0,
        f32::MIN_POSITIVE,
        -f32::MIN_POSITIVE,
        1e30,
        -1e30,
        f32::MAX,
        f32::MIN,
        f32::INFINITY,
        f32::NEG_INFINITY,
        f32::NAN,
    ];
    fn approx(a: f32, b: f32) -> bool {
        (a - b).abs() < 0.001
    }
    fn hash_of<T: Hash>(v: &T) -> u64 {
        let mut h = DefaultHasher::new();
        v.hash(&mut h);
        h.finish()
    }
    /// Renders anything through the `FormatAsCssValue` impl, which is otherwise
    /// only reachable with a live `Formatter`.
    struct CssVal<T>(T);
    impl<T: FormatAsCssValue> fmt::Display for CssVal<T> {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            self.0.format_as_css_value(f)
        }
    }
    fn as_css_value<T: FormatAsCssValue>(v: T) -> String {
        CssVal(v).to_string()
    }
    /// A context whose every reference value is distinct, so a resolver that
    /// reads the wrong field cannot accidentally produce the right number.
    fn distinct_context() -> ResolutionContext {
        ResolutionContext {
            vertical_writing_mode: false,
            element_font_size: 32.0,
            parent_font_size: 8.0,
            root_font_size: 4.0,
            containing_block_size: PhysicalSize::new(800.0, 600.0),
            element_size: Some(PhysicalSize::new(200.0, 100.0)),
            viewport_size: PhysicalSize::new(1000.0, 500.0),
        }
    }
    fn populated_metrics() -> SystemMetrics {
        SystemMetrics {
            corner_radius: OptionPixelValue::Some(PixelValue::px(1.0)),
            border_width: OptionPixelValue::Some(PixelValue::px(2.0)),
            button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(3.0)),
            button_padding_vertical: OptionPixelValue::Some(PixelValue::px(4.0)),
            titlebar: TitlebarMetrics {
                height: OptionPixelValue::Some(PixelValue::px(5.0)),
                button_area_width: OptionPixelValue::Some(PixelValue::px(6.0)),
                padding_horizontal: OptionPixelValue::Some(PixelValue::px(7.0)),
                safe_area: SafeAreaInsets {
                    top: OptionPixelValue::Some(PixelValue::px(8.0)),
                    bottom: OptionPixelValue::Some(PixelValue::px(9.0)),
                    left: OptionPixelValue::Some(PixelValue::px(10.0)),
                    right: OptionPixelValue::Some(PixelValue::px(11.0)),
                    // No keyboard inset at this site: a titlebar/desktop
                    // surface never has an on-screen keyboard over it.
                    keyboard: OptionPixelValue::None,
                },
                ..TitlebarMetrics::default()
            },
        }
    }
    // ============================================================== parsers ===
    #[test]
    fn parse_pixel_value_rejects_empty_and_whitespace_only() {
        assert_eq!(
            parse_pixel_value("").unwrap_err(),
            CssPixelValueParseError::EmptyString
        );
        for ws in ["   ", "\t\n", "\r\n\t ", "\n"] {
            assert_eq!(
                parse_pixel_value(ws).unwrap_err(),
                CssPixelValueParseError::EmptyString,
                "whitespace-only input {ws:?} must trim down to EmptyString"
            );
        }
    }
    #[test]
    fn parse_pixel_value_rejects_a_bare_unit_with_no_number() {
        // Every suffix that is reachable as a bare token must report NoValueGiven
        // (i.e. "the unit is fine, the number is missing") rather than panicking.
        // "vmin" is deliberately absent — see the vmin-shadowing test below.
        for unit in [
            "px", "rem", "em", "pt", "in", "mm", "cm", "vmax", "vw", "vh", "%",
        ] {
            let err = parse_pixel_value(unit).unwrap_err();
            assert!(
                matches!(err, CssPixelValueParseError::NoValueGiven(input, _) if input == unit),
                "bare unit {unit:?} should be NoValueGiven, got {err:?}"
            );
        }
        // Whitespace between the (missing) number and the unit is trimmed too.
        assert!(matches!(
            parse_pixel_value("   px").unwrap_err(),
            CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
        ));
    }
    #[test]
    fn parse_pixel_value_vmin_is_shadowed_by_the_in_suffix() {
        // FIXED (was a characterization of the bug): the suffix table used to test
        // "in" BEFORE "vmin", so "5vmin" stripped the "in" and failed to parse the
        // "5vm" remainder — every vmin length in a stylesheet was rejected. The table
        // now orders "vmin"/"vmax" ahead of "in".
        assert_eq!(
            parse_pixel_value("5vmin").unwrap(),
            PixelValue::from_metric(SizeMetric::Vmin, 5.0)
        );
        // The bare unit now reports NoValueGiven like every other bare unit, instead
        // of the bogus "vm" ValueParseErr.
        assert!(matches!(
            parse_pixel_value("vmin").unwrap_err(),
            CssPixelValueParseError::NoValueGiven(..)
        ));
        // The sibling viewport units are all fine.
        assert_eq!(
            parse_pixel_value("5vmax").unwrap(),
            PixelValue::from_metric(SizeMetric::Vmax, 5.0)
        );
        assert_eq!(
            parse_pixel_value("5vw").unwrap(),
            PixelValue::from_metric(SizeMetric::Vw, 5.0)
        );
        assert_eq!(
            parse_pixel_value("5vh").unwrap(),
            PixelValue::from_metric(SizeMetric::Vh, 5.0)
        );
    }
    #[test]
    fn parse_pixel_value_inner_proves_the_vmin_bug_is_pure_suffix_ordering() {
        // Same input, same two units, only the table order differs. This pins the
        // fix: move "vmin"/"vmax" ahead of "in" (and "em"/"rem" style ordering).
        let in_first: [(&'static str, SizeMetric); 2] =
            [("in", SizeMetric::In), ("vmin", SizeMetric::Vmin)];
        let vmin_first: [(&'static str, SizeMetric); 2] =
            [("vmin", SizeMetric::Vmin), ("in", SizeMetric::In)];
        assert!(parse_pixel_value_inner("5vmin", &in_first).is_err());
        assert_eq!(
            parse_pixel_value_inner("5vmin", &vmin_first).unwrap(),
            PixelValue::from_metric(SizeMetric::Vmin, 5.0)
        );
        // ...and the reordering does not break plain inches.
        assert_eq!(
            parse_pixel_value_inner("5in", &vmin_first).unwrap(),
            PixelValue::inch(5.0)
        );
    }
    #[test]
    fn parse_pixel_value_inner_with_an_empty_table_falls_back_to_unitless_px() {
        let empty: [(&'static str, SizeMetric); 0] = [];
        // No suffix table -> only a bare float is acceptable, and it means px.
        assert_eq!(
            parse_pixel_value_inner("10", &empty).unwrap(),
            PixelValue::px(10.0)
        );
        assert!(matches!(
            parse_pixel_value_inner("10px", &empty).unwrap_err(),
            CssPixelValueParseError::InvalidPixelValue("10px")
        ));
        assert_eq!(
            parse_pixel_value_inner("", &empty).unwrap_err(),
            CssPixelValueParseError::EmptyString
        );
    }
    #[test]
    fn parse_pixel_value_accepts_every_unit_it_advertises() {
        // Positive controls, including the liberal shapes this parser allows.
        let cases: [(&str, PixelValue); 12] = [
            ("10px", PixelValue::px(10.0)),
            ("1.5em", PixelValue::em(1.5)),
            ("2rem", PixelValue::rem(2.0)),
            ("-20pt", PixelValue::pt(-20.0)),
            ("50%", PixelValue::percent(50.0)),
            ("1in", PixelValue::inch(1.0)),
            ("2.54cm", PixelValue::cm(2.54)),
            ("10mm", PixelValue::mm(10.0)),
            ("+7px", PixelValue::px(7.0)),
            (".5px", PixelValue::px(0.5)),
            ("5.px", PixelValue::px(5.0)),
            ("1e2px", PixelValue::px(100.0)),
        ];
        for (input, expected) in cases {
            assert_eq!(
                parse_pixel_value(input).unwrap(),
                expected,
                "parsing {input:?}"
            );
        }
        // Unitless numbers mean px, and interior/exterior whitespace is trimmed.
        assert_eq!(parse_pixel_value("  0  ").unwrap(), PixelValue::px(0.0));
        assert_eq!(parse_pixel_value("10 px").unwrap(), PixelValue::px(10.0));
        assert_eq!(parse_pixel_value("\t10px\n").unwrap(), PixelValue::px(10.0));
    }
    #[test]
    fn parse_pixel_value_boundary_numbers_saturate_instead_of_overflowing() {
        // Signed zero collapses onto a single encoding.
        assert_eq!(parse_pixel_value("-0").unwrap(), PixelValue::px(0.0));
        assert_eq!(parse_pixel_value("-0").unwrap(), PixelValue::zero());
        // Anything under 1/1000 of a unit quantizes away to exactly zero.
        assert_eq!(parse_pixel_value("0.0004px").unwrap(), PixelValue::px(0.0));
        assert_eq!(parse_pixel_value("-0.0009px").unwrap(), PixelValue::px(0.0));
        assert_eq!(parse_pixel_value("1e-40px").unwrap(), PixelValue::px(0.0));
        // Values far past f32/isize range saturate; `get()` stays finite.
        for huge in ["9223372036854775807", "1e40px", "3.5e38"] {
            let v = parse_pixel_value(huge).unwrap();
            assert!(
                v.number.get().is_finite(),
                "{huge:?} leaked a non-finite value: {}",
                v.number.get()
            );
            assert!(v.number.get() > 0.0, "{huge:?} lost its sign");
        }
        let neg = parse_pixel_value("-1e40px").unwrap();
        assert!(neg.number.get().is_finite() && neg.number.get() < 0.0);
    }
    #[test]
    fn parse_pixel_value_inherits_rusts_float_keywords() {
        // BUG-adjacent (spec conformance, characterized): `str::parse::<f32>`
        // accepts "NaN"/"infinity", so CSS that no browser would accept is taken
        // here. NaN sanitizes to 0px and infinity saturates, so nothing downstream
        // sees a non-finite length -- but neither input should have parsed at all.
        assert_eq!(parse_pixel_value("NaN").unwrap(), PixelValue::zero());
        let inf = parse_pixel_value("infinity").unwrap();
        assert_eq!(inf, PixelValue::px(f32::INFINITY));
        assert!(inf.number.get().is_finite() && inf.number.get() > 0.0);
        let neg_inf = parse_pixel_value("-infinity").unwrap();
        assert_eq!(neg_inf, PixelValue::px(f32::NEG_INFINITY));
        assert!(neg_inf.number.get().is_finite() && neg_inf.number.get() < 0.0);
        // "inf" is accepted too. The "in" (inches) suffix does NOT eat it: a suffix
        // match needs the string to END in "in", and "inf" ends in "nf" -- so it
        // falls through to the same `str::parse::<f32>()` path as "infinity" above.
        let inf_short = parse_pixel_value("inf").unwrap();
        assert_eq!(inf_short, PixelValue::px(f32::INFINITY));
        assert!(inf_short.number.get().is_finite() && inf_short.number.get() > 0.0);
    }
    #[test]
    fn parse_pixel_value_is_case_sensitive_about_units() {
        // Conformance gap (characterized): CSS units are case-insensitive
        // ("10PX" is valid CSS), but the suffix table only matches lowercase, so
        // these fall through to the float parser and are rejected outright.
        for input in ["10PX", "10Px", "10EM", "10REM", "10VMAX"] {
            let err = parse_pixel_value(input).unwrap_err();
            assert!(
                matches!(err, CssPixelValueParseError::InvalidPixelValue(s) if s == input),
                "uppercase unit {input:?} should be InvalidPixelValue, got {err:?}"
            );
        }
    }
    #[test]
    fn parse_pixel_value_rejects_garbage_and_trailing_junk() {
        for input in [
            "ten-px",
            "px10",
            "10px;garbage",
            "10;",
            "--",
            "1%%",
            "10 20px",
            "#",
            "10px 10px",
            "e",
            "0x10px",
        ] {
            assert!(
                parse_pixel_value(input).is_err(),
                "{input:?} must not parse, got {:?}",
                parse_pixel_value(input)
            );
        }
    }
    #[test]
    fn parse_pixel_value_survives_unicode() {
        // Multibyte input must never slice mid-codepoint or panic.
        for input in [
            "\u{1F600}",          // emoji alone
            "10px\u{1F600}",      // emoji suffix
            "10px\u{0301}",       // combining acute after the unit
            "\u{200B}10px",       // zero-width space (NOT trimmable whitespace)
            "\u{0661}\u{0660}px", // arabic-indic digits
            "10\u{0440}\u{0445}", // cyrillic look-alike of "px"
            "\u{202E}10px",       // RTL override
        ] {
            let got = parse_pixel_value(input);
            assert!(got.is_err(), "{input:?} must be rejected, got {got:?}");
        }
        // The zero-width space specifically survives the trim and lands in the
        // reported remainder, which proves no byte-level slicing happened.
        assert!(matches!(
            parse_pixel_value("\u{200B}10px").unwrap_err(),
            CssPixelValueParseError::ValueParseErr(_, "\u{200B}10")
        ));
    }
    #[test]
    fn parse_pixel_value_handles_extremely_long_and_deeply_nested_input() {
        // 100k digits: must terminate quickly and saturate, not hang or overflow.
        let long_number = format!("{}px", "9".repeat(100_000));
        let parsed = parse_pixel_value(&long_number).unwrap();
        assert!(parsed.number.get().is_finite());
        assert_eq!(parsed.metric, SizeMetric::Px);
        // 100k junk bytes: rejected, no quadratic blow-up.
        let long_junk = "x".repeat(100_000);
        assert!(parse_pixel_value(&long_junk).is_err());
        // 10k nested brackets: this parser is not recursive, so this must be a
        // plain rejection rather than a stack overflow.
        let nested = "(".repeat(10_000);
        assert!(matches!(
            parse_pixel_value(&nested).unwrap_err(),
            CssPixelValueParseError::InvalidPixelValue(_)
        ));
    }
    #[test]
    fn parse_pixel_value_no_percent_rejects_percentages_but_keeps_the_rest() {
        assert_eq!(
            parse_pixel_value_no_percent("10px").unwrap().inner,
            PixelValue::px(10.0)
        );
        assert_eq!(
            parse_pixel_value_no_percent("5vmax").unwrap().inner,
            PixelValue::from_metric(SizeMetric::Vmax, 5.0)
        );
        // "%" is not in the table, so it falls through to the float parser.
        assert!(matches!(
            parse_pixel_value_no_percent("50%").unwrap_err(),
            CssPixelValueParseError::InvalidPixelValue("50%")
        ));
        assert!(matches!(
            parse_pixel_value_no_percent("%").unwrap_err(),
            CssPixelValueParseError::InvalidPixelValue("%")
        ));
        assert_eq!(
            parse_pixel_value_no_percent("").unwrap_err(),
            CssPixelValueParseError::EmptyString
        );
        assert_eq!(
            parse_pixel_value_no_percent("   ").unwrap_err(),
            CssPixelValueParseError::EmptyString
        );
        assert!(parse_pixel_value_no_percent("\u{1F600}").is_err());
        // FIXED: "5vmin" now parses (the suffix table orders "vmin" before "in").
        assert_eq!(
            parse_pixel_value_no_percent("5vmin").unwrap().inner,
            PixelValue::from_metric(SizeMetric::Vmin, 5.0)
        );
    }
    #[test]
    fn parse_pixel_value_with_auto_keywords_and_fallthrough() {
        assert_eq!(
            parse_pixel_value_with_auto("auto").unwrap(),
            PixelValueWithAuto::Auto
        );
        assert_eq!(
            parse_pixel_value_with_auto("  initial  ").unwrap(),
            PixelValueWithAuto::Initial
        );
        assert_eq!(
            parse_pixel_value_with_auto("\tinherit\n").unwrap(),
            PixelValueWithAuto::Inherit
        );
        assert_eq!(
            parse_pixel_value_with_auto("none").unwrap(),
            PixelValueWithAuto::None
        );
        assert_eq!(
            parse_pixel_value_with_auto("10px").unwrap(),
            PixelValueWithAuto::Exact(PixelValue::px(10.0))
        );
        // Keywords are matched case-sensitively (CSS says they should not be).
        for input in ["AUTO", "Auto", "INHERIT", "None"] {
            assert!(
                parse_pixel_value_with_auto(input).is_err(),
                "{input:?} unexpectedly matched a keyword"
            );
        }
        // Empty / junk / unicode all funnel into the pixel-value errors.
        assert_eq!(
            parse_pixel_value_with_auto("").unwrap_err(),
            CssPixelValueParseError::EmptyString
        );
        assert_eq!(
            parse_pixel_value_with_auto(" \t ").unwrap_err(),
            CssPixelValueParseError::EmptyString
        );
        assert!(parse_pixel_value_with_auto("auto;garbage").is_err());
        assert!(parse_pixel_value_with_auto("\u{1F600}").is_err());
        assert!(parse_pixel_value_with_auto(&"(".repeat(10_000)).is_err());
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_pixel_value_or_system_accepts_every_system_ref() {
        for r in ALL_SYSTEM_REFS {
            let css = r.as_css_str(); // already carries the "system:" prefix
            assert_eq!(
                parse_pixel_value_or_system(css).unwrap(),
                PixelValueOrSystem::System(r),
                "round-tripping {css:?}"
            );
            // Surrounding whitespace is trimmed before the prefix check.
            assert_eq!(
                parse_pixel_value_or_system(&format!("  {css}  ")).unwrap(),
                PixelValueOrSystem::System(r)
            );
        }
        // Plain lengths still work.
        assert_eq!(
            parse_pixel_value_or_system("10px").unwrap(),
            PixelValueOrSystem::Value(PixelValue::px(10.0))
        );
        assert_eq!(
            parse_pixel_value_or_system("1.5em").unwrap(),
            PixelValueOrSystem::Value(PixelValue::em(1.5))
        );
    }
    #[cfg(feature = "parser")]
    #[test]
    fn parse_pixel_value_or_system_rejects_malformed_system_refs() {
        // DOC BUG (characterized): the doc comment on `parse_pixel_value_or_system`
        // and on `PixelValueOrSystem` advertises `system:button-padding`, but
        // `from_css_str` only knows the -horizontal / -vertical spellings, so the
        // documented example is rejected.
        assert!(matches!(
            parse_pixel_value_or_system("system:button-padding").unwrap_err(),
            CssPixelValueParseError::InvalidPixelValue("system:button-padding")
        ));
        for input in [
            "system:",                // empty metric name
            "system:unknown",         // unknown metric
            "system: button-radius",  // no inner trim after the colon
            "system:BUTTON-RADIUS",   // case-sensitive
            "system:button-radius;x", // trailing junk
            "system:\u{1F600}",       // unicode metric name
        ] {
            let err = parse_pixel_value_or_system(input).unwrap_err();
            assert!(
                matches!(err, CssPixelValueParseError::InvalidPixelValue(s) if s == input),
                "{input:?} should be InvalidPixelValue, got {err:?}"
            );
        }
        // Without the exact lowercase prefix it is treated as a length, and fails
        // as one.
        assert!(matches!(
            parse_pixel_value_or_system("SYSTEM:button-radius").unwrap_err(),
            CssPixelValueParseError::InvalidPixelValue("SYSTEM:button-radius")
        ));
        assert_eq!(
            parse_pixel_value_or_system("").unwrap_err(),
            CssPixelValueParseError::EmptyString
        );
        assert_eq!(
            parse_pixel_value_or_system("   ").unwrap_err(),
            CssPixelValueParseError::EmptyString
        );
        // A pathologically long metric name must be rejected, not hang.
        let long = format!("system:{}", "a".repeat(100_000));
        assert!(parse_pixel_value_or_system(&long).is_err());
    }
    // ============================================== parse-error round-trips ===
    #[test]
    fn parse_errors_survive_the_owned_round_trip() {
        let float_err = "x".parse::<f32>().unwrap_err();
        let errors = [
            CssPixelValueParseError::EmptyString,
            CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px),
            CssPixelValueParseError::ValueParseErr(float_err, "abc"),
            CssPixelValueParseError::InvalidPixelValue("ten-px"),
        ];
        for err in errors {
            let owned = err.to_contained();
            let shared = owned.to_shared();
            assert_eq!(shared, err, "to_contained -> to_shared must be lossless");
            // Display must survive too, and never be empty.
            assert!(!err.to_string().is_empty());
            assert_eq!(shared.to_string(), err.to_string());
        }
    }
    #[test]
    fn parse_errors_round_trip_from_real_parse_failures() {
        // The same path, but with errors that actually came out of the parser
        // (including a unicode-bearing remainder).
        for input in ["", "px", "\u{200B}10px", "ten-px", "%"] {
            let err = parse_pixel_value(input).unwrap_err();
            let owned = err.to_contained();
            assert_eq!(owned.to_shared(), err, "round-trip failed for {input:?}");
        }
    }
    // ============================================================= numeric ===
    #[test]
    fn float_constructors_never_leak_a_non_finite_value() {
        // NaN must sanitize to 0 and infinities must saturate: `FloatValue`
        // decodes through an isize, so `get()` is finite for *every* input.
        type Ctor = (fn(f32) -> PixelValue, SizeMetric);
        let ctors: [Ctor; 8] = [
            (PixelValue::px, SizeMetric::Px),
            (PixelValue::em, SizeMetric::Em),
            (PixelValue::pt, SizeMetric::Pt),
            (PixelValue::inch, SizeMetric::In),
            (PixelValue::cm, SizeMetric::Cm),
            (PixelValue::mm, SizeMetric::Mm),
            (PixelValue::percent, SizeMetric::Percent),
            (PixelValue::rem, SizeMetric::Rem),
        ];
        for (ctor, metric) in ctors {
            for v in EXTREME_F32 {
                let px = ctor(v);
                assert_eq!(px.metric, metric, "constructor lost its metric for {v}");
                assert!(
                    px.number.get().is_finite(),
                    "{metric:?} constructor leaked a non-finite value for input {v}"
                );
            }
            assert_eq!(ctor(f32::NAN).number.get(), 0.0, "NaN must sanitize to 0");
            assert!(ctor(f32::INFINITY).number.get() > 0.0);
            assert!(ctor(f32::NEG_INFINITY).number.get() < 0.0);
        }
        // from_metric agrees with the named constructors for every metric.
        for metric in ALL_METRICS {
            for v in EXTREME_F32 {
                let px = PixelValue::from_metric(metric, v);
                assert_eq!(px.metric, metric);
                assert!(px.number.get().is_finite());
            }
            assert_eq!(
                PixelValue::from_metric(metric, 12.0),
                PixelValue {
                    metric,
                    number: FloatValue::new(12.0)
                }
            );
        }
    }
    #[test]
    fn float_constructors_quantize_to_one_thousandth() {
        // Sub-milli magnitudes vanish entirely...
        assert_eq!(PixelValue::px(0.0004).number.get(), 0.0);
        assert_eq!(PixelValue::px(-0.0009).number.get(), 0.0);
        // ...and the excess precision above 1/1000 is dropped, not rounded up.
        assert_eq!(PixelValue::px(1.0005).number.get(), 1.0);
        // Signed zero normalizes, which keeps Eq/Hash total (PixelValue is Eq +
        // Hash despite wrapping a float).
        assert_eq!(PixelValue::px(-0.0), PixelValue::px(0.0));
        assert_eq!(
            hash_of(&PixelValue::px(-0.0)),
            hash_of(&PixelValue::px(0.0))
        );
        // Even NaN is reflexive here, because it sanitizes to the zero encoding.
        assert_eq!(PixelValue::px(f32::NAN), PixelValue::px(f32::NAN));
        assert_eq!(PixelValue::px(f32::NAN), PixelValue::zero());
    }
    #[test]
    fn const_constructors_agree_with_their_float_twins() {
        assert_eq!(PixelValue::const_px(5), PixelValue::px(5.0));
        assert_eq!(PixelValue::const_em(5), PixelValue::em(5.0));
        assert_eq!(PixelValue::const_pt(5), PixelValue::pt(5.0));
        assert_eq!(PixelValue::const_percent(5), PixelValue::percent(5.0));
        assert_eq!(PixelValue::const_in(5), PixelValue::inch(5.0));
        assert_eq!(PixelValue::const_cm(5), PixelValue::cm(5.0));
        assert_eq!(PixelValue::const_mm(5), PixelValue::mm(5.0));
        assert_eq!(PixelValue::const_px(0), PixelValue::zero());
        assert_eq!(PixelValue::const_px(-7), PixelValue::px(-7.0));
        for metric in ALL_METRICS {
            assert_eq!(
                PixelValue::const_from_metric(metric, 7),
                PixelValue::from_metric(metric, 7.0),
                "const_from_metric disagrees with from_metric for {metric:?}"
            );
            assert_eq!(
                PixelValue::const_from_metric(metric, -7),
                PixelValue::from_metric(metric, -7.0)
            );
        }
    }
    #[test]
    fn const_constructors_are_usable_up_to_the_documented_isize_bound() {
        // `const_new` scales by 1000 in isize space, so MAX_SAFE_CONST is the
        // largest input that does not overflow the multiply. (Anything beyond it
        // -- e.g. `const_px(isize::MAX)` -- overflows: a debug panic and a
        // release wrap. Not exercised here because the two builds disagree.)
        for v in [0, 1, -1, MAX_SAFE_CONST, MIN_SAFE_CONST] {
            let px = PixelValue::const_px(v);
            assert!(
                px.number.get().is_finite(),
                "const_px({v}) leaked a non-finite value"
            );
        }
        assert!(PixelValue::const_px(MAX_SAFE_CONST).number.get() > 0.0);
        assert!(PixelValue::const_px(MIN_SAFE_CONST).number.get() < 0.0);
        assert_eq!(
            PixelValue::const_px(MAX_SAFE_CONST).number.number(),
            MAX_SAFE_CONST * 1000
        );
    }
    #[test]
    fn const_fractional_constructors_match_their_documented_examples() {
        // The doc-comment examples on `const_em_fractional`.
        assert!(approx(
            PixelValue::const_em_fractional(1, 5).number.get(),
            1.5
        ));
        assert!(approx(
            PixelValue::const_em_fractional(0, 83).number.get(),
            0.83
        ));
        assert!(approx(
            PixelValue::const_em_fractional(1, 17).number.get(),
            1.17
        ));
        assert_eq!(PixelValue::const_em_fractional(1, 5).metric, SizeMetric::Em);
        assert_eq!(PixelValue::const_pt_fractional(1, 5).metric, SizeMetric::Pt);
        assert!(approx(
            PixelValue::const_pt_fractional(2, 25).number.get(),
            2.25
        ));
        // Zero fraction, and the negative case (the sign must reach the fraction:
        // -1.5, not -1 + 0.5 = -0.5).
        assert_eq!(
            PixelValue::const_from_metric_fractional(SizeMetric::Px, 0, 0),
            PixelValue::zero()
        );
        assert!(approx(
            PixelValue::const_from_metric_fractional(SizeMetric::Px, -1, 5)
                .number
                .get(),
            -1.5
        ));
        // More than 3 decimals truncates to 3 (documented), rather than
        // overflowing the fixed-point encoding.
        assert!(approx(
            PixelValue::const_from_metric_fractional(SizeMetric::Px, 1, 5234)
                .number
                .get(),
            1.523
        ));
        // A pathological fraction must still land somewhere finite. (isize::MIN is
        // NOT exercised: negating it overflows inside the digit-counting code.)
        let extreme = PixelValue::const_from_metric_fractional(SizeMetric::Px, 0, isize::MAX);
        assert!(extreme.number.get().is_finite());
    }
    #[test]
    fn scale_for_dpi_is_defined_for_every_scale_factor() {
        let mut doubled = PixelValue::px(10.0);
        doubled.scale_for_dpi(2.0);
        assert_eq!(doubled, PixelValue::px(20.0));
        // Scaling compounds (it is not idempotent) -- worth pinning, since a
        // double-applied DPI scale is a classic layout bug.
        doubled.scale_for_dpi(2.0);
        assert_eq!(doubled, PixelValue::px(40.0));
        let mut zeroed = PixelValue::em(3.0);
        zeroed.scale_for_dpi(0.0);
        assert_eq!(zeroed, PixelValue::em(0.0));
        assert_eq!(zeroed.metric, SizeMetric::Em, "metric must be preserved");
        let mut flipped = PixelValue::px(10.0);
        flipped.scale_for_dpi(-1.5);
        assert_eq!(flipped, PixelValue::px(-15.0));
        // NaN collapses to zero, infinities saturate -- never a non-finite length.
        let mut nan_scaled = PixelValue::px(10.0);
        nan_scaled.scale_for_dpi(f32::NAN);
        assert_eq!(nan_scaled.number.get(), 0.0);
        let mut inf_scaled = PixelValue::px(10.0);
        inf_scaled.scale_for_dpi(f32::INFINITY);
        assert!(inf_scaled.number.get().is_finite() && inf_scaled.number.get() > 0.0);
        let mut max_scaled = PixelValue::px(f32::MAX);
        max_scaled.scale_for_dpi(f32::MAX);
        assert!(max_scaled.number.get().is_finite());
        // The no-percent wrapper just delegates.
        let mut wrapped = PixelValueNoPercent::from(PixelValue::px(10.0));
        wrapped.scale_for_dpi(2.5);
        assert_eq!(wrapped.inner, PixelValue::px(25.0));
        let mut wrapped_nan = PixelValueNoPercent::from(PixelValue::px(10.0));
        wrapped_nan.scale_for_dpi(f32::NAN);
        assert_eq!(wrapped_nan.inner.number.get(), 0.0);
    }
    #[test]
    fn interpolate_within_one_metric_keeps_that_metric() {
        let a = PixelValue::em(1.0);
        let b = PixelValue::em(3.0);
        assert_eq!(a.interpolate(&b, 0.0), a);
        assert_eq!(a.interpolate(&b, 1.0), b);
        assert_eq!(a.interpolate(&b, 0.5), PixelValue::em(2.0));
        // Out-of-range t extrapolates rather than clamping.
        assert_eq!(a.interpolate(&b, 2.0), PixelValue::em(5.0));
        assert_eq!(a.interpolate(&b, -1.0), PixelValue::em(-1.0));
        // Percent stays percent (it is NOT converted to px on the same-metric path).
        let p = PixelValue::percent(0.0).interpolate(&PixelValue::percent(100.0), 0.5);
        assert_eq!(p, PixelValue::percent(50.0));
        assert_eq!(p.metric, SizeMetric::Percent);
        // Non-finite t sanitizes to the zero encoding instead of poisoning layout.
        let nan_t = a.interpolate(&b, f32::NAN);
        assert_eq!(nan_t.number.get(), 0.0);
        assert_eq!(nan_t.metric, SizeMetric::Em);
        assert!(a.interpolate(&b, f32::INFINITY).number.get().is_finite());
    }
    #[test]
    fn interpolate_across_metrics_falls_back_to_px() {
        // Mixed metrics resolve through `to_pixels_internal` with DEFAULT_FONT_SIZE.
        let from_px = PixelValue::px(0.0);
        let to_em = PixelValue::em(1.0); // 16px at the default font size
        let mid = from_px.interpolate(&to_em, 0.5);
        assert_eq!(mid.metric, SizeMetric::Px);
        assert!(approx(mid.number.get(), DEFAULT_FONT_SIZE / 2.0));
        assert!(approx(
            PixelValue::px(0.0)
                .interpolate(&PixelValue::pt(72.0), 1.0)
                .number
                .get(),
            96.0
        ));
        // Percent and every viewport unit resolve to 0px on this path, because the
        // fallback has no containing block and no viewport. Documented as an
        // "acceptable animation fallback" -- pinned so it stays deliberate.
        for metric in [
            SizeMetric::Percent,
            SizeMetric::Vw,
            SizeMetric::Vh,
            SizeMetric::Vmin,
            SizeMetric::Vmax,
        ] {
            let other = PixelValue::from_metric(metric, 50.0);
            let done = PixelValue::px(100.0).interpolate(&other, 1.0);
            assert_eq!(
                done,
                PixelValue::px(0.0),
                "{metric:?} should collapse to 0px on the cross-metric path"
            );
        }
        let nan_t = PixelValue::px(0.0).interpolate(&PixelValue::em(1.0), f32::NAN);
        assert_eq!(nan_t.number.get(), 0.0);
    }
    /// The gap this closes: an app handed an `OptionPixelValue` (which is what
    /// `get_safe_area_insets()` returns) had NO sanctioned way to reach a
    /// number. `p.number.get()` worked only because the caller happened to
    /// know the value was absolute - it reports `24` for `24em` just as
    /// readily, and no type says otherwise.
    #[test]
    fn to_pixels_absolute_resolves_absolute_units_and_refuses_relative_ones() {
        for (v, expected) in [
            (PixelValue::px(24.0), 24.0),
            (PixelValue::pt(12.0), 12.0 * PT_TO_PX),
            (PixelValue::from_metric(SizeMetric::In, 1.0), 96.0),
            (PixelValue::from_metric(SizeMetric::Cm, 2.54), 96.0),
            (PixelValue::from_metric(SizeMetric::Mm, 25.4), 96.0),
        ] {
            match v.to_pixels_absolute() {
                OptionF32::Some(px) => assert!(
                    (px - expected).abs() < 0.001,
                    "{v:?} resolved to {px}, expected {expected}"
                ),
                OptionF32::None => panic!("{v:?} is absolute and must resolve"),
            }
        }
    }
    /// A relative unit has no pixel value until something supplies the
    /// reference, so answering with a number would be INVENTING one - which is
    /// precisely how `to_pixels_internal` reports every viewport unit as 0.0.
    #[test]
    fn to_pixels_absolute_returns_none_for_every_context_dependent_unit() {
        for v in [
            PixelValue::em(2.0),
            PixelValue::rem(2.0),
            PixelValue::percent(50.0),
            PixelValue::from_metric(SizeMetric::Vw, 50.0),
            PixelValue::from_metric(SizeMetric::Vh, 50.0),
            PixelValue::from_metric(SizeMetric::Vmin, 50.0),
            PixelValue::from_metric(SizeMetric::Vmax, 50.0),
        ] {
            assert_eq!(
                v.to_pixels_absolute(),
                OptionF32::None,
                "{v:?} needs a context and must not answer with a number"
            );
        }
    }
    /// `is_absolute` is the predicate `to_pixels_absolute` is built on, so the
    /// two must never disagree - otherwise a value could claim to need no
    /// context and then refuse to resolve.
    #[test]
    fn is_absolute_agrees_with_to_pixels_absolute() {
        for v in [
            PixelValue::px(1.0),
            PixelValue::pt(1.0),
            PixelValue::from_metric(SizeMetric::In, 1.0),
            PixelValue::from_metric(SizeMetric::Cm, 1.0),
            PixelValue::from_metric(SizeMetric::Mm, 1.0),
            PixelValue::em(1.0),
            PixelValue::rem(1.0),
            PixelValue::percent(1.0),
            PixelValue::from_metric(SizeMetric::Vw, 1.0),
            PixelValue::from_metric(SizeMetric::Vh, 1.0),
            PixelValue::from_metric(SizeMetric::Vmin, 1.0),
            PixelValue::from_metric(SizeMetric::Vmax, 1.0),
        ] {
            assert_eq!(
                v.is_absolute(),
                v.to_pixels_absolute() != OptionF32::None,
                "{v:?}: is_absolute() and to_pixels_absolute() disagree"
            );
        }
    }
    /// The public `to_pixels` must be the same function the engine uses, not a
    /// second implementation that can drift from it.
    #[test]
    fn to_pixels_matches_the_internal_resolver() {
        for v in [
            PixelValue::px(3.0),
            PixelValue::em(3.0),
            PixelValue::rem(3.0),
            PixelValue::percent(50.0),
        ] {
            assert_eq!(
                v.to_pixels(200.0, 16.0, 10.0),
                v.to_pixels_internal(200.0, 16.0, 10.0),
                "{v:?} diverged from the internal resolver"
            );
        }
    }
    #[test]
    fn to_pixels_internal_converts_every_absolute_and_relative_unit() {
        assert_eq!(
            PixelValue::px(10.0).to_pixels_internal(0.0, 16.0, 16.0),
            10.0
        );
        assert!(approx(
            PixelValue::pt(72.0).to_pixels_internal(0.0, 16.0, 16.0),
            96.0
        ));
        assert!(approx(
            PixelValue::inch(1.0).to_pixels_internal(0.0, 16.0, 16.0),
            96.0
        ));
        assert!(approx(
            PixelValue::cm(2.54).to_pixels_internal(0.0, 16.0, 16.0),
            96.0
        ));
        assert!(approx(
            PixelValue::mm(25.4).to_pixels_internal(0.0, 16.0, 16.0),
            96.0
        ));
        assert_eq!(PT_TO_PX, 96.0 / 72.0);
        // em and rem read different resolves (this legacy path is the one that
        // historically conflated them, so pin that they are separate arguments).
        assert_eq!(
            PixelValue::em(2.0).to_pixels_internal(0.0, 10.0, 100.0),
            20.0
        );
        assert_eq!(
            PixelValue::rem(2.0).to_pixels_internal(0.0, 10.0, 100.0),
            200.0
        );
        // % divides by 100 exactly once (the double-division bug this module's
        // NormalizedPercentage exists to prevent).
        assert_eq!(
            PixelValue::percent(50.0).to_pixels_internal(800.0, 16.0, 16.0),
            400.0
        );
        assert_eq!(
            PixelValue::percent(0.0).to_pixels_internal(800.0, 16.0, 16.0),
            0.0
        );
        assert_eq!(
            PixelValue::percent(-50.0).to_pixels_internal(800.0, 16.0, 16.0),
            -400.0
        );
        // Viewport units have no viewport here, so they are defined as 0 -- even
        // when the caller passes perfectly good resolves.
        for metric in [
            SizeMetric::Vw,
            SizeMetric::Vh,
            SizeMetric::Vmin,
            SizeMetric::Vmax,
        ] {
            assert_eq!(
                PixelValue::from_metric(metric, 50.0).to_pixels_internal(800.0, 16.0, 16.0),
                0.0,
                "{metric:?} must resolve to 0 on the legacy path"
            );
        }
    }
    #[test]
    fn to_pixels_internal_non_finite_resolves_are_defined_not_panics() {
        // The *result* of this method is a raw f32 (it is not re-encoded), so it
        // can go non-finite. Assert it does so predictably rather than panicking.
        assert!(PixelValue::em(1.0)
            .to_pixels_internal(0.0, f32::NAN, 0.0)
            .is_nan());
        assert!(PixelValue::rem(1.0)
            .to_pixels_internal(0.0, 0.0, f32::INFINITY)
            .is_infinite());
        assert!(PixelValue::percent(50.0)
            .to_pixels_internal(f32::INFINITY, 16.0, 16.0)
            .is_infinite());
        assert!(PixelValue::percent(50.0)
            .to_pixels_internal(f32::NAN, 16.0, 16.0)
            .is_nan());
        // 0% of an infinite containing block is NaN, not 0 -- worth knowing.
        assert!(PixelValue::percent(0.0)
            .to_pixels_internal(f32::INFINITY, 16.0, 16.0)
            .is_nan());
        // A saturated length times a huge resolve overflows to +inf (no panic).
        assert!(PixelValue::em(f32::MAX)
            .to_pixels_internal(0.0, f32::MAX, 0.0)
            .is_infinite());
        // Absolute units are always finite, whatever the resolves are.
        for v in EXTREME_F32 {
            assert!(PixelValue::px(v)
                .to_pixels_internal(f32::NAN, f32::NAN, f32::NAN)
                .is_finite());
        }
    }
    #[test]
    fn pixel_value_no_percent_to_pixels_internal_zeroes_out_percentages() {
        assert_eq!(
            PixelValueNoPercent::from(PixelValue::px(10.0)).to_pixels_internal(16.0, 16.0),
            10.0
        );
        assert_eq!(
            PixelValueNoPercent::from(PixelValue::em(2.0)).to_pixels_internal(10.0, 100.0),
            20.0
        );
        assert_eq!(
            PixelValueNoPercent::from(PixelValue::rem(2.0)).to_pixels_internal(10.0, 100.0),
            200.0
        );
        // The type forbids "%" at the *parser* level, but `From<PixelValue>` can
        // still smuggle one in. It resolves against a 0 containing block -> 0px.
        assert_eq!(
            PixelValueNoPercent::from(PixelValue::percent(50.0)).to_pixels_internal(16.0, 16.0),
            0.0
        );
        assert_eq!(
            PixelValueNoPercent::zero().to_pixels_internal(16.0, 16.0),
            0.0
        );
        assert_eq!(PixelValueNoPercent::zero().inner, PixelValue::zero());
        assert_eq!(PixelValueNoPercent::default().inner, PixelValue::zero());
    }
    // =================================================== getters/predicates ===
    #[test]
    fn to_percent_is_some_only_for_the_percent_metric() {
        for metric in ALL_METRICS {
            let v = PixelValue::from_metric(metric, 50.0);
            if metric == SizeMetric::Percent {
                assert_eq!(
                    v.to_percent().unwrap().get(),
                    0.5,
                    "50% must normalize to 0.5"
                );
            } else {
                assert!(
                    v.to_percent().is_none(),
                    "{metric:?} must not masquerade as a percentage"
                );
            }
        }
        // The returned percentage is already normalized: resolve() multiplies, it
        // must not divide by 100 a second time.
        assert_eq!(
            PixelValue::percent(50.0)
                .to_percent()
                .unwrap()
                .resolve(640.0),
            320.0
        );
        assert_eq!(PixelValue::percent(-50.0).to_percent().unwrap().get(), -0.5);
        assert_eq!(PixelValue::percent(0.0).to_percent().unwrap().get(), 0.0);
        // Extreme instances stay finite.
        assert!(PixelValue::percent(f32::MAX)
            .to_percent()
            .unwrap()
            .get()
            .is_finite());
        assert_eq!(
            PixelValue::percent(f32::NAN).to_percent().unwrap().get(),
            0.0
        );
    }
    #[test]
    fn normalized_percentage_new_and_from_unnormalized_disagree_by_100x() {
        // The whole point of the type: `new` takes 0.0-1.0, `from_unnormalized`
        // takes the CSS 0-100 scale.
        assert_eq!(NormalizedPercentage::new(0.5).get(), 0.5);
        assert_eq!(NormalizedPercentage::from_unnormalized(50.0).get(), 0.5);
        assert_eq!(NormalizedPercentage::from_unnormalized(0.0).get(), 0.0);
        assert_eq!(NormalizedPercentage::from_unnormalized(100.0).get(), 1.0);
        assert_eq!(NormalizedPercentage::from_unnormalized(-25.0).get(), -0.25);
        assert_eq!(NormalizedPercentage::new(0.5).resolve(640.0), 320.0);
        assert_eq!(NormalizedPercentage::new(0.0).resolve(640.0), 0.0);
        assert_eq!(NormalizedPercentage::new(1.0).resolve(f32::MAX), f32::MAX);
        assert_eq!(NormalizedPercentage::new(-1.0).resolve(100.0), -100.0);
        // Unlike PixelValue, this type is a raw f32 wrapper: it does NOT sanitize.
        // Non-finite in, non-finite out -- but never a panic.
        assert!(NormalizedPercentage::new(f32::NAN).get().is_nan());
        assert!(NormalizedPercentage::new(f32::NAN).resolve(100.0).is_nan());
        assert!(NormalizedPercentage::from_unnormalized(f32::INFINITY)
            .get()
            .is_infinite());
        assert!(NormalizedPercentage::new(1.0)
            .resolve(f32::INFINITY)
            .is_infinite());
        // 0 * inf is NaN, and this type will hand that straight to the layout.
        assert!(NormalizedPercentage::new(0.0)
            .resolve(f32::INFINITY)
            .is_nan());
        // Display renders back on the 0-100 scale.
        assert_eq!(NormalizedPercentage::new(0.5).to_string(), "50%");
        assert_eq!(NormalizedPercentage::new(0.0).to_string(), "0%");
        assert!(!NormalizedPercentage::new(f32::NAN).to_string().is_empty());
        assert!(!NormalizedPercentage::new(f32::INFINITY)
            .to_string()
            .is_empty());
    }
    #[test]
    fn resolve_with_context_reads_the_right_reference_for_each_property() {
        let ctx = distinct_context(); // element 32 / parent 8 / root 4, block 800x600,
                                      // element 200x100, viewport 1000x500
        // em: the element's own font-size, EXCEPT on font-size, where it is the
        // parent's. Getting this backwards is the classic CSS 2.1 §15.7 bug.
        assert_eq!(
            PixelValue::em(2.0).resolve_with_context(&ctx, PropertyContext::Margin),
            64.0
        );
        assert_eq!(
            PixelValue::em(2.0).resolve_with_context(&ctx, PropertyContext::FontSize),
            16.0
        );
        // rem: always the root, whatever the property.
        for pc in ALL_PROPERTY_CONTEXTS {
            assert_eq!(
                PixelValue::rem(2.0).resolve_with_context(&ctx, pc),
                8.0,
                "rem must ignore the property context ({pc:?})"
            );
        }
        // %: the reference depends entirely on the property.
        let pct = PixelValue::percent(50.0);
        assert_eq!(
            pct.resolve_with_context(&ctx, PropertyContext::Width),
            400.0,
            "width % -> containing block WIDTH"
        );
        assert_eq!(
            pct.resolve_with_context(&ctx, PropertyContext::Height),
            300.0,
            "height % -> containing block HEIGHT"
        );
        assert_eq!(
            pct.resolve_with_context(&ctx, PropertyContext::Margin),
            400.0,
            "margin % -> containing block WIDTH, even vertically (CSS 2.1 §8.3)"
        );
        assert_eq!(
            pct.resolve_with_context(&ctx, PropertyContext::Padding),
            400.0,
            "padding % -> containing block WIDTH, even vertically (CSS 2.1 §8.4)"
        );
        assert_eq!(
            pct.resolve_with_context(&ctx, PropertyContext::Other),
            400.0
        );
        assert_eq!(
            pct.resolve_with_context(&ctx, PropertyContext::FontSize),
            4.0,
            "font-size % -> PARENT font size"
        );
        assert_eq!(
            pct.resolve_with_context(&ctx, PropertyContext::BorderRadius),
            100.0,
            "border-radius % -> the element's own box"
        );
        assert_eq!(
            pct.resolve_with_context(&ctx, PropertyContext::Transform),
            100.0
        );
        assert_eq!(
            pct.resolve_with_context(&ctx, PropertyContext::BorderWidth),
            0.0,
            "% is invalid on border-width (CSS Backgrounds 3 §4.1) -> 0"
        );
    }
    #[test]
    fn resolve_with_context_percent_without_an_element_size_is_zero() {
        // element_size is None during the first layout pass; the % arms that read
        // it must degrade to 0 instead of unwrapping.
        let ctx = ResolutionContext {
            vertical_writing_mode: false,
            element_size: None,
            ..distinct_context()
        };
        assert_eq!(
            PixelValue::percent(50.0).resolve_with_context(&ctx, PropertyContext::BorderRadius),
            0.0
        );
        assert_eq!(
            PixelValue::percent(50.0).resolve_with_context(&ctx, PropertyContext::Transform),
            0.0
        );
    }
    #[test]
    fn resolve_with_context_absolute_units_ignore_the_context_entirely() {
        let sane = distinct_context();
        let poisoned = ResolutionContext {
            vertical_writing_mode: false,
            element_font_size: f32::NAN,
            parent_font_size: f32::INFINITY,
            root_font_size: f32::NEG_INFINITY,
            containing_block_size: PhysicalSize::new(f32::NAN, f32::NAN),
            element_size: Some(PhysicalSize::new(f32::INFINITY, f32::NAN)),
            viewport_size: PhysicalSize::new(f32::NAN, f32::INFINITY),
        };
        let absolutes = [
            PixelValue::px(10.0),
            PixelValue::pt(10.0),
            PixelValue::inch(10.0),
            PixelValue::cm(10.0),
            PixelValue::mm(10.0),
        ];
        for v in absolutes {
            for pc in ALL_PROPERTY_CONTEXTS {
                let a = v.resolve_with_context(&sane, pc);
                let b = v.resolve_with_context(&poisoned, pc);
                assert_eq!(a, b, "{:?} must not read the context ({pc:?})", v.metric);
                assert!(a.is_finite());
            }
        }
        // ...and they agree with the documented conversion factors.
        assert_eq!(
            PixelValue::px(10.0).resolve_with_context(&sane, PropertyContext::Width),
            10.0
        );
        assert!(approx(
            PixelValue::inch(1.0).resolve_with_context(&sane, PropertyContext::Width),
            96.0
        ));
        assert!(approx(
            PixelValue::pt(72.0).resolve_with_context(&sane, PropertyContext::Width),
            96.0
        ));
        assert!(approx(
            PixelValue::cm(2.54).resolve_with_context(&sane, PropertyContext::Width),
            96.0
        ));
        assert!(approx(
            PixelValue::mm(25.4).resolve_with_context(&sane, PropertyContext::Width),
            96.0
        ));
    }
    #[test]
    fn resolve_with_context_viewport_units_use_the_viewport() {
        let ctx = distinct_context(); // viewport 1000x500
        assert_eq!(
            PixelValue::from_metric(SizeMetric::Vw, 10.0)
                .resolve_with_context(&ctx, PropertyContext::Width),
            100.0
        );
        assert_eq!(
            PixelValue::from_metric(SizeMetric::Vh, 10.0)
                .resolve_with_context(&ctx, PropertyContext::Width),
            50.0
        );
        assert_eq!(
            PixelValue::from_metric(SizeMetric::Vmin, 10.0)
                .resolve_with_context(&ctx, PropertyContext::Width),
            50.0,
            "vmin must take the SMALLER viewport dimension"
        );
        assert_eq!(
            PixelValue::from_metric(SizeMetric::Vmax, 10.0)
                .resolve_with_context(&ctx, PropertyContext::Width),
            100.0,
            "vmax must take the LARGER viewport dimension"
        );
        // A zero viewport (the default context) is not a division-by-zero trap:
        // the /100 is on the viewport side, so this is a plain 0.
        let zero_vp = ResolutionContext::default_const();
        for metric in [
            SizeMetric::Vw,
            SizeMetric::Vh,
            SizeMetric::Vmin,
            SizeMetric::Vmax,
        ] {
            assert_eq!(
                PixelValue::from_metric(metric, 100.0)
                    .resolve_with_context(&zero_vp, PropertyContext::Width),
                0.0,
                "{metric:?} against a 0x0 viewport must be 0"
            );
        }
        // A non-finite viewport propagates rather than panicking.
        let nan_vp = ResolutionContext {
            vertical_writing_mode: false,
            viewport_size: PhysicalSize::new(f32::NAN, f32::NAN),
            ..distinct_context()
        };
        assert!(PixelValue::from_metric(SizeMetric::Vw, 10.0)
            .resolve_with_context(&nan_vp, PropertyContext::Width)
            .is_nan());
        // NOTE: f32::min/max return the non-NaN operand, so vmin/vmax against a
        // half-NaN viewport silently pick the finite axis instead of poisoning.
        let half_nan_vp = ResolutionContext {
            vertical_writing_mode: false,
            viewport_size: PhysicalSize::new(f32::NAN, 500.0),
            ..distinct_context()
        };
        assert_eq!(
            PixelValue::from_metric(SizeMetric::Vmin, 10.0)
                .resolve_with_context(&half_nan_vp, PropertyContext::Width),
            50.0
        );
    }
    #[test]
    fn resolve_with_context_never_panics_on_extreme_values() {
        let ctx = distinct_context();
        for metric in ALL_METRICS {
            for v in EXTREME_F32 {
                for pc in ALL_PROPERTY_CONTEXTS {
                    // The only contract here is "returns, deterministically".
                    let _ = PixelValue::from_metric(metric, v).resolve_with_context(&ctx, pc);
                }
            }
        }
    }
    #[test]
    fn resolution_context_default_matches_default_const() {
        // Two hand-written constructors for the same thing: they must not drift.
        let a = ResolutionContext::default();
        let b = ResolutionContext::default_const();
        assert_eq!(a.element_font_size, b.element_font_size);
        assert_eq!(a.parent_font_size, b.parent_font_size);
        assert_eq!(a.root_font_size, b.root_font_size);
        assert_eq!(a.containing_block_size, b.containing_block_size);
        assert_eq!(a.element_size, b.element_size);
        assert_eq!(a.viewport_size, b.viewport_size);
        // The default font size is the CSS "medium" keyword (16px).
        assert_eq!(a.element_font_size, DEFAULT_FONT_SIZE);
        assert!(a.element_size.is_none());
    }
    #[test]
    fn logical_and_physical_sizes_round_trip() {
        let logical = CssLogicalSize::new(800.0, 600.0);
        assert_eq!(logical.to_physical(), PhysicalSize::new(800.0, 600.0));
        assert_eq!(logical.to_physical().to_logical(), logical);
        let physical = PhysicalSize::new(1920.0, 1080.0);
        assert_eq!(physical.to_logical(), CssLogicalSize::new(1920.0, 1080.0));
        assert_eq!(physical.to_logical().to_physical(), physical);
        // In horizontal writing mode inline==width and block==height; a swapped
        // mapping would survive a square, so use a non-square size.
        assert_eq!(CssLogicalSize::new(800.0, 600.0).to_physical().width, 800.0);
        assert_eq!(
            PhysicalSize::new(800.0, 600.0).to_logical().block_size,
            600.0
        );
        // These are transparent f32 carriers: no sanitizing, no panics.
        let nan = PhysicalSize::new(f32::NAN, f32::INFINITY);
        assert!(nan.to_logical().inline_size.is_nan());
        assert!(nan.to_logical().block_size.is_infinite());
    }
    // ======================================== serializers and round-trips ===
    #[test]
    fn every_rendering_of_a_pixel_value_agrees() {
        // Display, Debug, PrintAsCssValue and FormatAsCssValue are four separate
        // impls of the same string; they must not drift apart.
        for metric in ALL_METRICS {
            let v = PixelValue::from_metric(metric, 1.5);
            let display = v.to_string();
            assert_eq!(format!("{v:?}"), display, "Debug != Display for {metric:?}");
            assert_eq!(v.print_as_css_value(), display);
            assert_eq!(as_css_value(v), display);
            assert!(display.starts_with("1.5"), "{display} lost its number");
            assert!(display.len() > 3, "{display} lost its unit");
        }
        assert_eq!(PixelValue::px(10.0).to_string(), "10px");
        assert_eq!(PixelValue::percent(50.0).to_string(), "50%");
        assert_eq!(PixelValue::zero().to_string(), "0px");
        assert_eq!(
            PixelValue::from_metric(SizeMetric::Vmin, 12.0).to_string(),
            "12vmin"
        );
        // The no-percent wrapper delegates to the inner value.
        let np = PixelValueNoPercent::from(PixelValue::px(10.0));
        assert_eq!(np.to_string(), "10px");
        assert_eq!(format!("{np:?}"), "10px");
        assert_eq!(PixelValueNoPercent::zero().to_string(), "0px");
    }
    #[test]
    fn display_never_leaks_nan_or_infinity_into_css() {
        // A stylesheet containing "NaNpx" would be a serializer bug. The isize
        // encoding is what prevents it -- pin that for every metric and every
        // pathological input.
        for metric in ALL_METRICS {
            for v in EXTREME_F32 {
                let s = PixelValue::from_metric(metric, v).to_string();
                assert!(
                    !s.contains("NaN") && !s.contains("inf"),
                    "{metric:?} with input {v} serialized to {s:?}"
                );
                assert!(!s.is_empty());
            }
        }
        assert_eq!(PixelValue::px(f32::NAN).to_string(), "0px");
    }
    #[test]
    fn pixel_values_round_trip_through_css_for_every_metric_but_vmin() {
        // encode == decode: print_as_css_value -> parse_pixel_value -> same value.
        for metric in ALL_METRICS {
            if metric == SizeMetric::Vmin {
                continue; // known-broken suffix table; see the vmin test above
            }
            for number in [0.0_f32, 1.0, 1.5, -20.0, 0.001, 12345.0] {
                let original = PixelValue::from_metric(metric, number);
                let css = original.print_as_css_value();
                let reparsed = parse_pixel_value(&css).unwrap_or_else(|e| {
                    panic!("{css:?} (from {metric:?} {number}) failed to re-parse: {e:?}")
                });
                assert_eq!(reparsed, original, "round-trip broke for {css:?}");
                // ...and re-printing is idempotent.
                assert_eq!(reparsed.print_as_css_value(), css);
            }
        }
        // The no-percent parser round-trips everything except % (and vmin).
        for metric in ALL_METRICS {
            if metric == SizeMetric::Vmin || metric == SizeMetric::Percent {
                continue;
            }
            let original = PixelValueNoPercent::from(PixelValue::from_metric(metric, 7.0));
            let css = original.to_string();
            assert_eq!(
                parse_pixel_value_no_percent(&css).unwrap(),
                original,
                "no-percent round-trip broke for {css:?}"
            );
        }
        // ...and the with-auto wrapper round-trips its keywords and its lengths.
        for (css, expected) in [
            ("auto", PixelValueWithAuto::Auto),
            ("none", PixelValueWithAuto::None),
            ("initial", PixelValueWithAuto::Initial),
            ("inherit", PixelValueWithAuto::Inherit),
        ] {
            assert_eq!(parse_pixel_value_with_auto(css).unwrap(), expected);
        }
        let exact = PixelValue::em(1.5);
        assert_eq!(
            parse_pixel_value_with_auto(&exact.print_as_css_value()).unwrap(),
            PixelValueWithAuto::Exact(exact)
        );
    }
    #[test]
    fn format_as_rust_code_emits_a_reconstructible_literal() {
        assert_eq!(
            PixelValue::px(10.0).format_as_rust_code(0),
            "PixelValue { metric: Px, number: FloatValue::new(10) }"
        );
        assert_eq!(
            PixelValue::percent(-1.5).format_as_rust_code(4),
            "PixelValue { metric: Percent, number: FloatValue::new(-1.5) }"
        );
        // Even a pathological input must emit compilable code, never "NaN".
        let nan = PixelValue::from_metric(SizeMetric::Vmax, f32::NAN).format_as_rust_code(0);
        assert_eq!(
            nan,
            "PixelValue { metric: Vmax, number: FloatValue::new(0) }"
        );
        assert!(!PixelValue::px(f32::INFINITY)
            .format_as_rust_code(0)
            .contains("inf"));
    }
    #[test]
    fn border_thickness_constants_match_the_css_keywords() {
        // thin/medium/thick are hand-encoded as raw FloatValue bit patterns, so a
        // change to FP_PRECISION_MULTIPLIER would silently rescale them.
        assert_eq!(THIN_BORDER_THICKNESS, PixelValue::px(1.0));
        assert_eq!(MEDIUM_BORDER_THICKNESS, PixelValue::px(3.0));
        assert_eq!(THICK_BORDER_THICKNESS, PixelValue::px(5.0));
        assert_eq!(THIN_BORDER_THICKNESS.number.get(), 1.0);
        assert_eq!(MEDIUM_BORDER_THICKNESS.number.get(), 3.0);
        assert_eq!(THICK_BORDER_THICKNESS.number.get(), 5.0);
        assert_eq!(THIN_BORDER_THICKNESS.number.number() as f32, MULT);
        assert!(THIN_BORDER_THICKNESS < MEDIUM_BORDER_THICKNESS);
        assert!(MEDIUM_BORDER_THICKNESS < THICK_BORDER_THICKNESS);
        assert_eq!(THIN_BORDER_THICKNESS.to_string(), "1px");
    }
    #[test]
    fn ord_is_lexicographic_by_metric_then_number_not_by_resolved_size() {
        // PixelValue derives Ord over (metric, number). That means 100px sorts
        // BELOW 1em even though it is far larger once resolved. Anything that
        // sorts or range-queries these values needs to know that.
        assert!(PixelValue::px(100.0) < PixelValue::em(1.0));
        assert!(PixelValue::px(1.0) < PixelValue::px(2.0));
        assert!(PixelValue::percent(1.0) > PixelValue::mm(9999.0));
        // Eq/Hash agree, including across the sanitized encodings.
        let a = PixelValue::px(1.5);
        let b = PixelValue::px(1.5);
        assert_eq!(a, b);
        assert_eq!(hash_of(&a), hash_of(&b));
        assert_ne!(hash_of(&PixelValue::px(1.0)), hash_of(&PixelValue::em(1.0)));
        // Two values that differ only below the 1/1000 quantum collide -- by
        // design, since that is what makes PixelValue hashable at all.
        assert_eq!(PixelValue::px(1.0001), PixelValue::px(1.0002));
        assert_eq!(
            hash_of(&PixelValue::px(1.0001)),
            hash_of(&PixelValue::px(1.0002))
        );
    }
    // ====================================================== system metrics ===
    #[test]
    fn system_metric_ref_css_strings_round_trip() {
        for r in ALL_SYSTEM_REFS {
            let css = r.as_css_str();
            assert!(
                css.starts_with("system:"),
                "{css:?} is missing the system: prefix"
            );
            assert_eq!(r.to_string(), css, "Display must match as_css_str");
            assert_eq!(as_css_value(r), css);
            // from_css_str takes the name WITHOUT the prefix.
            let name = css.strip_prefix("system:").unwrap();
            assert_eq!(
                SystemMetricRef::from_css_str(name),
                Some(r),
                "{name:?} must parse back to {r:?}"
            );
            // Serialize-parse-serialize is stable.
            assert_eq!(
                SystemMetricRef::from_css_str(name).unwrap().as_css_str(),
                css
            );
            // Footgun worth pinning: feeding the *full* CSS string back in fails,
            // because from_css_str does not strip the prefix itself.
            assert_eq!(SystemMetricRef::from_css_str(css), None);
        }
        assert_eq!(SystemMetricRef::default(), SystemMetricRef::ButtonRadius);
    }
    #[test]
    fn system_metric_ref_from_css_str_rejects_everything_else() {
        for input in [
            "",
            "   ",
            "\t\n",
            " button-radius ", // no trimming
            "Button-Radius",   // case-sensitive
            "button_radius",   // wrong separator
            "button-padding",  // the spelling the docs advertise; not a real one
            "button-radius;x",
            "\u{1F600}",
            "b\u{0301}utton-radius",
        ] {
            assert_eq!(
                SystemMetricRef::from_css_str(input),
                None,
                "{input:?} must not resolve to a system metric"
            );
        }
        // Long input is rejected without hanging.
        assert_eq!(SystemMetricRef::from_css_str(&"a".repeat(100_000)), None);
        assert_eq!(SystemMetricRef::from_css_str(&"(".repeat(10_000)), None);
    }
    #[test]
    fn system_metric_ref_resolve_maps_each_variant_to_its_own_field() {
        // Every field gets a distinct value, so a mis-wired arm cannot pass.
        let metrics = populated_metrics();
        let expected = [
            (SystemMetricRef::ButtonRadius, 1.0),
            (SystemMetricRef::ButtonBorderWidth, 2.0),
            (SystemMetricRef::ButtonPaddingHorizontal, 3.0),
            (SystemMetricRef::ButtonPaddingVertical, 4.0),
            (SystemMetricRef::TitlebarHeight, 5.0),
            (SystemMetricRef::TitlebarButtonWidth, 6.0),
            (SystemMetricRef::TitlebarPadding, 7.0),
            (SystemMetricRef::SafeAreaTop, 8.0),
            (SystemMetricRef::SafeAreaBottom, 9.0),
            (SystemMetricRef::SafeAreaLeft, 10.0),
            (SystemMetricRef::SafeAreaRight, 11.0),
        ];
        for (r, px) in expected {
            assert_eq!(
                r.resolve(&metrics),
                Some(PixelValue::px(px)),
                "{r:?} resolved to the wrong field"
            );
        }
        // An unpopulated SystemMetrics yields None for every variant (no unwraps).
        let empty = SystemMetrics::default();
        for r in ALL_SYSTEM_REFS {
            assert_eq!(r.resolve(&empty), None, "{r:?} must be None when unset");
        }
    }
    #[test]
    fn pixel_value_or_system_resolves_and_falls_back() {
        let metrics = populated_metrics();
        let empty = SystemMetrics::default();
        let fallback = PixelValue::px(99.0);
        // A concrete value ignores the system metrics entirely.
        let concrete = PixelValueOrSystem::value(PixelValue::px(10.0));
        assert_eq!(concrete.resolve(&metrics, fallback), PixelValue::px(10.0));
        assert_eq!(concrete.resolve(&empty, fallback), PixelValue::px(10.0));
        // A system ref takes the metric when present...
        let sys = PixelValueOrSystem::system(SystemMetricRef::ButtonRadius);
        assert_eq!(sys.resolve(&metrics, fallback), PixelValue::px(1.0));
        // ...and the fallback when absent, for every variant.
        for r in ALL_SYSTEM_REFS {
            assert_eq!(
                PixelValueOrSystem::system(r).resolve(&empty, fallback),
                fallback,
                "{r:?} must fall back when the metric is unset"
            );
        }
        // Extreme fallbacks stay finite (they went through FloatValue too).
        let nan_fallback = PixelValue::px(f32::NAN);
        assert_eq!(sys.resolve(&empty, nan_fallback).number.get(), 0.0);
        // Constructors / conversions / default.
        assert_eq!(
            PixelValueOrSystem::default(),
            PixelValueOrSystem::Value(PixelValue::zero())
        );
        assert_eq!(
            PixelValueOrSystem::from(PixelValue::em(2.0)),
            PixelValueOrSystem::Value(PixelValue::em(2.0))
        );
        assert_eq!(
            PixelValueOrSystem::default().resolve(&metrics, fallback),
            PixelValue::zero()
        );
    }
    #[test]
    fn pixel_value_or_system_renders_both_arms() {
        let concrete = PixelValueOrSystem::value(PixelValue::px(10.0));
        assert_eq!(concrete.to_string(), "10px");
        assert_eq!(as_css_value(concrete), "10px");
        let sys = PixelValueOrSystem::system(SystemMetricRef::TitlebarHeight);
        assert_eq!(sys.to_string(), "system:titlebar-height");
        assert_eq!(as_css_value(sys), "system:titlebar-height");
        assert_eq!(PixelValueOrSystem::default().to_string(), "0px");
        // No arm can serialize a non-finite number.
        for v in EXTREME_F32 {
            let s = PixelValueOrSystem::value(PixelValue::px(v)).to_string();
            assert!(!s.contains("NaN") && !s.contains("inf"), "leaked {s:?}");
        }
    }
}