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
9
//!   em vs rem, and resolves % based on property type per the CSS spec.
10
//! - `to_pixels_internal()` — legacy fallback used by `prop_cache.rs`; does not
11
//!   distinguish rem from em. Marked `#[doc(hidden)]`.
12

            
13
use core::fmt;
14
use std::num::ParseFloatError;
15
use crate::corety::AzString;
16

            
17
use crate::props::{
18
    basic::{error::ParseFloatErrorWithInput, FloatValue, SizeMetric},
19
    formatter::FormatAsCssValue,
20
};
21

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

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

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

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

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

            
57
    /// Get the raw normalized value (0.0-1.0)
58
    #[inline]
59
7250
    #[must_use] pub const fn get(self) -> f32 {
60
7250
        self.0
61
7250
    }
62

            
63
    /// Resolve this percentage against a containing block size
64
    ///
65
    /// This multiplies the normalized percentage by the containing block size.
66
    /// For example, 50% (0.5) of 640px = 320px.
67
    #[inline]
68
17805
    #[must_use] pub fn resolve(self, containing_block_size: f32) -> f32 {
69
17805
        self.0 * containing_block_size
70
17805
    }
71
}
72

            
73
impl fmt::Display for NormalizedPercentage {
74
4
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75
4
        write!(f, "{}%", self.0 * 100.0)
76
4
    }
77
}
78

            
79
/// Logical size in CSS logical coordinate system
80
#[derive(Debug, Copy, Clone, PartialEq)]
81
#[repr(C)]
82
pub struct CssLogicalSize {
83
    /// Inline-axis size (width in horizontal writing mode)
84
    pub inline_size: f32,
85
    /// Block-axis size (height in horizontal writing mode)
86
    pub block_size: f32,
87
}
88

            
89
impl CssLogicalSize {
90
    #[inline]
91
3
    #[must_use] pub const fn new(inline_size: f32, block_size: f32) -> Self {
92
3
        Self {
93
3
            inline_size,
94
3
            block_size,
95
3
        }
96
3
    }
97

            
98
    /// Convert to physical size (width, height) in horizontal writing mode
99
    #[inline]
100
4
    #[must_use] pub const fn to_physical(self) -> PhysicalSize {
101
4
        PhysicalSize {
102
4
            width: self.inline_size,
103
4
            height: self.block_size,
104
4
        }
105
4
    }
106
}
107

            
108
/// Physical size (always width x height, regardless of writing mode)
109
#[derive(Debug, Copy, Clone, PartialEq)]
110
#[repr(C)]
111
pub struct PhysicalSize {
112
    pub width: f32,
113
    pub height: f32,
114
}
115

            
116
impl PhysicalSize {
117
    #[inline]
118
1916403
    #[must_use] pub const fn new(width: f32, height: f32) -> Self {
119
1916403
        Self { width, height }
120
1916403
    }
121

            
122
    /// Convert to logical size in horizontal writing mode
123
    #[inline]
124
6
    #[must_use] pub const fn to_logical(self) -> CssLogicalSize {
125
6
        CssLogicalSize {
126
6
            inline_size: self.width,
127
6
            block_size: self.height,
128
6
        }
129
6
    }
130
}
131

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

            
152
    /// The computed font-size of the parent element (for em in font-size property)
153
    pub parent_font_size: f32,
154

            
155
    /// The computed font-size of the root element (for rem units)
156
    pub root_font_size: f32,
157

            
158
    /// The containing block dimensions (for % in width/height/margins/padding)
159
    pub containing_block_size: PhysicalSize,
160

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

            
165
    /// Is the element in a VERTICAL writing mode (`vertical-rl`/`vertical-lr`)?
166
    /// css-writing-modes-4 §7.2: margin/padding percentages resolve against
167
    /// the containing block's INLINE size - the physical HEIGHT in vertical
168
    /// modes. Physical width/height percentages are unaffected.
169
    pub vertical_writing_mode: bool,
170

            
171
    /// The viewport size in CSS pixels (for vw, vh, vmin, vmax units)
172
    /// This is the layout viewport size, not physical screen size
173
    pub viewport_size: PhysicalSize,
174
}
175

            
176
impl Default for ResolutionContext {
177
3
    fn default() -> Self {
178
3
        Self {
179
3
            element_font_size: 16.0,
180
3
            parent_font_size: 16.0,
181
3
            root_font_size: 16.0,
182
3
            containing_block_size: PhysicalSize::new(0.0, 0.0),
183
3
            element_size: None,
184
3
            viewport_size: PhysicalSize::new(0.0, 0.0),
185
3
            vertical_writing_mode: false,
186
3
        }
187
3
    }
188
}
189

            
190
impl ResolutionContext {
191
    /// Create a minimal context for testing or default resolution
192
    #[inline]
193
2
    #[must_use] pub const fn default_const() -> Self {
194
2
        Self {
195
2
            element_font_size: 16.0,
196
2
            parent_font_size: 16.0,
197
2
            root_font_size: 16.0,
198
2
            containing_block_size: PhysicalSize {
199
2
                width: 0.0,
200
2
                height: 0.0,
201
2
            },
202
2
            element_size: None,
203
2
            viewport_size: PhysicalSize {
204
2
                width: 0.0,
205
2
                height: 0.0,
206
2
            },
207
2
            vertical_writing_mode: false,
208
2
        }
209
2
    }
210

            
211
}
212

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

            
236
/// A CSS length value consisting of a numeric value and a unit (px, em, rem, %, etc.).
237
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
238
#[repr(C)]
239
pub struct PixelValue {
240
    pub metric: SizeMetric,
241
    pub number: FloatValue,
242
}
243

            
244
impl PixelValue {
245
175
    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
246
175
        self.number = FloatValue::new(self.number.get() * scale_factor);
247
175
    }
248
}
249

            
250
impl FormatAsCssValue for PixelValue {
251
193
    fn format_as_css_value(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252
193
        write!(f, "{}{}", self.number, self.metric)
253
193
    }
254
}
255

            
256
impl crate::css::PrintAsCssValue for PixelValue {
257
292
    fn print_as_css_value(&self) -> String {
258
292
        format!("{}{}", self.number, self.metric)
259
292
    }
260
}
261

            
262
impl crate::codegen::format::FormatAsRustCode for PixelValue {
263
4
    fn format_as_rust_code(&self, _tabs: usize) -> String {
264
4
        format!(
265
4
            "PixelValue {{ metric: {:?}, number: FloatValue::new({}) }}",
266
            self.metric,
267
4
            self.number.get()
268
        )
269
4
    }
270
}
271

            
272
// Manual Debug implementation, because the auto-generated one is nearly unreadable
273
impl fmt::Debug for PixelValue {
274
282946
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275
282946
        write!(f, "{}{}", self.number, self.metric)
276
282946
    }
277
}
278

            
279
impl fmt::Display for PixelValue {
280
7436
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281
7436
        write!(f, "{}{}", self.number, self.metric)
282
7436
    }
283
}
284

            
285
impl PixelValue {
286
    #[inline]
287
19133
    #[must_use] pub const fn zero() -> Self {
288
        const ZERO_PX: PixelValue = PixelValue::const_px(0);
289
19133
        ZERO_PX
290
19133
    }
291

            
292
    /// Same as `PixelValue::px()`, but only accepts whole numbers,
293
    /// since using `f32` in const fn is not yet stabilized.
294
    #[inline]
295
3721607
    #[must_use] pub const fn const_px(value: isize) -> Self {
296
3721607
        Self::const_from_metric(SizeMetric::Px, value)
297
3721607
    }
298

            
299
    /// Same as `PixelValue::em()`, but only accepts whole numbers,
300
    /// since using `f32` in const fn is not yet stabilized.
301
    #[inline]
302
13
    #[must_use] pub const fn const_em(value: isize) -> Self {
303
13
        Self::const_from_metric(SizeMetric::Em, value)
304
13
    }
305

            
306
    /// Creates an em value from a fractional number in const context.
307
    ///
308
    /// # Arguments
309
    /// * `pre_comma` - The integer part (e.g., 1 for 1.5em)
310
    /// * `post_comma` - The fractional part as digits (e.g., 5 for 0.5em, 83 for 0.83em)
311
    ///
312
    /// # Examples
313
    /// ```
314
    /// // 1.5em = const_em_fractional(1, 5)
315
    /// // 0.83em = const_em_fractional(0, 83)
316
    /// // 1.17em = const_em_fractional(1, 17)
317
    /// ```
318
    #[inline]
319
4
    #[must_use] pub const fn const_em_fractional(pre_comma: isize, post_comma: isize) -> Self {
320
4
        Self::const_from_metric_fractional(SizeMetric::Em, pre_comma, post_comma)
321
4
    }
322

            
323
    /// Same as `PixelValue::pt()`, but only accepts whole numbers,
324
    /// since using `f32` in const fn is not yet stabilized.
325
    #[inline]
326
16
    #[must_use] pub const fn const_pt(value: isize) -> Self {
327
16
        Self::const_from_metric(SizeMetric::Pt, value)
328
16
    }
329

            
330
    /// Creates a pt value from a fractional number in const context.
331
    #[inline]
332
2
    #[must_use] pub const fn const_pt_fractional(pre_comma: isize, post_comma: isize) -> Self {
333
2
        Self::const_from_metric_fractional(SizeMetric::Pt, pre_comma, post_comma)
334
2
    }
335

            
336
    /// Same as `PixelValue::percent()`, but only accepts whole numbers,
337
    /// since using `f32` in const fn is not yet stabilized.
338
    #[inline]
339
25363
    #[must_use] pub const fn const_percent(value: isize) -> Self {
340
25363
        Self::const_from_metric(SizeMetric::Percent, value)
341
25363
    }
342

            
343
    /// Same as `PixelValue::in()`, but only accepts whole numbers,
344
    /// since using `f32` in const fn is not yet stabilized.
345
    #[inline]
346
7
    #[must_use] pub const fn const_in(value: isize) -> Self {
347
7
        Self::const_from_metric(SizeMetric::In, value)
348
7
    }
349

            
350
    /// Same as `PixelValue::cm()`, but only accepts whole numbers,
351
    /// since using `f32` in const fn is not yet stabilized.
352
    #[inline]
353
8
    #[must_use] pub const fn const_cm(value: isize) -> Self {
354
8
        Self::const_from_metric(SizeMetric::Cm, value)
355
8
    }
356

            
357
    /// Same as `PixelValue::mm()`, but only accepts whole numbers,
358
    /// since using `f32` in const fn is not yet stabilized.
359
    #[inline]
360
8
    #[must_use] pub const fn const_mm(value: isize) -> Self {
361
8
        Self::const_from_metric(SizeMetric::Mm, value)
362
8
    }
363

            
364
    #[inline]
365
3747104
    #[must_use] pub const fn const_from_metric(metric: SizeMetric, value: isize) -> Self {
366
3747104
        Self {
367
3747104
            metric,
368
3747104
            number: FloatValue::const_new(value),
369
3747104
        }
370
3747104
    }
371

            
372
    /// Creates a `PixelValue` from a fractional number in const context.
373
    ///
374
    /// # Arguments
375
    /// * `metric` - The size metric (Px, Em, Pt, etc.)
376
    /// * `pre_comma` - The integer part
377
    /// * `post_comma` - The fractional part as digits
378
    #[inline]
379
10
    #[must_use] pub const fn const_from_metric_fractional(
380
10
        metric: SizeMetric,
381
10
        pre_comma: isize,
382
10
        post_comma: isize,
383
10
    ) -> Self {
384
10
        Self {
385
10
            metric,
386
10
            number: FloatValue::const_new_fractional(pre_comma, post_comma),
387
10
        }
388
10
    }
389

            
390
    #[inline]
391
10334705
    #[must_use] pub fn px(value: f32) -> Self {
392
10334705
        Self::from_metric(SizeMetric::Px, value)
393
10334705
    }
394

            
395
    #[inline]
396
1457
    #[must_use] pub fn em(value: f32) -> Self {
397
1457
        Self::from_metric(SizeMetric::Em, value)
398
1457
    }
399

            
400
    #[inline]
401
28
    #[must_use] pub fn inch(value: f32) -> Self {
402
28
        Self::from_metric(SizeMetric::In, value)
403
28
    }
404

            
405
    #[inline]
406
28
    #[must_use] pub fn cm(value: f32) -> Self {
407
28
        Self::from_metric(SizeMetric::Cm, value)
408
28
    }
409

            
410
    #[inline]
411
28
    #[must_use] pub fn mm(value: f32) -> Self {
412
28
        Self::from_metric(SizeMetric::Mm, value)
413
28
    }
414

            
415
    #[inline]
416
58
    #[must_use] pub fn pt(value: f32) -> Self {
417
58
        Self::from_metric(SizeMetric::Pt, value)
418
58
    }
419

            
420
    #[inline]
421
29251
    #[must_use] pub fn percent(value: f32) -> Self {
422
29251
        Self::from_metric(SizeMetric::Percent, value)
423
29251
    }
424

            
425
    #[inline]
426
69
    #[must_use] pub fn rem(value: f32) -> Self {
427
69
        Self::from_metric(SizeMetric::Rem, value)
428
69
    }
429

            
430
    #[inline]
431
11148900
    #[must_use] pub fn from_metric(metric: SizeMetric, value: f32) -> Self {
432
11148900
        Self {
433
11148900
            metric,
434
11148900
            number: FloatValue::new(value),
435
11148900
        }
436
11148900
    }
437

            
438
    #[inline]
439
    #[allow(clippy::suboptimal_flops)] // explicit FP; mul_add slower without +fma
440
1646
    #[must_use] pub fn interpolate(&self, other: &Self, t: f32) -> Self {
441
1646
        if self.metric == other.metric {
442
1601
            Self {
443
1601
                metric: self.metric,
444
1601
                number: self.number.interpolate(&other.number, t),
445
1601
            }
446
        } else {
447
            // Interpolate between different metrics by converting to px
448
            // Note: Uses DEFAULT_FONT_SIZE for em/rem - acceptable for animation fallback
449
45
            let self_px_interp = self.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
450
45
            let other_px_interp = other.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
451
45
            Self::from_metric(
452
45
                SizeMetric::Px,
453
45
                self_px_interp + (other_px_interp - self_px_interp) * t,
454
            )
455
        }
456
1646
    }
457

            
458
    /// Returns the value of the `SizeMetric` as a normalized percentage (0.0 = 0%, 1.0 = 100%)
459
    ///
460
    /// Returns `Some(NormalizedPercentage)` if this is a percentage value, `None` otherwise.
461
    /// The returned `NormalizedPercentage` is already normalized to 0.0-1.0 range,
462
    /// so you should multiply it directly with the containing block size.
463
    #[inline]
464
7486
    #[must_use] pub fn to_percent(&self) -> Option<NormalizedPercentage> {
465
7486
        match self.metric {
466
7244
            SizeMetric::Percent => Some(NormalizedPercentage::from_unnormalized(self.number.get())),
467
242
            _ => None,
468
        }
469
7486
    }
470

            
471
    /// Internal fallback method for converting to pixels with manual % resolution.
472
    ///
473
    /// Used internally by prop_cache.rs resolve_property_dependency().
474
    ///
475
    /// **DO NOT USE directly!** Use `resolve_with_context()` instead for new code.
476
    #[doc(hidden)]
477
    #[inline]
478
1544861
    #[must_use] pub fn to_pixels_internal(&self, percent_resolve: f32, em_resolve: f32, rem_resolve: f32) -> f32 {
479
1544861
        match self.metric {
480
1527760
            SizeMetric::Px => self.number.get(),
481
25
            SizeMetric::Pt => self.number.get() * PT_TO_PX,
482
9
            SizeMetric::In => self.number.get() * 96.0,
483
9
            SizeMetric::Cm => self.number.get() * 96.0 / 2.54,
484
9
            SizeMetric::Mm => self.number.get() * 96.0 / 25.4,
485
197
            SizeMetric::Em => self.number.get() * em_resolve,
486
23
            SizeMetric::Rem => self.number.get() * rem_resolve,
487
            SizeMetric::Percent => {
488
16756
                NormalizedPercentage::from_unnormalized(self.number.get()).resolve(percent_resolve)
489
            }
490
            // Viewport units: Cannot resolve without viewport context, return 0
491
            // These should use resolve_with_context() instead
492
73
            SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => 0.0,
493
        }
494
1544861
    }
495

            
496
    /// Resolve this value to pixels using proper CSS context.
497
    ///
498
    /// This is the **CORRECT** way to resolve CSS units. It properly handles:
499
    /// - em units: Uses element's own font-size (or parent's for font-size property)
500
    /// - rem units: Uses root element's font-size
501
    /// - % units: Uses property-appropriate reference (containing block width/height, element size,
502
    ///   etc.)
503
    /// - Absolute units: px, pt, in, cm, mm (already correct)
504
    ///
505
    /// # Arguments
506
    /// * `context` - Resolution context with font sizes and dimensions
507
    /// * `property_context` - Which property we're resolving for (affects % and em resolution)
508
    #[inline]
509
3433536
    #[must_use] pub fn resolve_with_context(
510
3433536
        &self,
511
3433536
        context: &ResolutionContext,
512
3433536
        property_context: PropertyContext,
513
3433536
    ) -> f32 {
514
3433536
        match self.metric {
515
            // Absolute units - already correct
516
3334643
            SizeMetric::Px => self.number.get(),
517
136
            SizeMetric::Pt => self.number.get() * PT_TO_PX,
518
136
            SizeMetric::In => self.number.get() * 96.0,
519
136
            SizeMetric::Cm => self.number.get() * 96.0 / 2.54,
520
136
            SizeMetric::Mm => self.number.get() * 96.0 / 25.4,
521

            
522
            // Em units - CRITICAL: different resolution for font-size vs other properties
523
            SizeMetric::Em => {
524
96162
                let reference_font_size = if property_context == PropertyContext::FontSize {
525
                    // Em on font-size refers to parent's font-size (CSS 2.1 §15.7)
526
103
                    context.parent_font_size
527
                } else {
528
                    // Em on other properties refers to element's own font-size (CSS 2.1 §10.5)
529
96059
                    context.element_font_size
530
                };
531
96162
                self.number.get() * reference_font_size
532
            }
533

            
534
            // Rem units - ALWAYS refer to root font-size (CSS Values 3)
535
183
            SizeMetric::Rem => self.number.get() * context.root_font_size,
536

            
537
            // Viewport units - refer to viewport dimensions (CSS Values 3 §6.2)
538
            // 1vw = 1% of viewport width, 1vh = 1% of viewport height
539
362
            SizeMetric::Vw => self.number.get() * context.viewport_size.width / 100.0,
540
405
            SizeMetric::Vh => self.number.get() * context.viewport_size.height / 100.0,
541
            // vmin = smaller of vw or vh
542
            SizeMetric::Vmin => {
543
142
                let min_dimension = context
544
142
                    .viewport_size
545
142
                    .width
546
142
                    .min(context.viewport_size.height);
547
142
                self.number.get() * min_dimension / 100.0
548
            }
549
            // vmax = larger of vw or vh
550
            SizeMetric::Vmax => {
551
130
                let max_dimension = context
552
130
                    .viewport_size
553
130
                    .width
554
130
                    .max(context.viewport_size.height);
555
130
                self.number.get() * max_dimension / 100.0
556
            }
557

            
558
            // Percent units - reference depends on property type
559
            SizeMetric::Percent => {
560
                // Width and Other deliberately both resolve to containing-block width but are
561
                // kept as separate arms for documentation / likely future divergence.
562
                #[allow(clippy::match_same_arms)]
563
965
                let reference = match property_context {
564
                    // Font-size %: refers to parent's font-size (CSS 2.1 §15.7)
565
14
                    PropertyContext::FontSize => context.parent_font_size,
566

            
567
                    // Width and horizontal properties: containing block width (CSS 2.1 §10.3)
568
102
                    PropertyContext::Width => context.containing_block_size.width,
569

            
570
                    // Height and vertical properties: containing block height (CSS 2.1 §10.5)
571
102
                    PropertyContext::Height => context.containing_block_size.height,
572

            
573
                    // +spec:box-model:66e123 - margin/padding % resolved against inline size (= width in horizontal-tb)
574
                    // +spec:width-calculation:bef810 - margin percentages refer to containing block width (even top/bottom)
575
                    // Margins: ALWAYS containing block WIDTH, even for top/bottom! (CSS 2.1 §8.3)
576
                    // +spec:width-calculation:d78514 - margin percentages refer to width of containing block
577
                    // Padding: ALWAYS containing block WIDTH, even for top/bottom! (CSS 2.1 §8.4)
578
                    PropertyContext::Margin | PropertyContext::Padding => {
579
                        // CSS3 (writing-modes-4 §7.2) upgrades CSS 2.1's
580
                        // "always width" to "the INLINE size": physical width
581
                        // in horizontal-tb, physical HEIGHT in vertical-rl/lr.
582
458
                        if context.vertical_writing_mode {
583
22
                            context.containing_block_size.height
584
                        } else {
585
436
                            context.containing_block_size.width
586
                        }
587
                    }
588

            
589
                    // Border-width: % is NOT valid per CSS spec (CSS Backgrounds 3 §4.1)
590
                    // Return 0.0 if someone tries to use % on border-width
591
69
                    PropertyContext::BorderWidth => 0.0,
592

            
593
                    // Border-radius: element's own dimensions (CSS Backgrounds 3 §5.1)
594
                    // Note: More complex - horizontal % uses width, vertical % uses height
595
                    // For now, use width as default
596
                    PropertyContext::BorderRadius => {
597
191
                        context.element_size.map_or(0.0, |s| s.width)
598
                    }
599

            
600
                    // Transforms: element's own dimensions (CSS Transforms §20.1)
601
                    PropertyContext::Transform => {
602
15
                        context.element_size.map_or(0.0, |s| s.width)
603
                    }
604

            
605
                    // Other properties: default to containing block width
606
14
                    PropertyContext::Other => context.containing_block_size.width,
607
                };
608

            
609
965
                NormalizedPercentage::from_unnormalized(self.number.get()).resolve(reference)
610
            }
611
        }
612
3433536
    }
613
}
614

            
615
// border-width: thin / medium / thick keyword values
616
// These are the canonical CSS definitions and should be used consistently
617
// across parsing and resolution.
618

            
619
/// border-width: thin = 1px (per CSS spec)
620
pub const THIN_BORDER_THICKNESS: PixelValue = PixelValue {
621
    metric: SizeMetric::Px,
622
    number: FloatValue { number: 1000 },
623
};
624

            
625
/// border-width: medium = 3px (per CSS spec, default)
626
pub const MEDIUM_BORDER_THICKNESS: PixelValue = PixelValue {
627
    metric: SizeMetric::Px,
628
    number: FloatValue { number: 3000 },
629
};
630

            
631
/// border-width: thick = 5px (per CSS spec)
632
pub const THICK_BORDER_THICKNESS: PixelValue = PixelValue {
633
    metric: SizeMetric::Px,
634
    number: FloatValue { number: 5000 },
635
};
636

            
637
/// Same as `PixelValue`, but doesn't allow a "%" sign
638
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
639
#[repr(C)]
640
pub struct PixelValueNoPercent {
641
    pub inner: PixelValue,
642
}
643

            
644
impl PixelValueNoPercent {
645
146
    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
646
146
        self.inner.scale_for_dpi(scale_factor);
647
146
    }
648
}
649

            
650
impl_option!(
651
    PixelValueNoPercent,
652
    OptionPixelValueNoPercent,
653
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
654
);
655

            
656
impl_option!(
657
    PixelValue,
658
    OptionPixelValue,
659
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
660
);
661

            
662
impl fmt::Display for PixelValueNoPercent {
663
506
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
664
506
        write!(f, "{}", self.inner)
665
506
    }
666
}
667

            
668
impl ::core::fmt::Debug for PixelValueNoPercent {
669
385
    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
670
385
        write!(f, "{self}")
671
385
    }
672
}
673

            
674
impl PixelValueNoPercent {
675
    /// Internal conversion to pixels (no percent support).
676
    ///
677
    /// Used internally by prop_cache.rs.
678
    ///
679
    /// **DO NOT USE directly!** Use `resolve_with_context()` on inner value instead.
680
    #[doc(hidden)]
681
    #[inline]
682
93
    #[must_use] pub fn to_pixels_internal(&self, em_resolve: f32, rem_resolve: f32) -> f32 {
683
93
        self.inner.to_pixels_internal(0.0, em_resolve, rem_resolve)
684
93
    }
685

            
686
    #[inline]
687
6
    #[must_use] pub const fn zero() -> Self {
688
        const ZERO_PXNP: PixelValueNoPercent = PixelValueNoPercent {
689
            inner: PixelValue::zero(),
690
        };
691
6
        ZERO_PXNP
692
6
    }
693
}
694
impl From<PixelValue> for PixelValueNoPercent {
695
17
    fn from(e: PixelValue) -> Self {
696
17
        Self { inner: e }
697
17
    }
698
}
699

            
700
#[derive(Clone, PartialEq, Eq)]
701
pub enum CssPixelValueParseError<'a> {
702
    EmptyString,
703
    NoValueGiven(&'a str, SizeMetric),
704
    ValueParseErr(ParseFloatError, &'a str),
705
    InvalidPixelValue(&'a str),
706
}
707

            
708
impl_debug_as_display!(CssPixelValueParseError<'a>);
709

            
710
impl_display! { CssPixelValueParseError<'a>, {
711
    EmptyString => format!("Missing [px / pt / em / %] value"),
712
    NoValueGiven(input, metric) => format!("Expected floating-point pixel value, got: \"{}{}\"", input, metric),
713
    ValueParseErr(err, number_str) => format!("Could not parse \"{}\" as floating-point value: \"{}\"", number_str, err),
714
    InvalidPixelValue(s) => format!("Invalid pixel value: \"{}\"", s),
715
}}
716

            
717
/// Wrapper for `NoValueGiven` error in pixel value parsing.
718
#[derive(Debug, Clone, PartialEq, Eq)]
719
#[repr(C)]
720
pub struct PixelNoValueGivenError {
721
    pub value: AzString,
722
    pub metric: SizeMetric,
723
}
724

            
725
/// Owned version of `CssPixelValueParseError`.
726
#[derive(Debug, Clone, PartialEq, Eq)]
727
#[repr(C, u8)]
728
pub enum CssPixelValueParseErrorOwned {
729
    EmptyString,
730
    NoValueGiven(PixelNoValueGivenError),
731
    ValueParseErr(ParseFloatErrorWithInput),
732
    InvalidPixelValue(AzString),
733
}
734

            
735
impl CssPixelValueParseError<'_> {
736
143
    #[must_use] pub fn to_contained(&self) -> CssPixelValueParseErrorOwned {
737
143
        match self {
738
29
            CssPixelValueParseError::EmptyString => CssPixelValueParseErrorOwned::EmptyString,
739
21
            CssPixelValueParseError::NoValueGiven(s, metric) => {
740
21
                CssPixelValueParseErrorOwned::NoValueGiven(PixelNoValueGivenError { value: (*s).to_string().into(), metric: *metric })
741
            }
742
23
            CssPixelValueParseError::ValueParseErr(err, s) => {
743
23
                CssPixelValueParseErrorOwned::ValueParseErr(ParseFloatErrorWithInput { error: err.clone().into(), input: (*s).to_string().into() })
744
            }
745
70
            CssPixelValueParseError::InvalidPixelValue(s) => {
746
70
                CssPixelValueParseErrorOwned::InvalidPixelValue((*s).to_string().into())
747
            }
748
        }
749
143
    }
750
}
751

            
752
impl CssPixelValueParseErrorOwned {
753
131
    #[must_use] pub fn to_shared(&self) -> CssPixelValueParseError<'_> {
754
131
        match self {
755
26
            Self::EmptyString => CssPixelValueParseError::EmptyString,
756
20
            Self::NoValueGiven(e) => {
757
20
                CssPixelValueParseError::NoValueGiven(e.value.as_str(), e.metric)
758
            }
759
20
            Self::ValueParseErr(e) => {
760
20
                CssPixelValueParseError::ValueParseErr(e.error.to_std(), e.input.as_str())
761
            }
762
65
            Self::InvalidPixelValue(s) => {
763
65
                CssPixelValueParseError::InvalidPixelValue(s.as_str())
764
            }
765
        }
766
131
    }
767
}
768

            
769
/// parses an angle value like `30deg`, `1.64rad`, `100%`, etc.
770
782795
fn parse_pixel_value_inner<'a>(
771
782795
    input: &'a str,
772
782795
    match_values: &[(&'static str, SizeMetric)],
773
782795
) -> Result<PixelValue, CssPixelValueParseError<'a>> {
774
782795
    let input = input.trim();
775

            
776
782795
    if input.is_empty() {
777
204
        return Err(CssPixelValueParseError::EmptyString);
778
782591
    }
779

            
780
1155645
    for (match_val, metric) in match_values {
781
1136572
        if let Some(value) = input.strip_suffix(match_val) {
782
763518
            let value = value.trim();
783
763518
            if value.is_empty() {
784
429
                return Err(CssPixelValueParseError::NoValueGiven(input, *metric));
785
763089
            }
786
763089
            match value.parse::<f32>() {
787
762786
                Ok(o) => {
788
762786
                    return Ok(PixelValue::from_metric(*metric, o));
789
                }
790
303
                Err(e) => {
791
303
                    return Err(CssPixelValueParseError::ValueParseErr(e, value));
792
                }
793
            }
794
373054
        }
795
    }
796

            
797
19073
    input.trim().parse::<f32>().map_or_else(
798
3375
        |_| Err(CssPixelValueParseError::InvalidPixelValue(input)),
799
15698
        |o| Ok(PixelValue::px(o)),
800
    )
801
782795
}
802

            
803
/// # Errors
804
///
805
/// Returns an error if `input` is not a valid CSS `pixel-value` value.
806
782405
pub fn parse_pixel_value(input: &str) -> Result<PixelValue, CssPixelValueParseError<'_>> {
807
782405
    parse_pixel_value_inner(
808
782405
        input,
809
782405
        &[
810
782405
            // ORDER IS LOAD-BEARING: matching is by `strip_suffix`, first hit wins, so
811
782405
            // any unit that is a SUFFIX of another must come after it.
812
782405
            ("px", SizeMetric::Px),
813
782405
            ("rem", SizeMetric::Rem), // before "em" ("rem" ends with "em")
814
782405
            ("em", SizeMetric::Em),
815
782405
            ("pt", SizeMetric::Pt),
816
782405
            ("vmax", SizeMetric::Vmax),
817
782405
            ("vmin", SizeMetric::Vmin), // before "in" -- "vmin" ends with "in"!
818
782405
            ("vw", SizeMetric::Vw),
819
782405
            ("vh", SizeMetric::Vh),
820
782405
            ("in", SizeMetric::In),
821
782405
            ("mm", SizeMetric::Mm),
822
782405
            ("cm", SizeMetric::Cm),
823
782405
            ("%", SizeMetric::Percent),
824
782405
        ],
825
    )
826
782405
}
827

            
828
/// # Errors
829
///
830
/// Returns an error if `input` is not a valid CSS `pixel-value-no-percent` value.
831
384
pub fn parse_pixel_value_no_percent(
832
384
    input: &str,
833
384
) -> Result<PixelValueNoPercent, CssPixelValueParseError<'_>> {
834
    Ok(PixelValueNoPercent {
835
384
        inner: parse_pixel_value_inner(
836
384
            input,
837
384
            &[
838
384
                // ORDER IS LOAD-BEARING -- see parse_pixel_value above.
839
384
                ("px", SizeMetric::Px),
840
384
                ("rem", SizeMetric::Rem), // before "em" ("rem" ends with "em")
841
384
                ("em", SizeMetric::Em),
842
384
                ("pt", SizeMetric::Pt),
843
384
                ("vmax", SizeMetric::Vmax),
844
384
                ("vmin", SizeMetric::Vmin), // before "in" -- "vmin" ends with "in"!
845
384
                ("vw", SizeMetric::Vw),
846
384
                ("vh", SizeMetric::Vh),
847
384
                ("in", SizeMetric::In),
848
384
                ("mm", SizeMetric::Mm),
849
384
                ("cm", SizeMetric::Cm),
850
384
            ],
851
35
        )?,
852
    })
853
384
}
854

            
855
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
856
pub enum PixelValueWithAuto {
857
    None,
858
    Initial,
859
    Inherit,
860
    Auto,
861
    Exact(PixelValue),
862
}
863

            
864
/// Parses a pixel value, but also tries values like "auto", "initial", "inherit" and "none"
865
/// # Errors
866
///
867
/// Returns an error if `input` is not a valid CSS `pixel-value-with-auto` value.
868
436180
pub fn parse_pixel_value_with_auto(
869
436180
    input: &str,
870
436180
) -> Result<PixelValueWithAuto, CssPixelValueParseError<'_>> {
871
436180
    let input = input.trim();
872
436180
    match input {
873
436180
        "none" => Ok(PixelValueWithAuto::None),
874
436176
        "initial" => Ok(PixelValueWithAuto::Initial),
875
436172
        "inherit" => Ok(PixelValueWithAuto::Inherit),
876
436166
        "auto" => Ok(PixelValueWithAuto::Auto),
877
436131
        e => Ok(PixelValueWithAuto::Exact(parse_pixel_value(e)?)),
878
    }
879
436180
}
880

            
881
// ============================================================================
882
// System Metric References (system:button-padding, system:button-radius, etc.)
883
// ============================================================================
884

            
885
/// Reference to a specific system metric value.
886
/// These are resolved at runtime based on the user's system preferences.
887
/// 
888
/// CSS syntax: `system:button-padding`, `system:button-radius`, `system:titlebar-height`, etc.
889
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
890
#[repr(C)]
891
#[derive(Default)]
892
pub enum SystemMetricRef {
893
    /// Button corner radius (system:button-radius)
894
    #[default]
895
    ButtonRadius,
896
    /// Button horizontal padding (system:button-padding-horizontal)
897
    ButtonPaddingHorizontal,
898
    /// Button vertical padding (system:button-padding-vertical)
899
    ButtonPaddingVertical,
900
    /// Button border width (system:button-border-width)
901
    ButtonBorderWidth,
902
    /// Titlebar height (system:titlebar-height)
903
    TitlebarHeight,
904
    /// Titlebar button area width (system:titlebar-button-width)
905
    TitlebarButtonWidth,
906
    /// Titlebar horizontal padding (system:titlebar-padding)
907
    TitlebarPadding,
908
    /// Safe area top inset for notched devices (system:safe-area-top)
909
    SafeAreaTop,
910
    /// Safe area bottom inset (system:safe-area-bottom)
911
    SafeAreaBottom,
912
    /// Safe area left inset (system:safe-area-left)
913
    SafeAreaLeft,
914
    /// Safe area right inset (system:safe-area-right)
915
    SafeAreaRight,
916
}
917

            
918

            
919
impl SystemMetricRef {
920
    /// Resolve this system metric reference against actual system metrics.
921
35
    #[must_use] pub const fn resolve(&self, metrics: &crate::system::SystemMetrics) -> Option<PixelValue> {
922
35
        match self {
923
5
            Self::ButtonRadius => metrics.corner_radius.as_option().copied(),
924
3
            Self::ButtonPaddingHorizontal => metrics.button_padding_horizontal.as_option().copied(),
925
3
            Self::ButtonPaddingVertical => metrics.button_padding_vertical.as_option().copied(),
926
3
            Self::ButtonBorderWidth => metrics.border_width.as_option().copied(),
927
3
            Self::TitlebarHeight => metrics.titlebar.height.as_option().copied(),
928
3
            Self::TitlebarButtonWidth => metrics.titlebar.button_area_width.as_option().copied(),
929
3
            Self::TitlebarPadding => metrics.titlebar.padding_horizontal.as_option().copied(),
930
3
            Self::SafeAreaTop => metrics.titlebar.safe_area.top.as_option().copied(),
931
3
            Self::SafeAreaBottom => metrics.titlebar.safe_area.bottom.as_option().copied(),
932
3
            Self::SafeAreaLeft => metrics.titlebar.safe_area.left.as_option().copied(),
933
3
            Self::SafeAreaRight => metrics.titlebar.safe_area.right.as_option().copied(),
934
        }
935
35
    }
936

            
937
    /// Returns the CSS string representation of this system metric reference.
938
57
    #[must_use] pub const fn as_css_str(&self) -> &'static str {
939
57
        match self {
940
5
            Self::ButtonRadius => "system:button-radius",
941
5
            Self::ButtonPaddingHorizontal => "system:button-padding-horizontal",
942
5
            Self::ButtonPaddingVertical => "system:button-padding-vertical",
943
5
            Self::ButtonBorderWidth => "system:button-border-width",
944
7
            Self::TitlebarHeight => "system:titlebar-height",
945
5
            Self::TitlebarButtonWidth => "system:titlebar-button-width",
946
5
            Self::TitlebarPadding => "system:titlebar-padding",
947
5
            Self::SafeAreaTop => "system:safe-area-top",
948
5
            Self::SafeAreaBottom => "system:safe-area-bottom",
949
5
            Self::SafeAreaLeft => "system:safe-area-left",
950
5
            Self::SafeAreaRight => "system:safe-area-right",
951
        }
952
57
    }
953

            
954
    /// Parse a system metric reference from a CSS string (without the "system:" prefix).
955
75
    #[must_use] pub fn from_css_str(s: &str) -> Option<Self> {
956
75
        match s {
957
75
            "button-radius" => Some(Self::ButtonRadius),
958
71
            "button-padding-horizontal" => Some(Self::ButtonPaddingHorizontal),
959
67
            "button-padding-vertical" => Some(Self::ButtonPaddingVertical),
960
63
            "button-border-width" => Some(Self::ButtonBorderWidth),
961
59
            "titlebar-height" => Some(Self::TitlebarHeight),
962
55
            "titlebar-button-width" => Some(Self::TitlebarButtonWidth),
963
51
            "titlebar-padding" => Some(Self::TitlebarPadding),
964
47
            "safe-area-top" => Some(Self::SafeAreaTop),
965
43
            "safe-area-bottom" => Some(Self::SafeAreaBottom),
966
39
            "safe-area-left" => Some(Self::SafeAreaLeft),
967
35
            "safe-area-right" => Some(Self::SafeAreaRight),
968
31
            _ => None,
969
        }
970
75
    }
971
}
972

            
973
impl fmt::Display for SystemMetricRef {
974
12
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
975
12
        write!(f, "{}", self.as_css_str())
976
12
    }
977
}
978

            
979
impl FormatAsCssValue for SystemMetricRef {
980
12
    fn format_as_css_value(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
981
12
        write!(f, "{}", self.as_css_str())
982
12
    }
983
}
984

            
985
/// A pixel value reference that can be either a concrete value or a system metric.
986
/// System metrics are lazily evaluated at runtime based on the user's system theme.
987
/// 
988
/// CSS syntax: `10px`, `1.5em`, `system:button-padding`, etc.
989
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
990
#[repr(C, u8)]
991
pub enum PixelValueOrSystem {
992
    /// A concrete pixel value.
993
    Value(PixelValue),
994
    /// A reference to a system metric, resolved at runtime.
995
    System(SystemMetricRef),
996
}
997

            
998
impl Default for PixelValueOrSystem {
999
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.
15
    #[must_use] pub const fn value(v: PixelValue) -> Self {
15
        Self::Value(v)
15
    }
    /// Create a new `PixelValueOrSystem` from a system metric reference.
13
    #[must_use] 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.
16
    #[must_use] pub fn resolve(&self, system_metrics: &crate::system::SystemMetrics, fallback: PixelValue) -> 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)),
                },
                ..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);
    }
    #[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:?}");
        }
    }
}