Skip to main content

azul_css/props/basic/
pixel.rs

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
13use crate::corety::{AzString, OptionF32};
14use core::fmt;
15use std::num::ParseFloatError;
16
17use 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).
24pub 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)
27pub 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)]
36pub struct NormalizedPercentage(f32);
37
38impl 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    #[must_use]
45    pub const fn new(value: f32) -> Self {
46        Self(value)
47    }
48
49    /// Create a percentage from an unnormalized value (0-100 scale)
50    ///
51    /// This divides by 100 internally, so you should use this when converting
52    /// from CSS percentage syntax like "50%" which is stored as 50.0.
53    #[inline]
54    #[must_use]
55    pub fn from_unnormalized(value: f32) -> Self {
56        Self(value / 100.0)
57    }
58
59    /// Get the raw normalized value (0.0-1.0)
60    #[inline]
61    #[must_use]
62    pub const fn get(self) -> f32 {
63        self.0
64    }
65
66    /// Resolve this percentage against a containing block size
67    ///
68    /// This multiplies the normalized percentage by the containing block size.
69    /// For example, 50% (0.5) of 640px = 320px.
70    #[inline]
71    #[must_use]
72    pub fn resolve(self, containing_block_size: f32) -> f32 {
73        self.0 * containing_block_size
74    }
75}
76
77impl fmt::Display for NormalizedPercentage {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "{}%", self.0 * 100.0)
80    }
81}
82
83/// Logical size in CSS logical coordinate system
84#[derive(Debug, Copy, Clone, PartialEq)]
85#[repr(C)]
86pub struct CssLogicalSize {
87    /// Inline-axis size (width in horizontal writing mode)
88    pub inline_size: f32,
89    /// Block-axis size (height in horizontal writing mode)
90    pub block_size: f32,
91}
92
93impl CssLogicalSize {
94    #[inline]
95    #[must_use]
96    pub const fn new(inline_size: f32, block_size: f32) -> Self {
97        Self {
98            inline_size,
99            block_size,
100        }
101    }
102
103    /// Convert to physical size (width, height) in horizontal writing mode
104    #[inline]
105    #[must_use]
106    pub const fn to_physical(self) -> PhysicalSize {
107        PhysicalSize {
108            width: self.inline_size,
109            height: self.block_size,
110        }
111    }
112}
113
114/// Physical size (always width x height, regardless of writing mode)
115#[derive(Debug, Copy, Clone, PartialEq)]
116#[repr(C)]
117pub struct PhysicalSize {
118    pub width: f32,
119    pub height: f32,
120}
121
122impl PhysicalSize {
123    #[inline]
124    #[must_use]
125    pub const fn new(width: f32, height: f32) -> Self {
126        Self { width, height }
127    }
128
129    /// Convert to logical size in horizontal writing mode
130    #[inline]
131    #[must_use]
132    pub const fn to_logical(self) -> CssLogicalSize {
133        CssLogicalSize {
134            inline_size: self.width,
135            block_size: self.height,
136        }
137    }
138}
139
140/// Context information needed to properly resolve CSS units (em, rem, %) to pixels.
141///
142/// This struct contains all the contextual information that `PixelValue::resolve()`
143/// needs to correctly convert relative units according to the CSS specification:
144///
145/// - **em** units: For most properties, em refers to the element's own computed font-size. For the
146///   font-size property itself, em refers to the parent's computed font-size.
147///
148/// - **rem** units: Always refer to the root element's computed font-size.
149///
150/// - **%** units: Percentage resolution depends on the property:
151///   - Width/height: relative to containing block dimensions
152///   - Margin/padding: relative to containing block width (even top/bottom!)
153///   - Border-radius: relative to element's own border box dimensions
154///   - Font-size: relative to parent's font-size
155#[derive(Debug, Copy, Clone)]
156pub struct ResolutionContext {
157    /// The computed font-size of the current element (for em in non-font properties)
158    pub element_font_size: f32,
159
160    /// The computed font-size of the parent element (for em in font-size property)
161    pub parent_font_size: f32,
162
163    /// The computed font-size of the root element (for rem units)
164    pub root_font_size: f32,
165
166    /// The containing block dimensions (for % in width/height/margins/padding)
167    pub containing_block_size: PhysicalSize,
168
169    /// The element's own border box size (for % in border-radius, transforms)
170    /// May be None during first layout pass before size is determined
171    pub element_size: Option<PhysicalSize>,
172
173    /// Is the element in a VERTICAL writing mode (`vertical-rl`/`vertical-lr`)?
174    /// css-writing-modes-4 §7.2: margin/padding percentages resolve against
175    /// the containing block's INLINE size - the physical HEIGHT in vertical
176    /// modes. Physical width/height percentages are unaffected.
177    pub vertical_writing_mode: bool,
178
179    /// The viewport size in CSS pixels (for vw, vh, vmin, vmax units)
180    /// This is the layout viewport size, not physical screen size
181    pub viewport_size: PhysicalSize,
182}
183
184impl Default for ResolutionContext {
185    fn default() -> Self {
186        Self {
187            element_font_size: 16.0,
188            parent_font_size: 16.0,
189            root_font_size: 16.0,
190            containing_block_size: PhysicalSize::new(0.0, 0.0),
191            element_size: None,
192            viewport_size: PhysicalSize::new(0.0, 0.0),
193            vertical_writing_mode: false,
194        }
195    }
196}
197
198impl ResolutionContext {
199    /// Create a minimal context for testing or default resolution
200    #[inline]
201    #[must_use]
202    pub const fn default_const() -> Self {
203        Self {
204            element_font_size: 16.0,
205            parent_font_size: 16.0,
206            root_font_size: 16.0,
207            containing_block_size: PhysicalSize {
208                width: 0.0,
209                height: 0.0,
210            },
211            element_size: None,
212            viewport_size: PhysicalSize {
213                width: 0.0,
214                height: 0.0,
215            },
216            vertical_writing_mode: false,
217        }
218    }
219}
220
221/// Specifies which property context we're resolving for, to determine correct reference values
222#[derive(Debug, Copy, Clone, PartialEq, Eq)]
223pub enum PropertyContext {
224    /// Resolving for the font-size property itself (em refers to parent)
225    FontSize,
226    /// Resolving for margin properties (% refers to containing block width)
227    Margin,
228    /// Resolving for padding properties (% refers to containing block width)
229    Padding,
230    /// Resolving for width or horizontal properties (% refers to containing block width)
231    Width,
232    /// Resolving for height or vertical properties (% refers to containing block height)
233    Height,
234    /// Resolving for border-width properties (only absolute lengths + em/rem, no % support)
235    BorderWidth,
236    /// Resolving for border-radius (% refers to element's own dimensions)
237    BorderRadius,
238    /// Resolving for transforms (% refers to element's own dimensions)
239    Transform,
240    /// Resolving for other properties (em refers to element font-size)
241    Other,
242}
243
244/// A CSS length value consisting of a numeric value and a unit (px, em, rem, %, etc.).
245#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
246#[repr(C)]
247pub struct PixelValue {
248    pub metric: SizeMetric,
249    pub number: FloatValue,
250}
251
252impl PixelValue {
253    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
254        self.number = FloatValue::new(self.number.get() * scale_factor);
255    }
256
257    /// Whether this value resolves to pixels with NO context.
258    ///
259    /// True for `px`, `pt`, `in`, `cm`, `mm`. False for `em`, `rem`, `%` and
260    /// the viewport units, all of which need something the value itself does
261    /// not carry.
262    #[must_use]
263    pub const fn is_absolute(&self) -> bool {
264        matches!(
265            self.metric,
266            SizeMetric::Px | SizeMetric::Pt | SizeMetric::In | SizeMetric::Cm | SizeMetric::Mm
267        )
268    }
269
270    /// Resolve to pixels WITHOUT a context, or `None` if this value needs one.
271    ///
272    /// The honest form for the many values that are absolute by construction -
273    /// a safe-area inset, a scrollbar width, a border reported by the system
274    /// theme. `CallbackInfo::get_safe_area_insets()` hands back
275    /// `OptionPixelValue`, and before this the only way out was
276    /// `p.number.get()`, which silently returns `24` for `24em` as readily as
277    /// for `24px`: correct only because the caller happened to know the value
278    /// was absolute.
279    ///
280    /// Returning `None` rather than a number is the point. A relative unit has
281    /// no pixel value until something supplies the reference, so answering
282    /// with one would be inventing it - which is exactly how
283    /// `to_pixels_internal` reports viewport units as `0.0`.
284    #[must_use]
285    pub fn to_pixels_absolute(&self) -> OptionF32 {
286        if self.is_absolute() {
287            // The resolves are unused for absolute metrics; pass zeroes rather
288            // than inventing a context.
289            OptionF32::Some(self.to_pixels_internal(0.0, 0.0, 0.0))
290        } else {
291            OptionF32::None
292        }
293    }
294
295    /// Resolve to pixels, supplying the reference each relative unit needs.
296    ///
297    /// - `percent_resolve` - the 100% reference for `%`
298    /// - `em_resolve` - the element's own font size, for `em`
299    /// - `rem_resolve` - the root font size, for `rem`
300    ///
301    /// Absolute units ignore all three, so an absolute value can be resolved
302    /// with zeroes - though [`Self::to_pixels_absolute`] says that more
303    /// clearly.
304    ///
305    /// # Viewport units
306    ///
307    /// `vw`/`vh`/`vmin`/`vmax` resolve to `0.0` here, because this signature
308    /// carries no viewport. That is a documented limitation of this entry
309    /// point rather than a correct answer; the engine's own resolution path
310    /// handles them.
311    #[must_use]
312    pub fn to_pixels(&self, percent_resolve: f32, em_resolve: f32, rem_resolve: f32) -> f32 {
313        self.to_pixels_internal(percent_resolve, em_resolve, rem_resolve)
314    }
315}
316
317impl FormatAsCssValue for PixelValue {
318    fn format_as_css_value(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        write!(f, "{}{}", self.number, self.metric)
320    }
321}
322
323impl crate::css::PrintAsCssValue for PixelValue {
324    fn print_as_css_value(&self) -> String {
325        format!("{}{}", self.number, self.metric)
326    }
327}
328
329impl crate::codegen::format::FormatAsRustCode for PixelValue {
330    fn format_as_rust_code(&self, _tabs: usize) -> String {
331        format!(
332            "PixelValue {{ metric: {:?}, number: FloatValue::new({}) }}",
333            self.metric,
334            self.number.get()
335        )
336    }
337}
338
339// Manual Debug implementation, because the auto-generated one is nearly unreadable
340impl fmt::Debug for PixelValue {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        write!(f, "{}{}", self.number, self.metric)
343    }
344}
345
346impl fmt::Display for PixelValue {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        write!(f, "{}{}", self.number, self.metric)
349    }
350}
351
352impl PixelValue {
353    #[inline]
354    #[must_use]
355    pub const fn zero() -> Self {
356        const ZERO_PX: PixelValue = PixelValue::const_px(0);
357        ZERO_PX
358    }
359
360    /// Same as `PixelValue::px()`, but only accepts whole numbers,
361    /// since using `f32` in const fn is not yet stabilized.
362    #[inline]
363    #[must_use]
364    pub const fn const_px(value: isize) -> Self {
365        Self::const_from_metric(SizeMetric::Px, value)
366    }
367
368    /// Same as `PixelValue::em()`, but only accepts whole numbers,
369    /// since using `f32` in const fn is not yet stabilized.
370    #[inline]
371    #[must_use]
372    pub const fn const_em(value: isize) -> Self {
373        Self::const_from_metric(SizeMetric::Em, value)
374    }
375
376    /// Creates an em value from a fractional number in const context.
377    ///
378    /// # Arguments
379    /// * `pre_comma` - The integer part (e.g., 1 for 1.5em)
380    /// * `post_comma` - The fractional part as digits (e.g., 5 for 0.5em, 83 for 0.83em)
381    ///
382    /// # Examples
383    /// ```
384    /// // 1.5em = const_em_fractional(1, 5)
385    /// // 0.83em = const_em_fractional(0, 83)
386    /// // 1.17em = const_em_fractional(1, 17)
387    /// ```
388    #[inline]
389    #[must_use]
390    pub const fn const_em_fractional(pre_comma: isize, post_comma: isize) -> Self {
391        Self::const_from_metric_fractional(SizeMetric::Em, pre_comma, post_comma)
392    }
393
394    /// Same as `PixelValue::pt()`, but only accepts whole numbers,
395    /// since using `f32` in const fn is not yet stabilized.
396    #[inline]
397    #[must_use]
398    pub const fn const_pt(value: isize) -> Self {
399        Self::const_from_metric(SizeMetric::Pt, value)
400    }
401
402    /// Creates a pt value from a fractional number in const context.
403    #[inline]
404    #[must_use]
405    pub const fn const_pt_fractional(pre_comma: isize, post_comma: isize) -> Self {
406        Self::const_from_metric_fractional(SizeMetric::Pt, pre_comma, post_comma)
407    }
408
409    /// Same as `PixelValue::percent()`, but only accepts whole numbers,
410    /// since using `f32` in const fn is not yet stabilized.
411    #[inline]
412    #[must_use]
413    pub const fn const_percent(value: isize) -> Self {
414        Self::const_from_metric(SizeMetric::Percent, value)
415    }
416
417    /// Same as `PixelValue::in()`, but only accepts whole numbers,
418    /// since using `f32` in const fn is not yet stabilized.
419    #[inline]
420    #[must_use]
421    pub const fn const_in(value: isize) -> Self {
422        Self::const_from_metric(SizeMetric::In, value)
423    }
424
425    /// Same as `PixelValue::cm()`, but only accepts whole numbers,
426    /// since using `f32` in const fn is not yet stabilized.
427    #[inline]
428    #[must_use]
429    pub const fn const_cm(value: isize) -> Self {
430        Self::const_from_metric(SizeMetric::Cm, value)
431    }
432
433    /// Same as `PixelValue::mm()`, but only accepts whole numbers,
434    /// since using `f32` in const fn is not yet stabilized.
435    #[inline]
436    #[must_use]
437    pub const fn const_mm(value: isize) -> Self {
438        Self::const_from_metric(SizeMetric::Mm, value)
439    }
440
441    #[inline]
442    #[must_use]
443    pub const fn const_from_metric(metric: SizeMetric, value: isize) -> Self {
444        Self {
445            metric,
446            number: FloatValue::const_new(value),
447        }
448    }
449
450    /// Creates a `PixelValue` from a fractional number in const context.
451    ///
452    /// # Arguments
453    /// * `metric` - The size metric (Px, Em, Pt, etc.)
454    /// * `pre_comma` - The integer part
455    /// * `post_comma` - The fractional part as digits
456    #[inline]
457    #[must_use]
458    pub const fn const_from_metric_fractional(
459        metric: SizeMetric,
460        pre_comma: isize,
461        post_comma: isize,
462    ) -> Self {
463        Self {
464            metric,
465            number: FloatValue::const_new_fractional(pre_comma, post_comma),
466        }
467    }
468
469    #[inline]
470    #[must_use]
471    pub fn px(value: f32) -> Self {
472        Self::from_metric(SizeMetric::Px, value)
473    }
474
475    #[inline]
476    #[must_use]
477    pub fn em(value: f32) -> Self {
478        Self::from_metric(SizeMetric::Em, value)
479    }
480
481    #[inline]
482    #[must_use]
483    pub fn inch(value: f32) -> Self {
484        Self::from_metric(SizeMetric::In, value)
485    }
486
487    #[inline]
488    #[must_use]
489    pub fn cm(value: f32) -> Self {
490        Self::from_metric(SizeMetric::Cm, value)
491    }
492
493    #[inline]
494    #[must_use]
495    pub fn mm(value: f32) -> Self {
496        Self::from_metric(SizeMetric::Mm, value)
497    }
498
499    #[inline]
500    #[must_use]
501    pub fn pt(value: f32) -> Self {
502        Self::from_metric(SizeMetric::Pt, value)
503    }
504
505    #[inline]
506    #[must_use]
507    pub fn percent(value: f32) -> Self {
508        Self::from_metric(SizeMetric::Percent, value)
509    }
510
511    #[inline]
512    #[must_use]
513    pub fn rem(value: f32) -> Self {
514        Self::from_metric(SizeMetric::Rem, value)
515    }
516
517    #[inline]
518    #[must_use]
519    pub fn from_metric(metric: SizeMetric, value: f32) -> Self {
520        Self {
521            metric,
522            number: FloatValue::new(value),
523        }
524    }
525
526    #[inline]
527    #[allow(clippy::suboptimal_flops)] // explicit FP; mul_add slower without +fma
528    #[must_use]
529    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
530        if self.metric == other.metric {
531            Self {
532                metric: self.metric,
533                number: self.number.interpolate(&other.number, t),
534            }
535        } else {
536            // Interpolate between different metrics by converting to px
537            // Note: Uses DEFAULT_FONT_SIZE for em/rem - acceptable for animation fallback
538            let self_px_interp = self.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
539            let other_px_interp =
540                other.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
541            Self::from_metric(
542                SizeMetric::Px,
543                self_px_interp + (other_px_interp - self_px_interp) * t,
544            )
545        }
546    }
547
548    /// Returns the value of the `SizeMetric` as a normalized percentage (0.0 = 0%, 1.0 = 100%)
549    ///
550    /// Returns `Some(NormalizedPercentage)` if this is a percentage value, `None` otherwise.
551    /// The returned `NormalizedPercentage` is already normalized to 0.0-1.0 range,
552    /// so you should multiply it directly with the containing block size.
553    #[inline]
554    #[must_use]
555    pub fn to_percent(&self) -> Option<NormalizedPercentage> {
556        match self.metric {
557            SizeMetric::Percent => Some(NormalizedPercentage::from_unnormalized(self.number.get())),
558            _ => None,
559        }
560    }
561
562    /// Internal fallback method for converting to pixels with manual % resolution.
563    ///
564    /// Used internally by prop_cache.rs resolve_property_dependency().
565    ///
566    /// **DO NOT USE directly!** Use `resolve_with_context()` instead for new code.
567    #[doc(hidden)]
568    #[inline]
569    #[must_use]
570    pub fn to_pixels_internal(
571        &self,
572        percent_resolve: f32,
573        em_resolve: f32,
574        rem_resolve: f32,
575    ) -> f32 {
576        match self.metric {
577            SizeMetric::Px => self.number.get(),
578            SizeMetric::Pt => self.number.get() * PT_TO_PX,
579            SizeMetric::In => self.number.get() * 96.0,
580            SizeMetric::Cm => self.number.get() * 96.0 / 2.54,
581            SizeMetric::Mm => self.number.get() * 96.0 / 25.4,
582            SizeMetric::Em => self.number.get() * em_resolve,
583            SizeMetric::Rem => self.number.get() * rem_resolve,
584            SizeMetric::Percent => {
585                NormalizedPercentage::from_unnormalized(self.number.get()).resolve(percent_resolve)
586            }
587            // Viewport units: Cannot resolve without viewport context, return 0
588            // These should use resolve_with_context() instead
589            SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => 0.0,
590        }
591    }
592
593    /// Resolve this value to pixels using proper CSS context.
594    ///
595    /// This is the **CORRECT** way to resolve CSS units. It properly handles:
596    /// - em units: Uses element's own font-size (or parent's for font-size property)
597    /// - rem units: Uses root element's font-size
598    /// - % units: Uses property-appropriate reference (containing block width/height, element size,
599    ///   etc.)
600    /// - Absolute units: px, pt, in, cm, mm (already correct)
601    ///
602    /// # Arguments
603    /// * `context` - Resolution context with font sizes and dimensions
604    /// * `property_context` - Which property we're resolving for (affects % and em resolution)
605    #[inline]
606    #[must_use]
607    pub fn resolve_with_context(
608        &self,
609        context: &ResolutionContext,
610        property_context: PropertyContext,
611    ) -> f32 {
612        match self.metric {
613            // Absolute units - already correct
614            SizeMetric::Px => self.number.get(),
615            SizeMetric::Pt => self.number.get() * PT_TO_PX,
616            SizeMetric::In => self.number.get() * 96.0,
617            SizeMetric::Cm => self.number.get() * 96.0 / 2.54,
618            SizeMetric::Mm => self.number.get() * 96.0 / 25.4,
619
620            // Em units - CRITICAL: different resolution for font-size vs other properties
621            SizeMetric::Em => {
622                let reference_font_size = if property_context == PropertyContext::FontSize {
623                    // Em on font-size refers to parent's font-size (CSS 2.1 §15.7)
624                    context.parent_font_size
625                } else {
626                    // Em on other properties refers to element's own font-size (CSS 2.1 §10.5)
627                    context.element_font_size
628                };
629                self.number.get() * reference_font_size
630            }
631
632            // Rem units - ALWAYS refer to root font-size (CSS Values 3)
633            SizeMetric::Rem => self.number.get() * context.root_font_size,
634
635            // Viewport units - refer to viewport dimensions (CSS Values 3 §6.2)
636            // 1vw = 1% of viewport width, 1vh = 1% of viewport height
637            SizeMetric::Vw => self.number.get() * context.viewport_size.width / 100.0,
638            SizeMetric::Vh => self.number.get() * context.viewport_size.height / 100.0,
639            // vmin = smaller of vw or vh
640            SizeMetric::Vmin => {
641                let min_dimension = context
642                    .viewport_size
643                    .width
644                    .min(context.viewport_size.height);
645                self.number.get() * min_dimension / 100.0
646            }
647            // vmax = larger of vw or vh
648            SizeMetric::Vmax => {
649                let max_dimension = context
650                    .viewport_size
651                    .width
652                    .max(context.viewport_size.height);
653                self.number.get() * max_dimension / 100.0
654            }
655
656            // Percent units - reference depends on property type
657            SizeMetric::Percent => {
658                // Width and Other deliberately both resolve to containing-block width but are
659                // kept as separate arms for documentation / likely future divergence.
660                #[allow(clippy::match_same_arms)]
661                let reference = match property_context {
662                    // Font-size %: refers to parent's font-size (CSS 2.1 §15.7)
663                    PropertyContext::FontSize => context.parent_font_size,
664
665                    // Width and horizontal properties: containing block width (CSS 2.1 §10.3)
666                    PropertyContext::Width => context.containing_block_size.width,
667
668                    // Height and vertical properties: containing block height (CSS 2.1 §10.5)
669                    PropertyContext::Height => context.containing_block_size.height,
670
671                    // +spec:box-model:66e123 - margin/padding % resolved against inline size (= width in horizontal-tb)
672                    // +spec:width-calculation:bef810 - margin percentages refer to containing block width (even top/bottom)
673                    // Margins: ALWAYS containing block WIDTH, even for top/bottom! (CSS 2.1 §8.3)
674                    // +spec:width-calculation:d78514 - margin percentages refer to width of containing block
675                    // Padding: ALWAYS containing block WIDTH, even for top/bottom! (CSS 2.1 §8.4)
676                    PropertyContext::Margin | PropertyContext::Padding => {
677                        // CSS3 (writing-modes-4 §7.2) upgrades CSS 2.1's
678                        // "always width" to "the INLINE size": physical width
679                        // in horizontal-tb, physical HEIGHT in vertical-rl/lr.
680                        if context.vertical_writing_mode {
681                            context.containing_block_size.height
682                        } else {
683                            context.containing_block_size.width
684                        }
685                    }
686
687                    // Border-width: % is NOT valid per CSS spec (CSS Backgrounds 3 §4.1)
688                    // Return 0.0 if someone tries to use % on border-width
689                    PropertyContext::BorderWidth => 0.0,
690
691                    // Border-radius: element's own dimensions (CSS Backgrounds 3 §5.1)
692                    // Note: More complex - horizontal % uses width, vertical % uses height
693                    // For now, use width as default
694                    PropertyContext::BorderRadius => context.element_size.map_or(0.0, |s| s.width),
695
696                    // Transforms: element's own dimensions (CSS Transforms §20.1)
697                    PropertyContext::Transform => context.element_size.map_or(0.0, |s| s.width),
698
699                    // Other properties: default to containing block width
700                    PropertyContext::Other => context.containing_block_size.width,
701                };
702
703                NormalizedPercentage::from_unnormalized(self.number.get()).resolve(reference)
704            }
705        }
706    }
707}
708
709// border-width: thin / medium / thick keyword values
710// These are the canonical CSS definitions and should be used consistently
711// across parsing and resolution.
712
713/// border-width: thin = 1px (per CSS spec)
714pub const THIN_BORDER_THICKNESS: PixelValue = PixelValue {
715    metric: SizeMetric::Px,
716    number: FloatValue { number: 1000 },
717};
718
719/// border-width: medium = 3px (per CSS spec, default)
720pub const MEDIUM_BORDER_THICKNESS: PixelValue = PixelValue {
721    metric: SizeMetric::Px,
722    number: FloatValue { number: 3000 },
723};
724
725/// border-width: thick = 5px (per CSS spec)
726pub const THICK_BORDER_THICKNESS: PixelValue = PixelValue {
727    metric: SizeMetric::Px,
728    number: FloatValue { number: 5000 },
729};
730
731/// Same as `PixelValue`, but doesn't allow a "%" sign
732#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
733#[repr(C)]
734pub struct PixelValueNoPercent {
735    pub inner: PixelValue,
736}
737
738impl PixelValueNoPercent {
739    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
740        self.inner.scale_for_dpi(scale_factor);
741    }
742}
743
744impl_option!(
745    PixelValueNoPercent,
746    OptionPixelValueNoPercent,
747    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
748);
749
750impl_option!(
751    PixelValue,
752    OptionPixelValue,
753    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
754);
755
756impl fmt::Display for PixelValueNoPercent {
757    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
758        write!(f, "{}", self.inner)
759    }
760}
761
762impl ::core::fmt::Debug for PixelValueNoPercent {
763    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
764        write!(f, "{self}")
765    }
766}
767
768impl PixelValueNoPercent {
769    /// Internal conversion to pixels (no percent support).
770    ///
771    /// Used internally by prop_cache.rs.
772    ///
773    /// **DO NOT USE directly!** Use `resolve_with_context()` on inner value instead.
774    #[doc(hidden)]
775    #[inline]
776    #[must_use]
777    pub fn to_pixels_internal(&self, em_resolve: f32, rem_resolve: f32) -> f32 {
778        self.inner.to_pixels_internal(0.0, em_resolve, rem_resolve)
779    }
780
781    #[inline]
782    #[must_use]
783    pub const fn zero() -> Self {
784        const ZERO_PXNP: PixelValueNoPercent = PixelValueNoPercent {
785            inner: PixelValue::zero(),
786        };
787        ZERO_PXNP
788    }
789}
790impl From<PixelValue> for PixelValueNoPercent {
791    fn from(e: PixelValue) -> Self {
792        Self { inner: e }
793    }
794}
795
796#[derive(Clone, PartialEq, Eq)]
797pub enum CssPixelValueParseError<'a> {
798    EmptyString,
799    NoValueGiven(&'a str, SizeMetric),
800    ValueParseErr(ParseFloatError, &'a str),
801    InvalidPixelValue(&'a str),
802}
803
804impl_debug_as_display!(CssPixelValueParseError<'a>);
805
806impl_display! { CssPixelValueParseError<'a>, {
807    EmptyString => format!("Missing [px / pt / em / %] value"),
808    NoValueGiven(input, metric) => format!("Expected floating-point pixel value, got: \"{}{}\"", input, metric),
809    ValueParseErr(err, number_str) => format!("Could not parse \"{}\" as floating-point value: \"{}\"", number_str, err),
810    InvalidPixelValue(s) => format!("Invalid pixel value: \"{}\"", s),
811}}
812
813/// Wrapper for `NoValueGiven` error in pixel value parsing.
814#[derive(Debug, Clone, PartialEq, Eq)]
815#[repr(C)]
816pub struct PixelNoValueGivenError {
817    pub value: AzString,
818    pub metric: SizeMetric,
819}
820
821/// Owned version of `CssPixelValueParseError`.
822#[derive(Debug, Clone, PartialEq, Eq)]
823#[repr(C, u8)]
824pub enum CssPixelValueParseErrorOwned {
825    EmptyString,
826    NoValueGiven(PixelNoValueGivenError),
827    ValueParseErr(ParseFloatErrorWithInput),
828    InvalidPixelValue(AzString),
829}
830
831impl CssPixelValueParseError<'_> {
832    #[must_use]
833    pub fn to_contained(&self) -> CssPixelValueParseErrorOwned {
834        match self {
835            CssPixelValueParseError::EmptyString => CssPixelValueParseErrorOwned::EmptyString,
836            CssPixelValueParseError::NoValueGiven(s, metric) => {
837                CssPixelValueParseErrorOwned::NoValueGiven(PixelNoValueGivenError {
838                    value: (*s).to_string().into(),
839                    metric: *metric,
840                })
841            }
842            CssPixelValueParseError::ValueParseErr(err, s) => {
843                CssPixelValueParseErrorOwned::ValueParseErr(ParseFloatErrorWithInput {
844                    error: err.clone().into(),
845                    input: (*s).to_string().into(),
846                })
847            }
848            CssPixelValueParseError::InvalidPixelValue(s) => {
849                CssPixelValueParseErrorOwned::InvalidPixelValue((*s).to_string().into())
850            }
851        }
852    }
853}
854
855impl CssPixelValueParseErrorOwned {
856    #[must_use]
857    pub fn to_shared(&self) -> CssPixelValueParseError<'_> {
858        match self {
859            Self::EmptyString => CssPixelValueParseError::EmptyString,
860            Self::NoValueGiven(e) => {
861                CssPixelValueParseError::NoValueGiven(e.value.as_str(), e.metric)
862            }
863            Self::ValueParseErr(e) => {
864                CssPixelValueParseError::ValueParseErr(e.error.to_std(), e.input.as_str())
865            }
866            Self::InvalidPixelValue(s) => CssPixelValueParseError::InvalidPixelValue(s.as_str()),
867        }
868    }
869}
870
871/// parses an angle value like `30deg`, `1.64rad`, `100%`, etc.
872fn parse_pixel_value_inner<'a>(
873    input: &'a str,
874    match_values: &[(&'static str, SizeMetric)],
875) -> Result<PixelValue, CssPixelValueParseError<'a>> {
876    let input = input.trim();
877
878    if input.is_empty() {
879        return Err(CssPixelValueParseError::EmptyString);
880    }
881
882    for (match_val, metric) in match_values {
883        if let Some(value) = input.strip_suffix(match_val) {
884            let value = value.trim();
885            if value.is_empty() {
886                return Err(CssPixelValueParseError::NoValueGiven(input, *metric));
887            }
888            match value.parse::<f32>() {
889                Ok(o) => {
890                    return Ok(PixelValue::from_metric(*metric, o));
891                }
892                Err(e) => {
893                    return Err(CssPixelValueParseError::ValueParseErr(e, value));
894                }
895            }
896        }
897    }
898
899    input.trim().parse::<f32>().map_or_else(
900        |_| Err(CssPixelValueParseError::InvalidPixelValue(input)),
901        |o| Ok(PixelValue::px(o)),
902    )
903}
904
905/// # Errors
906///
907/// Returns an error if `input` is not a valid CSS `pixel-value` value.
908pub fn parse_pixel_value(input: &str) -> Result<PixelValue, CssPixelValueParseError<'_>> {
909    parse_pixel_value_inner(
910        input,
911        &[
912            // ORDER IS LOAD-BEARING: matching is by `strip_suffix`, first hit wins, so
913            // any unit that is a SUFFIX of another must come after it.
914            ("px", SizeMetric::Px),
915            ("rem", SizeMetric::Rem), // before "em" ("rem" ends with "em")
916            ("em", SizeMetric::Em),
917            ("pt", SizeMetric::Pt),
918            ("vmax", SizeMetric::Vmax),
919            ("vmin", SizeMetric::Vmin), // before "in" -- "vmin" ends with "in"!
920            ("vw", SizeMetric::Vw),
921            ("vh", SizeMetric::Vh),
922            ("in", SizeMetric::In),
923            ("mm", SizeMetric::Mm),
924            ("cm", SizeMetric::Cm),
925            ("%", SizeMetric::Percent),
926        ],
927    )
928}
929
930/// # Errors
931///
932/// Returns an error if `input` is not a valid CSS `pixel-value-no-percent` value.
933pub fn parse_pixel_value_no_percent(
934    input: &str,
935) -> Result<PixelValueNoPercent, CssPixelValueParseError<'_>> {
936    Ok(PixelValueNoPercent {
937        inner: parse_pixel_value_inner(
938            input,
939            &[
940                // ORDER IS LOAD-BEARING -- see parse_pixel_value above.
941                ("px", SizeMetric::Px),
942                ("rem", SizeMetric::Rem), // before "em" ("rem" ends with "em")
943                ("em", SizeMetric::Em),
944                ("pt", SizeMetric::Pt),
945                ("vmax", SizeMetric::Vmax),
946                ("vmin", SizeMetric::Vmin), // before "in" -- "vmin" ends with "in"!
947                ("vw", SizeMetric::Vw),
948                ("vh", SizeMetric::Vh),
949                ("in", SizeMetric::In),
950                ("mm", SizeMetric::Mm),
951                ("cm", SizeMetric::Cm),
952            ],
953        )?,
954    })
955}
956
957#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
958pub enum PixelValueWithAuto {
959    None,
960    Initial,
961    Inherit,
962    Auto,
963    Exact(PixelValue),
964}
965
966/// Parses a pixel value, but also tries values like "auto", "initial", "inherit" and "none"
967/// # Errors
968///
969/// Returns an error if `input` is not a valid CSS `pixel-value-with-auto` value.
970pub fn parse_pixel_value_with_auto(
971    input: &str,
972) -> Result<PixelValueWithAuto, CssPixelValueParseError<'_>> {
973    let input = input.trim();
974    match input {
975        "none" => Ok(PixelValueWithAuto::None),
976        "initial" => Ok(PixelValueWithAuto::Initial),
977        "inherit" => Ok(PixelValueWithAuto::Inherit),
978        "auto" => Ok(PixelValueWithAuto::Auto),
979        e => Ok(PixelValueWithAuto::Exact(parse_pixel_value(e)?)),
980    }
981}
982
983// ============================================================================
984// System Metric References (system:button-padding, system:button-radius, etc.)
985// ============================================================================
986
987/// Reference to a specific system metric value.
988/// These are resolved at runtime based on the user's system preferences.
989///
990/// CSS syntax: `system:button-padding`, `system:button-radius`, `system:titlebar-height`, etc.
991#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
992#[repr(C)]
993#[derive(Default)]
994pub enum SystemMetricRef {
995    /// Button corner radius (system:button-radius)
996    #[default]
997    ButtonRadius,
998    /// Button horizontal padding (system:button-padding-horizontal)
999    ButtonPaddingHorizontal,
1000    /// Button vertical padding (system:button-padding-vertical)
1001    ButtonPaddingVertical,
1002    /// Button border width (system:button-border-width)
1003    ButtonBorderWidth,
1004    /// Titlebar height (system:titlebar-height)
1005    TitlebarHeight,
1006    /// Titlebar button area width (system:titlebar-button-width)
1007    TitlebarButtonWidth,
1008    /// Titlebar horizontal padding (system:titlebar-padding)
1009    TitlebarPadding,
1010    /// Safe area top inset for notched devices (system:safe-area-top)
1011    SafeAreaTop,
1012    /// Safe area bottom inset (system:safe-area-bottom)
1013    SafeAreaBottom,
1014    /// Safe area left inset (system:safe-area-left)
1015    SafeAreaLeft,
1016    /// Safe area right inset (system:safe-area-right)
1017    SafeAreaRight,
1018}
1019
1020impl SystemMetricRef {
1021    /// Resolve this system metric reference against actual system metrics.
1022    #[must_use]
1023    pub const fn resolve(&self, metrics: &crate::system::SystemMetrics) -> Option<PixelValue> {
1024        match self {
1025            Self::ButtonRadius => metrics.corner_radius.as_option().copied(),
1026            Self::ButtonPaddingHorizontal => metrics.button_padding_horizontal.as_option().copied(),
1027            Self::ButtonPaddingVertical => metrics.button_padding_vertical.as_option().copied(),
1028            Self::ButtonBorderWidth => metrics.border_width.as_option().copied(),
1029            Self::TitlebarHeight => metrics.titlebar.height.as_option().copied(),
1030            Self::TitlebarButtonWidth => metrics.titlebar.button_area_width.as_option().copied(),
1031            Self::TitlebarPadding => metrics.titlebar.padding_horizontal.as_option().copied(),
1032            Self::SafeAreaTop => metrics.titlebar.safe_area.top.as_option().copied(),
1033            Self::SafeAreaBottom => metrics.titlebar.safe_area.bottom.as_option().copied(),
1034            Self::SafeAreaLeft => metrics.titlebar.safe_area.left.as_option().copied(),
1035            Self::SafeAreaRight => metrics.titlebar.safe_area.right.as_option().copied(),
1036        }
1037    }
1038
1039    /// Returns the CSS string representation of this system metric reference.
1040    #[must_use]
1041    pub const fn as_css_str(&self) -> &'static str {
1042        match self {
1043            Self::ButtonRadius => "system:button-radius",
1044            Self::ButtonPaddingHorizontal => "system:button-padding-horizontal",
1045            Self::ButtonPaddingVertical => "system:button-padding-vertical",
1046            Self::ButtonBorderWidth => "system:button-border-width",
1047            Self::TitlebarHeight => "system:titlebar-height",
1048            Self::TitlebarButtonWidth => "system:titlebar-button-width",
1049            Self::TitlebarPadding => "system:titlebar-padding",
1050            Self::SafeAreaTop => "system:safe-area-top",
1051            Self::SafeAreaBottom => "system:safe-area-bottom",
1052            Self::SafeAreaLeft => "system:safe-area-left",
1053            Self::SafeAreaRight => "system:safe-area-right",
1054        }
1055    }
1056
1057    /// Parse a system metric reference from a CSS string (without the "system:" prefix).
1058    #[must_use]
1059    pub fn from_css_str(s: &str) -> Option<Self> {
1060        match s {
1061            "button-radius" => Some(Self::ButtonRadius),
1062            "button-padding-horizontal" => Some(Self::ButtonPaddingHorizontal),
1063            "button-padding-vertical" => Some(Self::ButtonPaddingVertical),
1064            "button-border-width" => Some(Self::ButtonBorderWidth),
1065            "titlebar-height" => Some(Self::TitlebarHeight),
1066            "titlebar-button-width" => Some(Self::TitlebarButtonWidth),
1067            "titlebar-padding" => Some(Self::TitlebarPadding),
1068            "safe-area-top" => Some(Self::SafeAreaTop),
1069            "safe-area-bottom" => Some(Self::SafeAreaBottom),
1070            "safe-area-left" => Some(Self::SafeAreaLeft),
1071            "safe-area-right" => Some(Self::SafeAreaRight),
1072            _ => None,
1073        }
1074    }
1075}
1076
1077impl fmt::Display for SystemMetricRef {
1078    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1079        write!(f, "{}", self.as_css_str())
1080    }
1081}
1082
1083impl FormatAsCssValue for SystemMetricRef {
1084    fn format_as_css_value(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1085        write!(f, "{}", self.as_css_str())
1086    }
1087}
1088
1089/// A pixel value reference that can be either a concrete value or a system metric.
1090/// System metrics are lazily evaluated at runtime based on the user's system theme.
1091///
1092/// CSS syntax: `10px`, `1.5em`, `system:button-padding`, etc.
1093#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
1094#[repr(C, u8)]
1095pub enum PixelValueOrSystem {
1096    /// A concrete pixel value.
1097    Value(PixelValue),
1098    /// A reference to a system metric, resolved at runtime.
1099    System(SystemMetricRef),
1100}
1101
1102impl Default for PixelValueOrSystem {
1103    fn default() -> Self {
1104        Self::Value(PixelValue::zero())
1105    }
1106}
1107
1108impl From<PixelValue> for PixelValueOrSystem {
1109    fn from(value: PixelValue) -> Self {
1110        Self::Value(value)
1111    }
1112}
1113
1114impl PixelValueOrSystem {
1115    /// Create a new `PixelValueOrSystem` from a concrete value.
1116    #[must_use]
1117    pub const fn value(v: PixelValue) -> Self {
1118        Self::Value(v)
1119    }
1120
1121    /// Create a new `PixelValueOrSystem` from a system metric reference.
1122    #[must_use]
1123    pub const fn system(s: SystemMetricRef) -> Self {
1124        Self::System(s)
1125    }
1126
1127    /// Resolve the pixel value against a `SystemMetrics` struct.
1128    /// Returns the system metric if available, or falls back to the provided default.
1129    #[must_use]
1130    pub fn resolve(
1131        &self,
1132        system_metrics: &crate::system::SystemMetrics,
1133        fallback: PixelValue,
1134    ) -> PixelValue {
1135        match self {
1136            Self::Value(v) => *v,
1137            Self::System(ref_type) => ref_type.resolve(system_metrics).unwrap_or(fallback),
1138        }
1139    }
1140}
1141
1142impl fmt::Display for PixelValueOrSystem {
1143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1144        match self {
1145            Self::Value(v) => write!(f, "{v}"),
1146            Self::System(s) => write!(f, "{s}"),
1147        }
1148    }
1149}
1150
1151impl FormatAsCssValue for PixelValueOrSystem {
1152    fn format_as_css_value(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1153        match self {
1154            Self::Value(v) => v.format_as_css_value(f),
1155            Self::System(s) => s.format_as_css_value(f),
1156        }
1157    }
1158}
1159
1160/// Parse a pixel value that may include system metric references.
1161///
1162/// Accepts: `10px`, `1.5em`, `system:button-padding`, etc.
1163#[cfg(feature = "parser")]
1164/// # Errors
1165///
1166/// Returns an error if `input` is not a valid CSS `pixel-value-or-system` value.
1167pub fn parse_pixel_value_or_system(
1168    input: &str,
1169) -> Result<PixelValueOrSystem, CssPixelValueParseError<'_>> {
1170    let input = input.trim();
1171
1172    // Check for system metric reference
1173    if let Some(metric_name) = input.strip_prefix("system:") {
1174        if let Some(metric_ref) = SystemMetricRef::from_css_str(metric_name) {
1175            return Ok(PixelValueOrSystem::System(metric_ref));
1176        }
1177        return Err(CssPixelValueParseError::InvalidPixelValue(input));
1178    }
1179
1180    // Parse as regular pixel value
1181    Ok(PixelValueOrSystem::Value(parse_pixel_value(input)?))
1182}
1183
1184#[cfg(all(test, feature = "parser"))]
1185mod tests {
1186    // Tests assert that parsed values equal the exact source literals.
1187    #![allow(clippy::float_cmp)]
1188    use super::*;
1189
1190    #[test]
1191    fn test_parse_pixel_value() {
1192        assert_eq!(parse_pixel_value("10px").unwrap(), PixelValue::px(10.0));
1193        assert_eq!(parse_pixel_value("1.5em").unwrap(), PixelValue::em(1.5));
1194        assert_eq!(parse_pixel_value("2rem").unwrap(), PixelValue::rem(2.0));
1195        assert_eq!(parse_pixel_value("-20pt").unwrap(), PixelValue::pt(-20.0));
1196        assert_eq!(parse_pixel_value("50%").unwrap(), PixelValue::percent(50.0));
1197        assert_eq!(parse_pixel_value("1in").unwrap(), PixelValue::inch(1.0));
1198        assert_eq!(parse_pixel_value("2.54cm").unwrap(), PixelValue::cm(2.54));
1199        assert_eq!(parse_pixel_value("10mm").unwrap(), PixelValue::mm(10.0));
1200        assert_eq!(parse_pixel_value("  0  ").unwrap(), PixelValue::px(0.0));
1201    }
1202
1203    #[test]
1204    fn test_resolve_with_context_em() {
1205        // Element has font-size: 32px, margin: 0.67em
1206        let context = ResolutionContext {
1207            vertical_writing_mode: false,
1208            element_font_size: 32.0,
1209            parent_font_size: 16.0,
1210            ..Default::default()
1211        };
1212
1213        // Margin em uses element's own font-size
1214        let margin = PixelValue::em(0.67);
1215        assert!(
1216            (margin.resolve_with_context(&context, PropertyContext::Margin) - 21.44).abs() < 0.01
1217        );
1218
1219        // Font-size em uses parent's font-size
1220        let font_size = PixelValue::em(2.0);
1221        assert_eq!(
1222            font_size.resolve_with_context(&context, PropertyContext::FontSize),
1223            32.0
1224        );
1225    }
1226
1227    #[test]
1228    fn test_resolve_with_context_rem() {
1229        // Root has font-size: 18px
1230        let context = ResolutionContext {
1231            vertical_writing_mode: false,
1232            element_font_size: 32.0,
1233            parent_font_size: 16.0,
1234            root_font_size: 18.0,
1235            ..Default::default()
1236        };
1237
1238        // Rem always uses root font-size, regardless of property
1239        let margin = PixelValue::rem(2.0);
1240        assert_eq!(
1241            margin.resolve_with_context(&context, PropertyContext::Margin),
1242            36.0
1243        );
1244
1245        let font_size = PixelValue::rem(1.5);
1246        assert_eq!(
1247            font_size.resolve_with_context(&context, PropertyContext::FontSize),
1248            27.0
1249        );
1250    }
1251
1252    #[test]
1253    fn test_resolve_with_context_percent_margin() {
1254        // Margin % uses containing block WIDTH (even for top/bottom!)
1255        let context = ResolutionContext {
1256            vertical_writing_mode: false,
1257            element_font_size: 16.0,
1258            parent_font_size: 16.0,
1259            root_font_size: 16.0,
1260            containing_block_size: PhysicalSize::new(800.0, 600.0),
1261            element_size: None,
1262            viewport_size: PhysicalSize::new(1920.0, 1080.0),
1263        };
1264
1265        let margin = PixelValue::percent(10.0); // 10%
1266        assert_eq!(
1267            margin.resolve_with_context(&context, PropertyContext::Margin),
1268            80.0
1269        ); // 10% of 800
1270    }
1271
1272    #[test]
1273    fn test_parse_pixel_value_no_percent() {
1274        assert_eq!(
1275            parse_pixel_value_no_percent("10px").unwrap().inner,
1276            PixelValue::px(10.0)
1277        );
1278        assert!(parse_pixel_value_no_percent("50%").is_err());
1279    }
1280
1281    #[test]
1282    fn test_parse_pixel_value_with_auto() {
1283        assert_eq!(
1284            parse_pixel_value_with_auto("10px").unwrap(),
1285            PixelValueWithAuto::Exact(PixelValue::px(10.0))
1286        );
1287        assert_eq!(
1288            parse_pixel_value_with_auto("auto").unwrap(),
1289            PixelValueWithAuto::Auto
1290        );
1291        assert_eq!(
1292            parse_pixel_value_with_auto("initial").unwrap(),
1293            PixelValueWithAuto::Initial
1294        );
1295        assert_eq!(
1296            parse_pixel_value_with_auto("inherit").unwrap(),
1297            PixelValueWithAuto::Inherit
1298        );
1299        assert_eq!(
1300            parse_pixel_value_with_auto("none").unwrap(),
1301            PixelValueWithAuto::None
1302        );
1303    }
1304
1305    #[test]
1306    fn test_parse_pixel_value_errors() {
1307        assert!(parse_pixel_value("").is_err());
1308        // Modern CSS parsers can be liberal - unitless numbers treated as px
1309        assert!(parse_pixel_value("10").is_ok()); // Parsed as 10px
1310                                                  // This parser is liberal and trims whitespace, so "10 px" is accepted
1311        assert!(parse_pixel_value("10 px").is_ok()); // Liberal parsing accepts this
1312        assert!(parse_pixel_value("px").is_err());
1313        assert!(parse_pixel_value("ten-px").is_err());
1314    }
1315}
1316
1317#[cfg(test)]
1318#[allow(
1319    clippy::float_cmp,
1320    clippy::unreadable_literal,
1321    clippy::cast_precision_loss,
1322    clippy::too_many_lines,
1323    clippy::excessive_precision
1324)]
1325mod autotest_generated {
1326    use std::{
1327        collections::hash_map::DefaultHasher,
1328        hash::{Hash, Hasher},
1329    };
1330
1331    use super::*;
1332    use crate::{
1333        codegen::format::FormatAsRustCode,
1334        css::PrintAsCssValue,
1335        props::{
1336            basic::length::{FloatValue, SizeMetric},
1337            formatter::FormatAsCssValue,
1338        },
1339        system::{SafeAreaInsets, SystemMetrics, TitlebarMetrics},
1340    };
1341
1342    /// `FloatValue` stores `f32 * 1000` truncated into an `isize`, so every value
1343    /// is quantized to 1/1000 and every `get()` is finite by construction.
1344    const MULT: f32 = 1000.0;
1345
1346    /// `const_new` multiplies by 1000 in `isize` space, so anything beyond this
1347    /// overflows the multiply (debug-panics / release-wraps). The `const_*`
1348    /// constructors are only usable up to here.
1349    const MAX_SAFE_CONST: isize = isize::MAX / 1000;
1350    const MIN_SAFE_CONST: isize = isize::MIN / 1000;
1351
1352    const ALL_METRICS: [SizeMetric; 12] = [
1353        SizeMetric::Px,
1354        SizeMetric::Pt,
1355        SizeMetric::Em,
1356        SizeMetric::Rem,
1357        SizeMetric::In,
1358        SizeMetric::Cm,
1359        SizeMetric::Mm,
1360        SizeMetric::Percent,
1361        SizeMetric::Vw,
1362        SizeMetric::Vh,
1363        SizeMetric::Vmin,
1364        SizeMetric::Vmax,
1365    ];
1366
1367    const ALL_PROPERTY_CONTEXTS: [PropertyContext; 9] = [
1368        PropertyContext::FontSize,
1369        PropertyContext::Margin,
1370        PropertyContext::Padding,
1371        PropertyContext::Width,
1372        PropertyContext::Height,
1373        PropertyContext::BorderWidth,
1374        PropertyContext::BorderRadius,
1375        PropertyContext::Transform,
1376        PropertyContext::Other,
1377    ];
1378
1379    const ALL_SYSTEM_REFS: [SystemMetricRef; 11] = [
1380        SystemMetricRef::ButtonRadius,
1381        SystemMetricRef::ButtonPaddingHorizontal,
1382        SystemMetricRef::ButtonPaddingVertical,
1383        SystemMetricRef::ButtonBorderWidth,
1384        SystemMetricRef::TitlebarHeight,
1385        SystemMetricRef::TitlebarButtonWidth,
1386        SystemMetricRef::TitlebarPadding,
1387        SystemMetricRef::SafeAreaTop,
1388        SystemMetricRef::SafeAreaBottom,
1389        SystemMetricRef::SafeAreaLeft,
1390        SystemMetricRef::SafeAreaRight,
1391    ];
1392
1393    /// The values that historically break fixed-point encoders.
1394    const EXTREME_F32: [f32; 13] = [
1395        0.0,
1396        -0.0,
1397        1.0,
1398        -1.0,
1399        f32::MIN_POSITIVE,
1400        -f32::MIN_POSITIVE,
1401        1e30,
1402        -1e30,
1403        f32::MAX,
1404        f32::MIN,
1405        f32::INFINITY,
1406        f32::NEG_INFINITY,
1407        f32::NAN,
1408    ];
1409
1410    fn approx(a: f32, b: f32) -> bool {
1411        (a - b).abs() < 0.001
1412    }
1413
1414    fn hash_of<T: Hash>(v: &T) -> u64 {
1415        let mut h = DefaultHasher::new();
1416        v.hash(&mut h);
1417        h.finish()
1418    }
1419
1420    /// Renders anything through the `FormatAsCssValue` impl, which is otherwise
1421    /// only reachable with a live `Formatter`.
1422    struct CssVal<T>(T);
1423
1424    impl<T: FormatAsCssValue> fmt::Display for CssVal<T> {
1425        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1426            self.0.format_as_css_value(f)
1427        }
1428    }
1429
1430    fn as_css_value<T: FormatAsCssValue>(v: T) -> String {
1431        CssVal(v).to_string()
1432    }
1433
1434    /// A context whose every reference value is distinct, so a resolver that
1435    /// reads the wrong field cannot accidentally produce the right number.
1436    fn distinct_context() -> ResolutionContext {
1437        ResolutionContext {
1438            vertical_writing_mode: false,
1439            element_font_size: 32.0,
1440            parent_font_size: 8.0,
1441            root_font_size: 4.0,
1442            containing_block_size: PhysicalSize::new(800.0, 600.0),
1443            element_size: Some(PhysicalSize::new(200.0, 100.0)),
1444            viewport_size: PhysicalSize::new(1000.0, 500.0),
1445        }
1446    }
1447
1448    fn populated_metrics() -> SystemMetrics {
1449        SystemMetrics {
1450            corner_radius: OptionPixelValue::Some(PixelValue::px(1.0)),
1451            border_width: OptionPixelValue::Some(PixelValue::px(2.0)),
1452            button_padding_horizontal: OptionPixelValue::Some(PixelValue::px(3.0)),
1453            button_padding_vertical: OptionPixelValue::Some(PixelValue::px(4.0)),
1454            titlebar: TitlebarMetrics {
1455                height: OptionPixelValue::Some(PixelValue::px(5.0)),
1456                button_area_width: OptionPixelValue::Some(PixelValue::px(6.0)),
1457                padding_horizontal: OptionPixelValue::Some(PixelValue::px(7.0)),
1458                safe_area: SafeAreaInsets {
1459                    top: OptionPixelValue::Some(PixelValue::px(8.0)),
1460                    bottom: OptionPixelValue::Some(PixelValue::px(9.0)),
1461                    left: OptionPixelValue::Some(PixelValue::px(10.0)),
1462                    right: OptionPixelValue::Some(PixelValue::px(11.0)),
1463                    // No keyboard inset at this site: a titlebar/desktop
1464                    // surface never has an on-screen keyboard over it.
1465                    keyboard: OptionPixelValue::None,
1466                },
1467                ..TitlebarMetrics::default()
1468            },
1469        }
1470    }
1471
1472    // ============================================================== parsers ===
1473
1474    #[test]
1475    fn parse_pixel_value_rejects_empty_and_whitespace_only() {
1476        assert_eq!(
1477            parse_pixel_value("").unwrap_err(),
1478            CssPixelValueParseError::EmptyString
1479        );
1480        for ws in ["   ", "\t\n", "\r\n\t ", "\n"] {
1481            assert_eq!(
1482                parse_pixel_value(ws).unwrap_err(),
1483                CssPixelValueParseError::EmptyString,
1484                "whitespace-only input {ws:?} must trim down to EmptyString"
1485            );
1486        }
1487    }
1488
1489    #[test]
1490    fn parse_pixel_value_rejects_a_bare_unit_with_no_number() {
1491        // Every suffix that is reachable as a bare token must report NoValueGiven
1492        // (i.e. "the unit is fine, the number is missing") rather than panicking.
1493        // "vmin" is deliberately absent — see the vmin-shadowing test below.
1494        for unit in [
1495            "px", "rem", "em", "pt", "in", "mm", "cm", "vmax", "vw", "vh", "%",
1496        ] {
1497            let err = parse_pixel_value(unit).unwrap_err();
1498            assert!(
1499                matches!(err, CssPixelValueParseError::NoValueGiven(input, _) if input == unit),
1500                "bare unit {unit:?} should be NoValueGiven, got {err:?}"
1501            );
1502        }
1503        // Whitespace between the (missing) number and the unit is trimmed too.
1504        assert!(matches!(
1505            parse_pixel_value("   px").unwrap_err(),
1506            CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
1507        ));
1508    }
1509
1510    #[test]
1511    fn parse_pixel_value_vmin_is_shadowed_by_the_in_suffix() {
1512        // FIXED (was a characterization of the bug): the suffix table used to test
1513        // "in" BEFORE "vmin", so "5vmin" stripped the "in" and failed to parse the
1514        // "5vm" remainder — every vmin length in a stylesheet was rejected. The table
1515        // now orders "vmin"/"vmax" ahead of "in".
1516        assert_eq!(
1517            parse_pixel_value("5vmin").unwrap(),
1518            PixelValue::from_metric(SizeMetric::Vmin, 5.0)
1519        );
1520        // The bare unit now reports NoValueGiven like every other bare unit, instead
1521        // of the bogus "vm" ValueParseErr.
1522        assert!(matches!(
1523            parse_pixel_value("vmin").unwrap_err(),
1524            CssPixelValueParseError::NoValueGiven(..)
1525        ));
1526
1527        // The sibling viewport units are all fine.
1528        assert_eq!(
1529            parse_pixel_value("5vmax").unwrap(),
1530            PixelValue::from_metric(SizeMetric::Vmax, 5.0)
1531        );
1532        assert_eq!(
1533            parse_pixel_value("5vw").unwrap(),
1534            PixelValue::from_metric(SizeMetric::Vw, 5.0)
1535        );
1536        assert_eq!(
1537            parse_pixel_value("5vh").unwrap(),
1538            PixelValue::from_metric(SizeMetric::Vh, 5.0)
1539        );
1540    }
1541
1542    #[test]
1543    fn parse_pixel_value_inner_proves_the_vmin_bug_is_pure_suffix_ordering() {
1544        // Same input, same two units, only the table order differs. This pins the
1545        // fix: move "vmin"/"vmax" ahead of "in" (and "em"/"rem" style ordering).
1546        let in_first: [(&'static str, SizeMetric); 2] =
1547            [("in", SizeMetric::In), ("vmin", SizeMetric::Vmin)];
1548        let vmin_first: [(&'static str, SizeMetric); 2] =
1549            [("vmin", SizeMetric::Vmin), ("in", SizeMetric::In)];
1550
1551        assert!(parse_pixel_value_inner("5vmin", &in_first).is_err());
1552        assert_eq!(
1553            parse_pixel_value_inner("5vmin", &vmin_first).unwrap(),
1554            PixelValue::from_metric(SizeMetric::Vmin, 5.0)
1555        );
1556        // ...and the reordering does not break plain inches.
1557        assert_eq!(
1558            parse_pixel_value_inner("5in", &vmin_first).unwrap(),
1559            PixelValue::inch(5.0)
1560        );
1561    }
1562
1563    #[test]
1564    fn parse_pixel_value_inner_with_an_empty_table_falls_back_to_unitless_px() {
1565        let empty: [(&'static str, SizeMetric); 0] = [];
1566
1567        // No suffix table -> only a bare float is acceptable, and it means px.
1568        assert_eq!(
1569            parse_pixel_value_inner("10", &empty).unwrap(),
1570            PixelValue::px(10.0)
1571        );
1572        assert!(matches!(
1573            parse_pixel_value_inner("10px", &empty).unwrap_err(),
1574            CssPixelValueParseError::InvalidPixelValue("10px")
1575        ));
1576        assert_eq!(
1577            parse_pixel_value_inner("", &empty).unwrap_err(),
1578            CssPixelValueParseError::EmptyString
1579        );
1580    }
1581
1582    #[test]
1583    fn parse_pixel_value_accepts_every_unit_it_advertises() {
1584        // Positive controls, including the liberal shapes this parser allows.
1585        let cases: [(&str, PixelValue); 12] = [
1586            ("10px", PixelValue::px(10.0)),
1587            ("1.5em", PixelValue::em(1.5)),
1588            ("2rem", PixelValue::rem(2.0)),
1589            ("-20pt", PixelValue::pt(-20.0)),
1590            ("50%", PixelValue::percent(50.0)),
1591            ("1in", PixelValue::inch(1.0)),
1592            ("2.54cm", PixelValue::cm(2.54)),
1593            ("10mm", PixelValue::mm(10.0)),
1594            ("+7px", PixelValue::px(7.0)),
1595            (".5px", PixelValue::px(0.5)),
1596            ("5.px", PixelValue::px(5.0)),
1597            ("1e2px", PixelValue::px(100.0)),
1598        ];
1599        for (input, expected) in cases {
1600            assert_eq!(
1601                parse_pixel_value(input).unwrap(),
1602                expected,
1603                "parsing {input:?}"
1604            );
1605        }
1606
1607        // Unitless numbers mean px, and interior/exterior whitespace is trimmed.
1608        assert_eq!(parse_pixel_value("  0  ").unwrap(), PixelValue::px(0.0));
1609        assert_eq!(parse_pixel_value("10 px").unwrap(), PixelValue::px(10.0));
1610        assert_eq!(parse_pixel_value("\t10px\n").unwrap(), PixelValue::px(10.0));
1611    }
1612
1613    #[test]
1614    fn parse_pixel_value_boundary_numbers_saturate_instead_of_overflowing() {
1615        // Signed zero collapses onto a single encoding.
1616        assert_eq!(parse_pixel_value("-0").unwrap(), PixelValue::px(0.0));
1617        assert_eq!(parse_pixel_value("-0").unwrap(), PixelValue::zero());
1618
1619        // Anything under 1/1000 of a unit quantizes away to exactly zero.
1620        assert_eq!(parse_pixel_value("0.0004px").unwrap(), PixelValue::px(0.0));
1621        assert_eq!(parse_pixel_value("-0.0009px").unwrap(), PixelValue::px(0.0));
1622        assert_eq!(parse_pixel_value("1e-40px").unwrap(), PixelValue::px(0.0));
1623
1624        // Values far past f32/isize range saturate; `get()` stays finite.
1625        for huge in ["9223372036854775807", "1e40px", "3.5e38"] {
1626            let v = parse_pixel_value(huge).unwrap();
1627            assert!(
1628                v.number.get().is_finite(),
1629                "{huge:?} leaked a non-finite value: {}",
1630                v.number.get()
1631            );
1632            assert!(v.number.get() > 0.0, "{huge:?} lost its sign");
1633        }
1634        let neg = parse_pixel_value("-1e40px").unwrap();
1635        assert!(neg.number.get().is_finite() && neg.number.get() < 0.0);
1636    }
1637
1638    #[test]
1639    fn parse_pixel_value_inherits_rusts_float_keywords() {
1640        // BUG-adjacent (spec conformance, characterized): `str::parse::<f32>`
1641        // accepts "NaN"/"infinity", so CSS that no browser would accept is taken
1642        // here. NaN sanitizes to 0px and infinity saturates, so nothing downstream
1643        // sees a non-finite length -- but neither input should have parsed at all.
1644        assert_eq!(parse_pixel_value("NaN").unwrap(), PixelValue::zero());
1645
1646        let inf = parse_pixel_value("infinity").unwrap();
1647        assert_eq!(inf, PixelValue::px(f32::INFINITY));
1648        assert!(inf.number.get().is_finite() && inf.number.get() > 0.0);
1649
1650        let neg_inf = parse_pixel_value("-infinity").unwrap();
1651        assert_eq!(neg_inf, PixelValue::px(f32::NEG_INFINITY));
1652        assert!(neg_inf.number.get().is_finite() && neg_inf.number.get() < 0.0);
1653
1654        // "inf" is accepted too. The "in" (inches) suffix does NOT eat it: a suffix
1655        // match needs the string to END in "in", and "inf" ends in "nf" -- so it
1656        // falls through to the same `str::parse::<f32>()` path as "infinity" above.
1657        let inf_short = parse_pixel_value("inf").unwrap();
1658        assert_eq!(inf_short, PixelValue::px(f32::INFINITY));
1659        assert!(inf_short.number.get().is_finite() && inf_short.number.get() > 0.0);
1660    }
1661
1662    #[test]
1663    fn parse_pixel_value_is_case_sensitive_about_units() {
1664        // Conformance gap (characterized): CSS units are case-insensitive
1665        // ("10PX" is valid CSS), but the suffix table only matches lowercase, so
1666        // these fall through to the float parser and are rejected outright.
1667        for input in ["10PX", "10Px", "10EM", "10REM", "10VMAX"] {
1668            let err = parse_pixel_value(input).unwrap_err();
1669            assert!(
1670                matches!(err, CssPixelValueParseError::InvalidPixelValue(s) if s == input),
1671                "uppercase unit {input:?} should be InvalidPixelValue, got {err:?}"
1672            );
1673        }
1674    }
1675
1676    #[test]
1677    fn parse_pixel_value_rejects_garbage_and_trailing_junk() {
1678        for input in [
1679            "ten-px",
1680            "px10",
1681            "10px;garbage",
1682            "10;",
1683            "--",
1684            "1%%",
1685            "10 20px",
1686            "#",
1687            "10px 10px",
1688            "e",
1689            "0x10px",
1690        ] {
1691            assert!(
1692                parse_pixel_value(input).is_err(),
1693                "{input:?} must not parse, got {:?}",
1694                parse_pixel_value(input)
1695            );
1696        }
1697    }
1698
1699    #[test]
1700    fn parse_pixel_value_survives_unicode() {
1701        // Multibyte input must never slice mid-codepoint or panic.
1702        for input in [
1703            "\u{1F600}",          // emoji alone
1704            "10px\u{1F600}",      // emoji suffix
1705            "10px\u{0301}",       // combining acute after the unit
1706            "\u{200B}10px",       // zero-width space (NOT trimmable whitespace)
1707            "\u{0661}\u{0660}px", // arabic-indic digits
1708            "10\u{0440}\u{0445}", // cyrillic look-alike of "px"
1709            "\u{202E}10px",       // RTL override
1710        ] {
1711            let got = parse_pixel_value(input);
1712            assert!(got.is_err(), "{input:?} must be rejected, got {got:?}");
1713        }
1714
1715        // The zero-width space specifically survives the trim and lands in the
1716        // reported remainder, which proves no byte-level slicing happened.
1717        assert!(matches!(
1718            parse_pixel_value("\u{200B}10px").unwrap_err(),
1719            CssPixelValueParseError::ValueParseErr(_, "\u{200B}10")
1720        ));
1721    }
1722
1723    #[test]
1724    fn parse_pixel_value_handles_extremely_long_and_deeply_nested_input() {
1725        // 100k digits: must terminate quickly and saturate, not hang or overflow.
1726        let long_number = format!("{}px", "9".repeat(100_000));
1727        let parsed = parse_pixel_value(&long_number).unwrap();
1728        assert!(parsed.number.get().is_finite());
1729        assert_eq!(parsed.metric, SizeMetric::Px);
1730
1731        // 100k junk bytes: rejected, no quadratic blow-up.
1732        let long_junk = "x".repeat(100_000);
1733        assert!(parse_pixel_value(&long_junk).is_err());
1734
1735        // 10k nested brackets: this parser is not recursive, so this must be a
1736        // plain rejection rather than a stack overflow.
1737        let nested = "(".repeat(10_000);
1738        assert!(matches!(
1739            parse_pixel_value(&nested).unwrap_err(),
1740            CssPixelValueParseError::InvalidPixelValue(_)
1741        ));
1742    }
1743
1744    #[test]
1745    fn parse_pixel_value_no_percent_rejects_percentages_but_keeps_the_rest() {
1746        assert_eq!(
1747            parse_pixel_value_no_percent("10px").unwrap().inner,
1748            PixelValue::px(10.0)
1749        );
1750        assert_eq!(
1751            parse_pixel_value_no_percent("5vmax").unwrap().inner,
1752            PixelValue::from_metric(SizeMetric::Vmax, 5.0)
1753        );
1754
1755        // "%" is not in the table, so it falls through to the float parser.
1756        assert!(matches!(
1757            parse_pixel_value_no_percent("50%").unwrap_err(),
1758            CssPixelValueParseError::InvalidPixelValue("50%")
1759        ));
1760        assert!(matches!(
1761            parse_pixel_value_no_percent("%").unwrap_err(),
1762            CssPixelValueParseError::InvalidPixelValue("%")
1763        ));
1764
1765        assert_eq!(
1766            parse_pixel_value_no_percent("").unwrap_err(),
1767            CssPixelValueParseError::EmptyString
1768        );
1769        assert_eq!(
1770            parse_pixel_value_no_percent("   ").unwrap_err(),
1771            CssPixelValueParseError::EmptyString
1772        );
1773        assert!(parse_pixel_value_no_percent("\u{1F600}").is_err());
1774        // FIXED: "5vmin" now parses (the suffix table orders "vmin" before "in").
1775        assert_eq!(
1776            parse_pixel_value_no_percent("5vmin").unwrap().inner,
1777            PixelValue::from_metric(SizeMetric::Vmin, 5.0)
1778        );
1779    }
1780
1781    #[test]
1782    fn parse_pixel_value_with_auto_keywords_and_fallthrough() {
1783        assert_eq!(
1784            parse_pixel_value_with_auto("auto").unwrap(),
1785            PixelValueWithAuto::Auto
1786        );
1787        assert_eq!(
1788            parse_pixel_value_with_auto("  initial  ").unwrap(),
1789            PixelValueWithAuto::Initial
1790        );
1791        assert_eq!(
1792            parse_pixel_value_with_auto("\tinherit\n").unwrap(),
1793            PixelValueWithAuto::Inherit
1794        );
1795        assert_eq!(
1796            parse_pixel_value_with_auto("none").unwrap(),
1797            PixelValueWithAuto::None
1798        );
1799        assert_eq!(
1800            parse_pixel_value_with_auto("10px").unwrap(),
1801            PixelValueWithAuto::Exact(PixelValue::px(10.0))
1802        );
1803
1804        // Keywords are matched case-sensitively (CSS says they should not be).
1805        for input in ["AUTO", "Auto", "INHERIT", "None"] {
1806            assert!(
1807                parse_pixel_value_with_auto(input).is_err(),
1808                "{input:?} unexpectedly matched a keyword"
1809            );
1810        }
1811
1812        // Empty / junk / unicode all funnel into the pixel-value errors.
1813        assert_eq!(
1814            parse_pixel_value_with_auto("").unwrap_err(),
1815            CssPixelValueParseError::EmptyString
1816        );
1817        assert_eq!(
1818            parse_pixel_value_with_auto(" \t ").unwrap_err(),
1819            CssPixelValueParseError::EmptyString
1820        );
1821        assert!(parse_pixel_value_with_auto("auto;garbage").is_err());
1822        assert!(parse_pixel_value_with_auto("\u{1F600}").is_err());
1823        assert!(parse_pixel_value_with_auto(&"(".repeat(10_000)).is_err());
1824    }
1825
1826    #[cfg(feature = "parser")]
1827    #[test]
1828    fn parse_pixel_value_or_system_accepts_every_system_ref() {
1829        for r in ALL_SYSTEM_REFS {
1830            let css = r.as_css_str(); // already carries the "system:" prefix
1831            assert_eq!(
1832                parse_pixel_value_or_system(css).unwrap(),
1833                PixelValueOrSystem::System(r),
1834                "round-tripping {css:?}"
1835            );
1836            // Surrounding whitespace is trimmed before the prefix check.
1837            assert_eq!(
1838                parse_pixel_value_or_system(&format!("  {css}  ")).unwrap(),
1839                PixelValueOrSystem::System(r)
1840            );
1841        }
1842
1843        // Plain lengths still work.
1844        assert_eq!(
1845            parse_pixel_value_or_system("10px").unwrap(),
1846            PixelValueOrSystem::Value(PixelValue::px(10.0))
1847        );
1848        assert_eq!(
1849            parse_pixel_value_or_system("1.5em").unwrap(),
1850            PixelValueOrSystem::Value(PixelValue::em(1.5))
1851        );
1852    }
1853
1854    #[cfg(feature = "parser")]
1855    #[test]
1856    fn parse_pixel_value_or_system_rejects_malformed_system_refs() {
1857        // DOC BUG (characterized): the doc comment on `parse_pixel_value_or_system`
1858        // and on `PixelValueOrSystem` advertises `system:button-padding`, but
1859        // `from_css_str` only knows the -horizontal / -vertical spellings, so the
1860        // documented example is rejected.
1861        assert!(matches!(
1862            parse_pixel_value_or_system("system:button-padding").unwrap_err(),
1863            CssPixelValueParseError::InvalidPixelValue("system:button-padding")
1864        ));
1865
1866        for input in [
1867            "system:",                // empty metric name
1868            "system:unknown",         // unknown metric
1869            "system: button-radius",  // no inner trim after the colon
1870            "system:BUTTON-RADIUS",   // case-sensitive
1871            "system:button-radius;x", // trailing junk
1872            "system:\u{1F600}",       // unicode metric name
1873        ] {
1874            let err = parse_pixel_value_or_system(input).unwrap_err();
1875            assert!(
1876                matches!(err, CssPixelValueParseError::InvalidPixelValue(s) if s == input),
1877                "{input:?} should be InvalidPixelValue, got {err:?}"
1878            );
1879        }
1880
1881        // Without the exact lowercase prefix it is treated as a length, and fails
1882        // as one.
1883        assert!(matches!(
1884            parse_pixel_value_or_system("SYSTEM:button-radius").unwrap_err(),
1885            CssPixelValueParseError::InvalidPixelValue("SYSTEM:button-radius")
1886        ));
1887        assert_eq!(
1888            parse_pixel_value_or_system("").unwrap_err(),
1889            CssPixelValueParseError::EmptyString
1890        );
1891        assert_eq!(
1892            parse_pixel_value_or_system("   ").unwrap_err(),
1893            CssPixelValueParseError::EmptyString
1894        );
1895
1896        // A pathologically long metric name must be rejected, not hang.
1897        let long = format!("system:{}", "a".repeat(100_000));
1898        assert!(parse_pixel_value_or_system(&long).is_err());
1899    }
1900
1901    // ============================================== parse-error round-trips ===
1902
1903    #[test]
1904    fn parse_errors_survive_the_owned_round_trip() {
1905        let float_err = "x".parse::<f32>().unwrap_err();
1906        let errors = [
1907            CssPixelValueParseError::EmptyString,
1908            CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px),
1909            CssPixelValueParseError::ValueParseErr(float_err, "abc"),
1910            CssPixelValueParseError::InvalidPixelValue("ten-px"),
1911        ];
1912
1913        for err in errors {
1914            let owned = err.to_contained();
1915            let shared = owned.to_shared();
1916            assert_eq!(shared, err, "to_contained -> to_shared must be lossless");
1917            // Display must survive too, and never be empty.
1918            assert!(!err.to_string().is_empty());
1919            assert_eq!(shared.to_string(), err.to_string());
1920        }
1921    }
1922
1923    #[test]
1924    fn parse_errors_round_trip_from_real_parse_failures() {
1925        // The same path, but with errors that actually came out of the parser
1926        // (including a unicode-bearing remainder).
1927        for input in ["", "px", "\u{200B}10px", "ten-px", "%"] {
1928            let err = parse_pixel_value(input).unwrap_err();
1929            let owned = err.to_contained();
1930            assert_eq!(owned.to_shared(), err, "round-trip failed for {input:?}");
1931        }
1932    }
1933
1934    // ============================================================= numeric ===
1935
1936    #[test]
1937    fn float_constructors_never_leak_a_non_finite_value() {
1938        // NaN must sanitize to 0 and infinities must saturate: `FloatValue`
1939        // decodes through an isize, so `get()` is finite for *every* input.
1940        type Ctor = (fn(f32) -> PixelValue, SizeMetric);
1941        let ctors: [Ctor; 8] = [
1942            (PixelValue::px, SizeMetric::Px),
1943            (PixelValue::em, SizeMetric::Em),
1944            (PixelValue::pt, SizeMetric::Pt),
1945            (PixelValue::inch, SizeMetric::In),
1946            (PixelValue::cm, SizeMetric::Cm),
1947            (PixelValue::mm, SizeMetric::Mm),
1948            (PixelValue::percent, SizeMetric::Percent),
1949            (PixelValue::rem, SizeMetric::Rem),
1950        ];
1951
1952        for (ctor, metric) in ctors {
1953            for v in EXTREME_F32 {
1954                let px = ctor(v);
1955                assert_eq!(px.metric, metric, "constructor lost its metric for {v}");
1956                assert!(
1957                    px.number.get().is_finite(),
1958                    "{metric:?} constructor leaked a non-finite value for input {v}"
1959                );
1960            }
1961            assert_eq!(ctor(f32::NAN).number.get(), 0.0, "NaN must sanitize to 0");
1962            assert!(ctor(f32::INFINITY).number.get() > 0.0);
1963            assert!(ctor(f32::NEG_INFINITY).number.get() < 0.0);
1964        }
1965
1966        // from_metric agrees with the named constructors for every metric.
1967        for metric in ALL_METRICS {
1968            for v in EXTREME_F32 {
1969                let px = PixelValue::from_metric(metric, v);
1970                assert_eq!(px.metric, metric);
1971                assert!(px.number.get().is_finite());
1972            }
1973            assert_eq!(
1974                PixelValue::from_metric(metric, 12.0),
1975                PixelValue {
1976                    metric,
1977                    number: FloatValue::new(12.0)
1978                }
1979            );
1980        }
1981    }
1982
1983    #[test]
1984    fn float_constructors_quantize_to_one_thousandth() {
1985        // Sub-milli magnitudes vanish entirely...
1986        assert_eq!(PixelValue::px(0.0004).number.get(), 0.0);
1987        assert_eq!(PixelValue::px(-0.0009).number.get(), 0.0);
1988        // ...and the excess precision above 1/1000 is dropped, not rounded up.
1989        assert_eq!(PixelValue::px(1.0005).number.get(), 1.0);
1990
1991        // Signed zero normalizes, which keeps Eq/Hash total (PixelValue is Eq +
1992        // Hash despite wrapping a float).
1993        assert_eq!(PixelValue::px(-0.0), PixelValue::px(0.0));
1994        assert_eq!(
1995            hash_of(&PixelValue::px(-0.0)),
1996            hash_of(&PixelValue::px(0.0))
1997        );
1998        // Even NaN is reflexive here, because it sanitizes to the zero encoding.
1999        assert_eq!(PixelValue::px(f32::NAN), PixelValue::px(f32::NAN));
2000        assert_eq!(PixelValue::px(f32::NAN), PixelValue::zero());
2001    }
2002
2003    #[test]
2004    fn const_constructors_agree_with_their_float_twins() {
2005        assert_eq!(PixelValue::const_px(5), PixelValue::px(5.0));
2006        assert_eq!(PixelValue::const_em(5), PixelValue::em(5.0));
2007        assert_eq!(PixelValue::const_pt(5), PixelValue::pt(5.0));
2008        assert_eq!(PixelValue::const_percent(5), PixelValue::percent(5.0));
2009        assert_eq!(PixelValue::const_in(5), PixelValue::inch(5.0));
2010        assert_eq!(PixelValue::const_cm(5), PixelValue::cm(5.0));
2011        assert_eq!(PixelValue::const_mm(5), PixelValue::mm(5.0));
2012
2013        assert_eq!(PixelValue::const_px(0), PixelValue::zero());
2014        assert_eq!(PixelValue::const_px(-7), PixelValue::px(-7.0));
2015
2016        for metric in ALL_METRICS {
2017            assert_eq!(
2018                PixelValue::const_from_metric(metric, 7),
2019                PixelValue::from_metric(metric, 7.0),
2020                "const_from_metric disagrees with from_metric for {metric:?}"
2021            );
2022            assert_eq!(
2023                PixelValue::const_from_metric(metric, -7),
2024                PixelValue::from_metric(metric, -7.0)
2025            );
2026        }
2027    }
2028
2029    #[test]
2030    fn const_constructors_are_usable_up_to_the_documented_isize_bound() {
2031        // `const_new` scales by 1000 in isize space, so MAX_SAFE_CONST is the
2032        // largest input that does not overflow the multiply. (Anything beyond it
2033        // -- e.g. `const_px(isize::MAX)` -- overflows: a debug panic and a
2034        // release wrap. Not exercised here because the two builds disagree.)
2035        for v in [0, 1, -1, MAX_SAFE_CONST, MIN_SAFE_CONST] {
2036            let px = PixelValue::const_px(v);
2037            assert!(
2038                px.number.get().is_finite(),
2039                "const_px({v}) leaked a non-finite value"
2040            );
2041        }
2042        assert!(PixelValue::const_px(MAX_SAFE_CONST).number.get() > 0.0);
2043        assert!(PixelValue::const_px(MIN_SAFE_CONST).number.get() < 0.0);
2044        assert_eq!(
2045            PixelValue::const_px(MAX_SAFE_CONST).number.number(),
2046            MAX_SAFE_CONST * 1000
2047        );
2048    }
2049
2050    #[test]
2051    fn const_fractional_constructors_match_their_documented_examples() {
2052        // The doc-comment examples on `const_em_fractional`.
2053        assert!(approx(
2054            PixelValue::const_em_fractional(1, 5).number.get(),
2055            1.5
2056        ));
2057        assert!(approx(
2058            PixelValue::const_em_fractional(0, 83).number.get(),
2059            0.83
2060        ));
2061        assert!(approx(
2062            PixelValue::const_em_fractional(1, 17).number.get(),
2063            1.17
2064        ));
2065        assert_eq!(PixelValue::const_em_fractional(1, 5).metric, SizeMetric::Em);
2066        assert_eq!(PixelValue::const_pt_fractional(1, 5).metric, SizeMetric::Pt);
2067        assert!(approx(
2068            PixelValue::const_pt_fractional(2, 25).number.get(),
2069            2.25
2070        ));
2071
2072        // Zero fraction, and the negative case (the sign must reach the fraction:
2073        // -1.5, not -1 + 0.5 = -0.5).
2074        assert_eq!(
2075            PixelValue::const_from_metric_fractional(SizeMetric::Px, 0, 0),
2076            PixelValue::zero()
2077        );
2078        assert!(approx(
2079            PixelValue::const_from_metric_fractional(SizeMetric::Px, -1, 5)
2080                .number
2081                .get(),
2082            -1.5
2083        ));
2084
2085        // More than 3 decimals truncates to 3 (documented), rather than
2086        // overflowing the fixed-point encoding.
2087        assert!(approx(
2088            PixelValue::const_from_metric_fractional(SizeMetric::Px, 1, 5234)
2089                .number
2090                .get(),
2091            1.523
2092        ));
2093
2094        // A pathological fraction must still land somewhere finite. (isize::MIN is
2095        // NOT exercised: negating it overflows inside the digit-counting code.)
2096        let extreme = PixelValue::const_from_metric_fractional(SizeMetric::Px, 0, isize::MAX);
2097        assert!(extreme.number.get().is_finite());
2098    }
2099
2100    #[test]
2101    fn scale_for_dpi_is_defined_for_every_scale_factor() {
2102        let mut doubled = PixelValue::px(10.0);
2103        doubled.scale_for_dpi(2.0);
2104        assert_eq!(doubled, PixelValue::px(20.0));
2105
2106        // Scaling compounds (it is not idempotent) -- worth pinning, since a
2107        // double-applied DPI scale is a classic layout bug.
2108        doubled.scale_for_dpi(2.0);
2109        assert_eq!(doubled, PixelValue::px(40.0));
2110
2111        let mut zeroed = PixelValue::em(3.0);
2112        zeroed.scale_for_dpi(0.0);
2113        assert_eq!(zeroed, PixelValue::em(0.0));
2114        assert_eq!(zeroed.metric, SizeMetric::Em, "metric must be preserved");
2115
2116        let mut flipped = PixelValue::px(10.0);
2117        flipped.scale_for_dpi(-1.5);
2118        assert_eq!(flipped, PixelValue::px(-15.0));
2119
2120        // NaN collapses to zero, infinities saturate -- never a non-finite length.
2121        let mut nan_scaled = PixelValue::px(10.0);
2122        nan_scaled.scale_for_dpi(f32::NAN);
2123        assert_eq!(nan_scaled.number.get(), 0.0);
2124
2125        let mut inf_scaled = PixelValue::px(10.0);
2126        inf_scaled.scale_for_dpi(f32::INFINITY);
2127        assert!(inf_scaled.number.get().is_finite() && inf_scaled.number.get() > 0.0);
2128
2129        let mut max_scaled = PixelValue::px(f32::MAX);
2130        max_scaled.scale_for_dpi(f32::MAX);
2131        assert!(max_scaled.number.get().is_finite());
2132
2133        // The no-percent wrapper just delegates.
2134        let mut wrapped = PixelValueNoPercent::from(PixelValue::px(10.0));
2135        wrapped.scale_for_dpi(2.5);
2136        assert_eq!(wrapped.inner, PixelValue::px(25.0));
2137
2138        let mut wrapped_nan = PixelValueNoPercent::from(PixelValue::px(10.0));
2139        wrapped_nan.scale_for_dpi(f32::NAN);
2140        assert_eq!(wrapped_nan.inner.number.get(), 0.0);
2141    }
2142
2143    #[test]
2144    fn interpolate_within_one_metric_keeps_that_metric() {
2145        let a = PixelValue::em(1.0);
2146        let b = PixelValue::em(3.0);
2147
2148        assert_eq!(a.interpolate(&b, 0.0), a);
2149        assert_eq!(a.interpolate(&b, 1.0), b);
2150        assert_eq!(a.interpolate(&b, 0.5), PixelValue::em(2.0));
2151
2152        // Out-of-range t extrapolates rather than clamping.
2153        assert_eq!(a.interpolate(&b, 2.0), PixelValue::em(5.0));
2154        assert_eq!(a.interpolate(&b, -1.0), PixelValue::em(-1.0));
2155
2156        // Percent stays percent (it is NOT converted to px on the same-metric path).
2157        let p = PixelValue::percent(0.0).interpolate(&PixelValue::percent(100.0), 0.5);
2158        assert_eq!(p, PixelValue::percent(50.0));
2159        assert_eq!(p.metric, SizeMetric::Percent);
2160
2161        // Non-finite t sanitizes to the zero encoding instead of poisoning layout.
2162        let nan_t = a.interpolate(&b, f32::NAN);
2163        assert_eq!(nan_t.number.get(), 0.0);
2164        assert_eq!(nan_t.metric, SizeMetric::Em);
2165        assert!(a.interpolate(&b, f32::INFINITY).number.get().is_finite());
2166    }
2167
2168    #[test]
2169    fn interpolate_across_metrics_falls_back_to_px() {
2170        // Mixed metrics resolve through `to_pixels_internal` with DEFAULT_FONT_SIZE.
2171        let from_px = PixelValue::px(0.0);
2172        let to_em = PixelValue::em(1.0); // 16px at the default font size
2173        let mid = from_px.interpolate(&to_em, 0.5);
2174        assert_eq!(mid.metric, SizeMetric::Px);
2175        assert!(approx(mid.number.get(), DEFAULT_FONT_SIZE / 2.0));
2176
2177        assert!(approx(
2178            PixelValue::px(0.0)
2179                .interpolate(&PixelValue::pt(72.0), 1.0)
2180                .number
2181                .get(),
2182            96.0
2183        ));
2184
2185        // Percent and every viewport unit resolve to 0px on this path, because the
2186        // fallback has no containing block and no viewport. Documented as an
2187        // "acceptable animation fallback" -- pinned so it stays deliberate.
2188        for metric in [
2189            SizeMetric::Percent,
2190            SizeMetric::Vw,
2191            SizeMetric::Vh,
2192            SizeMetric::Vmin,
2193            SizeMetric::Vmax,
2194        ] {
2195            let other = PixelValue::from_metric(metric, 50.0);
2196            let done = PixelValue::px(100.0).interpolate(&other, 1.0);
2197            assert_eq!(
2198                done,
2199                PixelValue::px(0.0),
2200                "{metric:?} should collapse to 0px on the cross-metric path"
2201            );
2202        }
2203
2204        let nan_t = PixelValue::px(0.0).interpolate(&PixelValue::em(1.0), f32::NAN);
2205        assert_eq!(nan_t.number.get(), 0.0);
2206    }
2207
2208    /// The gap this closes: an app handed an `OptionPixelValue` (which is what
2209    /// `get_safe_area_insets()` returns) had NO sanctioned way to reach a
2210    /// number. `p.number.get()` worked only because the caller happened to
2211    /// know the value was absolute - it reports `24` for `24em` just as
2212    /// readily, and no type says otherwise.
2213    #[test]
2214    fn to_pixels_absolute_resolves_absolute_units_and_refuses_relative_ones() {
2215        for (v, expected) in [
2216            (PixelValue::px(24.0), 24.0),
2217            (PixelValue::pt(12.0), 12.0 * PT_TO_PX),
2218            (PixelValue::from_metric(SizeMetric::In, 1.0), 96.0),
2219            (PixelValue::from_metric(SizeMetric::Cm, 2.54), 96.0),
2220            (PixelValue::from_metric(SizeMetric::Mm, 25.4), 96.0),
2221        ] {
2222            match v.to_pixels_absolute() {
2223                OptionF32::Some(px) => assert!(
2224                    (px - expected).abs() < 0.001,
2225                    "{v:?} resolved to {px}, expected {expected}"
2226                ),
2227                OptionF32::None => panic!("{v:?} is absolute and must resolve"),
2228            }
2229        }
2230    }
2231
2232    /// A relative unit has no pixel value until something supplies the
2233    /// reference, so answering with a number would be INVENTING one - which is
2234    /// precisely how `to_pixels_internal` reports every viewport unit as 0.0.
2235    #[test]
2236    fn to_pixels_absolute_returns_none_for_every_context_dependent_unit() {
2237        for v in [
2238            PixelValue::em(2.0),
2239            PixelValue::rem(2.0),
2240            PixelValue::percent(50.0),
2241            PixelValue::from_metric(SizeMetric::Vw, 50.0),
2242            PixelValue::from_metric(SizeMetric::Vh, 50.0),
2243            PixelValue::from_metric(SizeMetric::Vmin, 50.0),
2244            PixelValue::from_metric(SizeMetric::Vmax, 50.0),
2245        ] {
2246            assert_eq!(
2247                v.to_pixels_absolute(),
2248                OptionF32::None,
2249                "{v:?} needs a context and must not answer with a number"
2250            );
2251        }
2252    }
2253
2254    /// `is_absolute` is the predicate `to_pixels_absolute` is built on, so the
2255    /// two must never disagree - otherwise a value could claim to need no
2256    /// context and then refuse to resolve.
2257    #[test]
2258    fn is_absolute_agrees_with_to_pixels_absolute() {
2259        for v in [
2260            PixelValue::px(1.0),
2261            PixelValue::pt(1.0),
2262            PixelValue::from_metric(SizeMetric::In, 1.0),
2263            PixelValue::from_metric(SizeMetric::Cm, 1.0),
2264            PixelValue::from_metric(SizeMetric::Mm, 1.0),
2265            PixelValue::em(1.0),
2266            PixelValue::rem(1.0),
2267            PixelValue::percent(1.0),
2268            PixelValue::from_metric(SizeMetric::Vw, 1.0),
2269            PixelValue::from_metric(SizeMetric::Vh, 1.0),
2270            PixelValue::from_metric(SizeMetric::Vmin, 1.0),
2271            PixelValue::from_metric(SizeMetric::Vmax, 1.0),
2272        ] {
2273            assert_eq!(
2274                v.is_absolute(),
2275                v.to_pixels_absolute() != OptionF32::None,
2276                "{v:?}: is_absolute() and to_pixels_absolute() disagree"
2277            );
2278        }
2279    }
2280
2281    /// The public `to_pixels` must be the same function the engine uses, not a
2282    /// second implementation that can drift from it.
2283    #[test]
2284    fn to_pixels_matches_the_internal_resolver() {
2285        for v in [
2286            PixelValue::px(3.0),
2287            PixelValue::em(3.0),
2288            PixelValue::rem(3.0),
2289            PixelValue::percent(50.0),
2290        ] {
2291            assert_eq!(
2292                v.to_pixels(200.0, 16.0, 10.0),
2293                v.to_pixels_internal(200.0, 16.0, 10.0),
2294                "{v:?} diverged from the internal resolver"
2295            );
2296        }
2297    }
2298
2299    #[test]
2300    fn to_pixels_internal_converts_every_absolute_and_relative_unit() {
2301        assert_eq!(
2302            PixelValue::px(10.0).to_pixels_internal(0.0, 16.0, 16.0),
2303            10.0
2304        );
2305        assert!(approx(
2306            PixelValue::pt(72.0).to_pixels_internal(0.0, 16.0, 16.0),
2307            96.0
2308        ));
2309        assert!(approx(
2310            PixelValue::inch(1.0).to_pixels_internal(0.0, 16.0, 16.0),
2311            96.0
2312        ));
2313        assert!(approx(
2314            PixelValue::cm(2.54).to_pixels_internal(0.0, 16.0, 16.0),
2315            96.0
2316        ));
2317        assert!(approx(
2318            PixelValue::mm(25.4).to_pixels_internal(0.0, 16.0, 16.0),
2319            96.0
2320        ));
2321        assert_eq!(PT_TO_PX, 96.0 / 72.0);
2322
2323        // em and rem read different resolves (this legacy path is the one that
2324        // historically conflated them, so pin that they are separate arguments).
2325        assert_eq!(
2326            PixelValue::em(2.0).to_pixels_internal(0.0, 10.0, 100.0),
2327            20.0
2328        );
2329        assert_eq!(
2330            PixelValue::rem(2.0).to_pixels_internal(0.0, 10.0, 100.0),
2331            200.0
2332        );
2333
2334        // % divides by 100 exactly once (the double-division bug this module's
2335        // NormalizedPercentage exists to prevent).
2336        assert_eq!(
2337            PixelValue::percent(50.0).to_pixels_internal(800.0, 16.0, 16.0),
2338            400.0
2339        );
2340        assert_eq!(
2341            PixelValue::percent(0.0).to_pixels_internal(800.0, 16.0, 16.0),
2342            0.0
2343        );
2344        assert_eq!(
2345            PixelValue::percent(-50.0).to_pixels_internal(800.0, 16.0, 16.0),
2346            -400.0
2347        );
2348
2349        // Viewport units have no viewport here, so they are defined as 0 -- even
2350        // when the caller passes perfectly good resolves.
2351        for metric in [
2352            SizeMetric::Vw,
2353            SizeMetric::Vh,
2354            SizeMetric::Vmin,
2355            SizeMetric::Vmax,
2356        ] {
2357            assert_eq!(
2358                PixelValue::from_metric(metric, 50.0).to_pixels_internal(800.0, 16.0, 16.0),
2359                0.0,
2360                "{metric:?} must resolve to 0 on the legacy path"
2361            );
2362        }
2363    }
2364
2365    #[test]
2366    fn to_pixels_internal_non_finite_resolves_are_defined_not_panics() {
2367        // The *result* of this method is a raw f32 (it is not re-encoded), so it
2368        // can go non-finite. Assert it does so predictably rather than panicking.
2369        assert!(PixelValue::em(1.0)
2370            .to_pixels_internal(0.0, f32::NAN, 0.0)
2371            .is_nan());
2372        assert!(PixelValue::rem(1.0)
2373            .to_pixels_internal(0.0, 0.0, f32::INFINITY)
2374            .is_infinite());
2375        assert!(PixelValue::percent(50.0)
2376            .to_pixels_internal(f32::INFINITY, 16.0, 16.0)
2377            .is_infinite());
2378        assert!(PixelValue::percent(50.0)
2379            .to_pixels_internal(f32::NAN, 16.0, 16.0)
2380            .is_nan());
2381        // 0% of an infinite containing block is NaN, not 0 -- worth knowing.
2382        assert!(PixelValue::percent(0.0)
2383            .to_pixels_internal(f32::INFINITY, 16.0, 16.0)
2384            .is_nan());
2385
2386        // A saturated length times a huge resolve overflows to +inf (no panic).
2387        assert!(PixelValue::em(f32::MAX)
2388            .to_pixels_internal(0.0, f32::MAX, 0.0)
2389            .is_infinite());
2390
2391        // Absolute units are always finite, whatever the resolves are.
2392        for v in EXTREME_F32 {
2393            assert!(PixelValue::px(v)
2394                .to_pixels_internal(f32::NAN, f32::NAN, f32::NAN)
2395                .is_finite());
2396        }
2397    }
2398
2399    #[test]
2400    fn pixel_value_no_percent_to_pixels_internal_zeroes_out_percentages() {
2401        assert_eq!(
2402            PixelValueNoPercent::from(PixelValue::px(10.0)).to_pixels_internal(16.0, 16.0),
2403            10.0
2404        );
2405        assert_eq!(
2406            PixelValueNoPercent::from(PixelValue::em(2.0)).to_pixels_internal(10.0, 100.0),
2407            20.0
2408        );
2409        assert_eq!(
2410            PixelValueNoPercent::from(PixelValue::rem(2.0)).to_pixels_internal(10.0, 100.0),
2411            200.0
2412        );
2413
2414        // The type forbids "%" at the *parser* level, but `From<PixelValue>` can
2415        // still smuggle one in. It resolves against a 0 containing block -> 0px.
2416        assert_eq!(
2417            PixelValueNoPercent::from(PixelValue::percent(50.0)).to_pixels_internal(16.0, 16.0),
2418            0.0
2419        );
2420        assert_eq!(
2421            PixelValueNoPercent::zero().to_pixels_internal(16.0, 16.0),
2422            0.0
2423        );
2424        assert_eq!(PixelValueNoPercent::zero().inner, PixelValue::zero());
2425        assert_eq!(PixelValueNoPercent::default().inner, PixelValue::zero());
2426    }
2427
2428    // =================================================== getters/predicates ===
2429
2430    #[test]
2431    fn to_percent_is_some_only_for_the_percent_metric() {
2432        for metric in ALL_METRICS {
2433            let v = PixelValue::from_metric(metric, 50.0);
2434            if metric == SizeMetric::Percent {
2435                assert_eq!(
2436                    v.to_percent().unwrap().get(),
2437                    0.5,
2438                    "50% must normalize to 0.5"
2439                );
2440            } else {
2441                assert!(
2442                    v.to_percent().is_none(),
2443                    "{metric:?} must not masquerade as a percentage"
2444                );
2445            }
2446        }
2447
2448        // The returned percentage is already normalized: resolve() multiplies, it
2449        // must not divide by 100 a second time.
2450        assert_eq!(
2451            PixelValue::percent(50.0)
2452                .to_percent()
2453                .unwrap()
2454                .resolve(640.0),
2455            320.0
2456        );
2457        assert_eq!(PixelValue::percent(-50.0).to_percent().unwrap().get(), -0.5);
2458        assert_eq!(PixelValue::percent(0.0).to_percent().unwrap().get(), 0.0);
2459        // Extreme instances stay finite.
2460        assert!(PixelValue::percent(f32::MAX)
2461            .to_percent()
2462            .unwrap()
2463            .get()
2464            .is_finite());
2465        assert_eq!(
2466            PixelValue::percent(f32::NAN).to_percent().unwrap().get(),
2467            0.0
2468        );
2469    }
2470
2471    #[test]
2472    fn normalized_percentage_new_and_from_unnormalized_disagree_by_100x() {
2473        // The whole point of the type: `new` takes 0.0-1.0, `from_unnormalized`
2474        // takes the CSS 0-100 scale.
2475        assert_eq!(NormalizedPercentage::new(0.5).get(), 0.5);
2476        assert_eq!(NormalizedPercentage::from_unnormalized(50.0).get(), 0.5);
2477        assert_eq!(NormalizedPercentage::from_unnormalized(0.0).get(), 0.0);
2478        assert_eq!(NormalizedPercentage::from_unnormalized(100.0).get(), 1.0);
2479        assert_eq!(NormalizedPercentage::from_unnormalized(-25.0).get(), -0.25);
2480
2481        assert_eq!(NormalizedPercentage::new(0.5).resolve(640.0), 320.0);
2482        assert_eq!(NormalizedPercentage::new(0.0).resolve(640.0), 0.0);
2483        assert_eq!(NormalizedPercentage::new(1.0).resolve(f32::MAX), f32::MAX);
2484        assert_eq!(NormalizedPercentage::new(-1.0).resolve(100.0), -100.0);
2485
2486        // Unlike PixelValue, this type is a raw f32 wrapper: it does NOT sanitize.
2487        // Non-finite in, non-finite out -- but never a panic.
2488        assert!(NormalizedPercentage::new(f32::NAN).get().is_nan());
2489        assert!(NormalizedPercentage::new(f32::NAN).resolve(100.0).is_nan());
2490        assert!(NormalizedPercentage::from_unnormalized(f32::INFINITY)
2491            .get()
2492            .is_infinite());
2493        assert!(NormalizedPercentage::new(1.0)
2494            .resolve(f32::INFINITY)
2495            .is_infinite());
2496        // 0 * inf is NaN, and this type will hand that straight to the layout.
2497        assert!(NormalizedPercentage::new(0.0)
2498            .resolve(f32::INFINITY)
2499            .is_nan());
2500
2501        // Display renders back on the 0-100 scale.
2502        assert_eq!(NormalizedPercentage::new(0.5).to_string(), "50%");
2503        assert_eq!(NormalizedPercentage::new(0.0).to_string(), "0%");
2504        assert!(!NormalizedPercentage::new(f32::NAN).to_string().is_empty());
2505        assert!(!NormalizedPercentage::new(f32::INFINITY)
2506            .to_string()
2507            .is_empty());
2508    }
2509
2510    #[test]
2511    fn resolve_with_context_reads_the_right_reference_for_each_property() {
2512        let ctx = distinct_context(); // element 32 / parent 8 / root 4, block 800x600,
2513                                      // element 200x100, viewport 1000x500
2514
2515        // em: the element's own font-size, EXCEPT on font-size, where it is the
2516        // parent's. Getting this backwards is the classic CSS 2.1 §15.7 bug.
2517        assert_eq!(
2518            PixelValue::em(2.0).resolve_with_context(&ctx, PropertyContext::Margin),
2519            64.0
2520        );
2521        assert_eq!(
2522            PixelValue::em(2.0).resolve_with_context(&ctx, PropertyContext::FontSize),
2523            16.0
2524        );
2525
2526        // rem: always the root, whatever the property.
2527        for pc in ALL_PROPERTY_CONTEXTS {
2528            assert_eq!(
2529                PixelValue::rem(2.0).resolve_with_context(&ctx, pc),
2530                8.0,
2531                "rem must ignore the property context ({pc:?})"
2532            );
2533        }
2534
2535        // %: the reference depends entirely on the property.
2536        let pct = PixelValue::percent(50.0);
2537        assert_eq!(
2538            pct.resolve_with_context(&ctx, PropertyContext::Width),
2539            400.0,
2540            "width % -> containing block WIDTH"
2541        );
2542        assert_eq!(
2543            pct.resolve_with_context(&ctx, PropertyContext::Height),
2544            300.0,
2545            "height % -> containing block HEIGHT"
2546        );
2547        assert_eq!(
2548            pct.resolve_with_context(&ctx, PropertyContext::Margin),
2549            400.0,
2550            "margin % -> containing block WIDTH, even vertically (CSS 2.1 §8.3)"
2551        );
2552        assert_eq!(
2553            pct.resolve_with_context(&ctx, PropertyContext::Padding),
2554            400.0,
2555            "padding % -> containing block WIDTH, even vertically (CSS 2.1 §8.4)"
2556        );
2557        assert_eq!(
2558            pct.resolve_with_context(&ctx, PropertyContext::Other),
2559            400.0
2560        );
2561        assert_eq!(
2562            pct.resolve_with_context(&ctx, PropertyContext::FontSize),
2563            4.0,
2564            "font-size % -> PARENT font size"
2565        );
2566        assert_eq!(
2567            pct.resolve_with_context(&ctx, PropertyContext::BorderRadius),
2568            100.0,
2569            "border-radius % -> the element's own box"
2570        );
2571        assert_eq!(
2572            pct.resolve_with_context(&ctx, PropertyContext::Transform),
2573            100.0
2574        );
2575        assert_eq!(
2576            pct.resolve_with_context(&ctx, PropertyContext::BorderWidth),
2577            0.0,
2578            "% is invalid on border-width (CSS Backgrounds 3 §4.1) -> 0"
2579        );
2580    }
2581
2582    #[test]
2583    fn resolve_with_context_percent_without_an_element_size_is_zero() {
2584        // element_size is None during the first layout pass; the % arms that read
2585        // it must degrade to 0 instead of unwrapping.
2586        let ctx = ResolutionContext {
2587            vertical_writing_mode: false,
2588            element_size: None,
2589            ..distinct_context()
2590        };
2591        assert_eq!(
2592            PixelValue::percent(50.0).resolve_with_context(&ctx, PropertyContext::BorderRadius),
2593            0.0
2594        );
2595        assert_eq!(
2596            PixelValue::percent(50.0).resolve_with_context(&ctx, PropertyContext::Transform),
2597            0.0
2598        );
2599    }
2600
2601    #[test]
2602    fn resolve_with_context_absolute_units_ignore_the_context_entirely() {
2603        let sane = distinct_context();
2604        let poisoned = ResolutionContext {
2605            vertical_writing_mode: false,
2606            element_font_size: f32::NAN,
2607            parent_font_size: f32::INFINITY,
2608            root_font_size: f32::NEG_INFINITY,
2609            containing_block_size: PhysicalSize::new(f32::NAN, f32::NAN),
2610            element_size: Some(PhysicalSize::new(f32::INFINITY, f32::NAN)),
2611            viewport_size: PhysicalSize::new(f32::NAN, f32::INFINITY),
2612        };
2613
2614        let absolutes = [
2615            PixelValue::px(10.0),
2616            PixelValue::pt(10.0),
2617            PixelValue::inch(10.0),
2618            PixelValue::cm(10.0),
2619            PixelValue::mm(10.0),
2620        ];
2621        for v in absolutes {
2622            for pc in ALL_PROPERTY_CONTEXTS {
2623                let a = v.resolve_with_context(&sane, pc);
2624                let b = v.resolve_with_context(&poisoned, pc);
2625                assert_eq!(a, b, "{:?} must not read the context ({pc:?})", v.metric);
2626                assert!(a.is_finite());
2627            }
2628        }
2629
2630        // ...and they agree with the documented conversion factors.
2631        assert_eq!(
2632            PixelValue::px(10.0).resolve_with_context(&sane, PropertyContext::Width),
2633            10.0
2634        );
2635        assert!(approx(
2636            PixelValue::inch(1.0).resolve_with_context(&sane, PropertyContext::Width),
2637            96.0
2638        ));
2639        assert!(approx(
2640            PixelValue::pt(72.0).resolve_with_context(&sane, PropertyContext::Width),
2641            96.0
2642        ));
2643        assert!(approx(
2644            PixelValue::cm(2.54).resolve_with_context(&sane, PropertyContext::Width),
2645            96.0
2646        ));
2647        assert!(approx(
2648            PixelValue::mm(25.4).resolve_with_context(&sane, PropertyContext::Width),
2649            96.0
2650        ));
2651    }
2652
2653    #[test]
2654    fn resolve_with_context_viewport_units_use_the_viewport() {
2655        let ctx = distinct_context(); // viewport 1000x500
2656
2657        assert_eq!(
2658            PixelValue::from_metric(SizeMetric::Vw, 10.0)
2659                .resolve_with_context(&ctx, PropertyContext::Width),
2660            100.0
2661        );
2662        assert_eq!(
2663            PixelValue::from_metric(SizeMetric::Vh, 10.0)
2664                .resolve_with_context(&ctx, PropertyContext::Width),
2665            50.0
2666        );
2667        assert_eq!(
2668            PixelValue::from_metric(SizeMetric::Vmin, 10.0)
2669                .resolve_with_context(&ctx, PropertyContext::Width),
2670            50.0,
2671            "vmin must take the SMALLER viewport dimension"
2672        );
2673        assert_eq!(
2674            PixelValue::from_metric(SizeMetric::Vmax, 10.0)
2675                .resolve_with_context(&ctx, PropertyContext::Width),
2676            100.0,
2677            "vmax must take the LARGER viewport dimension"
2678        );
2679
2680        // A zero viewport (the default context) is not a division-by-zero trap:
2681        // the /100 is on the viewport side, so this is a plain 0.
2682        let zero_vp = ResolutionContext::default_const();
2683        for metric in [
2684            SizeMetric::Vw,
2685            SizeMetric::Vh,
2686            SizeMetric::Vmin,
2687            SizeMetric::Vmax,
2688        ] {
2689            assert_eq!(
2690                PixelValue::from_metric(metric, 100.0)
2691                    .resolve_with_context(&zero_vp, PropertyContext::Width),
2692                0.0,
2693                "{metric:?} against a 0x0 viewport must be 0"
2694            );
2695        }
2696
2697        // A non-finite viewport propagates rather than panicking.
2698        let nan_vp = ResolutionContext {
2699            vertical_writing_mode: false,
2700            viewport_size: PhysicalSize::new(f32::NAN, f32::NAN),
2701            ..distinct_context()
2702        };
2703        assert!(PixelValue::from_metric(SizeMetric::Vw, 10.0)
2704            .resolve_with_context(&nan_vp, PropertyContext::Width)
2705            .is_nan());
2706        // NOTE: f32::min/max return the non-NaN operand, so vmin/vmax against a
2707        // half-NaN viewport silently pick the finite axis instead of poisoning.
2708        let half_nan_vp = ResolutionContext {
2709            vertical_writing_mode: false,
2710            viewport_size: PhysicalSize::new(f32::NAN, 500.0),
2711            ..distinct_context()
2712        };
2713        assert_eq!(
2714            PixelValue::from_metric(SizeMetric::Vmin, 10.0)
2715                .resolve_with_context(&half_nan_vp, PropertyContext::Width),
2716            50.0
2717        );
2718    }
2719
2720    #[test]
2721    fn resolve_with_context_never_panics_on_extreme_values() {
2722        let ctx = distinct_context();
2723        for metric in ALL_METRICS {
2724            for v in EXTREME_F32 {
2725                for pc in ALL_PROPERTY_CONTEXTS {
2726                    // The only contract here is "returns, deterministically".
2727                    let _ = PixelValue::from_metric(metric, v).resolve_with_context(&ctx, pc);
2728                }
2729            }
2730        }
2731    }
2732
2733    #[test]
2734    fn resolution_context_default_matches_default_const() {
2735        // Two hand-written constructors for the same thing: they must not drift.
2736        let a = ResolutionContext::default();
2737        let b = ResolutionContext::default_const();
2738
2739        assert_eq!(a.element_font_size, b.element_font_size);
2740        assert_eq!(a.parent_font_size, b.parent_font_size);
2741        assert_eq!(a.root_font_size, b.root_font_size);
2742        assert_eq!(a.containing_block_size, b.containing_block_size);
2743        assert_eq!(a.element_size, b.element_size);
2744        assert_eq!(a.viewport_size, b.viewport_size);
2745
2746        // The default font size is the CSS "medium" keyword (16px).
2747        assert_eq!(a.element_font_size, DEFAULT_FONT_SIZE);
2748        assert!(a.element_size.is_none());
2749    }
2750
2751    #[test]
2752    fn logical_and_physical_sizes_round_trip() {
2753        let logical = CssLogicalSize::new(800.0, 600.0);
2754        assert_eq!(logical.to_physical(), PhysicalSize::new(800.0, 600.0));
2755        assert_eq!(logical.to_physical().to_logical(), logical);
2756
2757        let physical = PhysicalSize::new(1920.0, 1080.0);
2758        assert_eq!(physical.to_logical(), CssLogicalSize::new(1920.0, 1080.0));
2759        assert_eq!(physical.to_logical().to_physical(), physical);
2760
2761        // In horizontal writing mode inline==width and block==height; a swapped
2762        // mapping would survive a square, so use a non-square size.
2763        assert_eq!(CssLogicalSize::new(800.0, 600.0).to_physical().width, 800.0);
2764        assert_eq!(
2765            PhysicalSize::new(800.0, 600.0).to_logical().block_size,
2766            600.0
2767        );
2768
2769        // These are transparent f32 carriers: no sanitizing, no panics.
2770        let nan = PhysicalSize::new(f32::NAN, f32::INFINITY);
2771        assert!(nan.to_logical().inline_size.is_nan());
2772        assert!(nan.to_logical().block_size.is_infinite());
2773    }
2774
2775    // ======================================== serializers and round-trips ===
2776
2777    #[test]
2778    fn every_rendering_of_a_pixel_value_agrees() {
2779        // Display, Debug, PrintAsCssValue and FormatAsCssValue are four separate
2780        // impls of the same string; they must not drift apart.
2781        for metric in ALL_METRICS {
2782            let v = PixelValue::from_metric(metric, 1.5);
2783            let display = v.to_string();
2784            assert_eq!(format!("{v:?}"), display, "Debug != Display for {metric:?}");
2785            assert_eq!(v.print_as_css_value(), display);
2786            assert_eq!(as_css_value(v), display);
2787            assert!(display.starts_with("1.5"), "{display} lost its number");
2788            assert!(display.len() > 3, "{display} lost its unit");
2789        }
2790
2791        assert_eq!(PixelValue::px(10.0).to_string(), "10px");
2792        assert_eq!(PixelValue::percent(50.0).to_string(), "50%");
2793        assert_eq!(PixelValue::zero().to_string(), "0px");
2794        assert_eq!(
2795            PixelValue::from_metric(SizeMetric::Vmin, 12.0).to_string(),
2796            "12vmin"
2797        );
2798
2799        // The no-percent wrapper delegates to the inner value.
2800        let np = PixelValueNoPercent::from(PixelValue::px(10.0));
2801        assert_eq!(np.to_string(), "10px");
2802        assert_eq!(format!("{np:?}"), "10px");
2803        assert_eq!(PixelValueNoPercent::zero().to_string(), "0px");
2804    }
2805
2806    #[test]
2807    fn display_never_leaks_nan_or_infinity_into_css() {
2808        // A stylesheet containing "NaNpx" would be a serializer bug. The isize
2809        // encoding is what prevents it -- pin that for every metric and every
2810        // pathological input.
2811        for metric in ALL_METRICS {
2812            for v in EXTREME_F32 {
2813                let s = PixelValue::from_metric(metric, v).to_string();
2814                assert!(
2815                    !s.contains("NaN") && !s.contains("inf"),
2816                    "{metric:?} with input {v} serialized to {s:?}"
2817                );
2818                assert!(!s.is_empty());
2819            }
2820        }
2821        assert_eq!(PixelValue::px(f32::NAN).to_string(), "0px");
2822    }
2823
2824    #[test]
2825    fn pixel_values_round_trip_through_css_for_every_metric_but_vmin() {
2826        // encode == decode: print_as_css_value -> parse_pixel_value -> same value.
2827        for metric in ALL_METRICS {
2828            if metric == SizeMetric::Vmin {
2829                continue; // known-broken suffix table; see the vmin test above
2830            }
2831            for number in [0.0_f32, 1.0, 1.5, -20.0, 0.001, 12345.0] {
2832                let original = PixelValue::from_metric(metric, number);
2833                let css = original.print_as_css_value();
2834                let reparsed = parse_pixel_value(&css).unwrap_or_else(|e| {
2835                    panic!("{css:?} (from {metric:?} {number}) failed to re-parse: {e:?}")
2836                });
2837                assert_eq!(reparsed, original, "round-trip broke for {css:?}");
2838                // ...and re-printing is idempotent.
2839                assert_eq!(reparsed.print_as_css_value(), css);
2840            }
2841        }
2842
2843        // The no-percent parser round-trips everything except % (and vmin).
2844        for metric in ALL_METRICS {
2845            if metric == SizeMetric::Vmin || metric == SizeMetric::Percent {
2846                continue;
2847            }
2848            let original = PixelValueNoPercent::from(PixelValue::from_metric(metric, 7.0));
2849            let css = original.to_string();
2850            assert_eq!(
2851                parse_pixel_value_no_percent(&css).unwrap(),
2852                original,
2853                "no-percent round-trip broke for {css:?}"
2854            );
2855        }
2856
2857        // ...and the with-auto wrapper round-trips its keywords and its lengths.
2858        for (css, expected) in [
2859            ("auto", PixelValueWithAuto::Auto),
2860            ("none", PixelValueWithAuto::None),
2861            ("initial", PixelValueWithAuto::Initial),
2862            ("inherit", PixelValueWithAuto::Inherit),
2863        ] {
2864            assert_eq!(parse_pixel_value_with_auto(css).unwrap(), expected);
2865        }
2866        let exact = PixelValue::em(1.5);
2867        assert_eq!(
2868            parse_pixel_value_with_auto(&exact.print_as_css_value()).unwrap(),
2869            PixelValueWithAuto::Exact(exact)
2870        );
2871    }
2872
2873    #[test]
2874    fn format_as_rust_code_emits_a_reconstructible_literal() {
2875        assert_eq!(
2876            PixelValue::px(10.0).format_as_rust_code(0),
2877            "PixelValue { metric: Px, number: FloatValue::new(10) }"
2878        );
2879        assert_eq!(
2880            PixelValue::percent(-1.5).format_as_rust_code(4),
2881            "PixelValue { metric: Percent, number: FloatValue::new(-1.5) }"
2882        );
2883        // Even a pathological input must emit compilable code, never "NaN".
2884        let nan = PixelValue::from_metric(SizeMetric::Vmax, f32::NAN).format_as_rust_code(0);
2885        assert_eq!(
2886            nan,
2887            "PixelValue { metric: Vmax, number: FloatValue::new(0) }"
2888        );
2889        assert!(!PixelValue::px(f32::INFINITY)
2890            .format_as_rust_code(0)
2891            .contains("inf"));
2892    }
2893
2894    #[test]
2895    fn border_thickness_constants_match_the_css_keywords() {
2896        // thin/medium/thick are hand-encoded as raw FloatValue bit patterns, so a
2897        // change to FP_PRECISION_MULTIPLIER would silently rescale them.
2898        assert_eq!(THIN_BORDER_THICKNESS, PixelValue::px(1.0));
2899        assert_eq!(MEDIUM_BORDER_THICKNESS, PixelValue::px(3.0));
2900        assert_eq!(THICK_BORDER_THICKNESS, PixelValue::px(5.0));
2901
2902        assert_eq!(THIN_BORDER_THICKNESS.number.get(), 1.0);
2903        assert_eq!(MEDIUM_BORDER_THICKNESS.number.get(), 3.0);
2904        assert_eq!(THICK_BORDER_THICKNESS.number.get(), 5.0);
2905        assert_eq!(THIN_BORDER_THICKNESS.number.number() as f32, MULT);
2906
2907        assert!(THIN_BORDER_THICKNESS < MEDIUM_BORDER_THICKNESS);
2908        assert!(MEDIUM_BORDER_THICKNESS < THICK_BORDER_THICKNESS);
2909        assert_eq!(THIN_BORDER_THICKNESS.to_string(), "1px");
2910    }
2911
2912    #[test]
2913    fn ord_is_lexicographic_by_metric_then_number_not_by_resolved_size() {
2914        // PixelValue derives Ord over (metric, number). That means 100px sorts
2915        // BELOW 1em even though it is far larger once resolved. Anything that
2916        // sorts or range-queries these values needs to know that.
2917        assert!(PixelValue::px(100.0) < PixelValue::em(1.0));
2918        assert!(PixelValue::px(1.0) < PixelValue::px(2.0));
2919        assert!(PixelValue::percent(1.0) > PixelValue::mm(9999.0));
2920
2921        // Eq/Hash agree, including across the sanitized encodings.
2922        let a = PixelValue::px(1.5);
2923        let b = PixelValue::px(1.5);
2924        assert_eq!(a, b);
2925        assert_eq!(hash_of(&a), hash_of(&b));
2926        assert_ne!(hash_of(&PixelValue::px(1.0)), hash_of(&PixelValue::em(1.0)));
2927
2928        // Two values that differ only below the 1/1000 quantum collide -- by
2929        // design, since that is what makes PixelValue hashable at all.
2930        assert_eq!(PixelValue::px(1.0001), PixelValue::px(1.0002));
2931        assert_eq!(
2932            hash_of(&PixelValue::px(1.0001)),
2933            hash_of(&PixelValue::px(1.0002))
2934        );
2935    }
2936
2937    // ====================================================== system metrics ===
2938
2939    #[test]
2940    fn system_metric_ref_css_strings_round_trip() {
2941        for r in ALL_SYSTEM_REFS {
2942            let css = r.as_css_str();
2943            assert!(
2944                css.starts_with("system:"),
2945                "{css:?} is missing the system: prefix"
2946            );
2947            assert_eq!(r.to_string(), css, "Display must match as_css_str");
2948            assert_eq!(as_css_value(r), css);
2949
2950            // from_css_str takes the name WITHOUT the prefix.
2951            let name = css.strip_prefix("system:").unwrap();
2952            assert_eq!(
2953                SystemMetricRef::from_css_str(name),
2954                Some(r),
2955                "{name:?} must parse back to {r:?}"
2956            );
2957            // Serialize-parse-serialize is stable.
2958            assert_eq!(
2959                SystemMetricRef::from_css_str(name).unwrap().as_css_str(),
2960                css
2961            );
2962
2963            // Footgun worth pinning: feeding the *full* CSS string back in fails,
2964            // because from_css_str does not strip the prefix itself.
2965            assert_eq!(SystemMetricRef::from_css_str(css), None);
2966        }
2967
2968        assert_eq!(SystemMetricRef::default(), SystemMetricRef::ButtonRadius);
2969    }
2970
2971    #[test]
2972    fn system_metric_ref_from_css_str_rejects_everything_else() {
2973        for input in [
2974            "",
2975            "   ",
2976            "\t\n",
2977            " button-radius ", // no trimming
2978            "Button-Radius",   // case-sensitive
2979            "button_radius",   // wrong separator
2980            "button-padding",  // the spelling the docs advertise; not a real one
2981            "button-radius;x",
2982            "\u{1F600}",
2983            "b\u{0301}utton-radius",
2984        ] {
2985            assert_eq!(
2986                SystemMetricRef::from_css_str(input),
2987                None,
2988                "{input:?} must not resolve to a system metric"
2989            );
2990        }
2991
2992        // Long input is rejected without hanging.
2993        assert_eq!(SystemMetricRef::from_css_str(&"a".repeat(100_000)), None);
2994        assert_eq!(SystemMetricRef::from_css_str(&"(".repeat(10_000)), None);
2995    }
2996
2997    #[test]
2998    fn system_metric_ref_resolve_maps_each_variant_to_its_own_field() {
2999        // Every field gets a distinct value, so a mis-wired arm cannot pass.
3000        let metrics = populated_metrics();
3001        let expected = [
3002            (SystemMetricRef::ButtonRadius, 1.0),
3003            (SystemMetricRef::ButtonBorderWidth, 2.0),
3004            (SystemMetricRef::ButtonPaddingHorizontal, 3.0),
3005            (SystemMetricRef::ButtonPaddingVertical, 4.0),
3006            (SystemMetricRef::TitlebarHeight, 5.0),
3007            (SystemMetricRef::TitlebarButtonWidth, 6.0),
3008            (SystemMetricRef::TitlebarPadding, 7.0),
3009            (SystemMetricRef::SafeAreaTop, 8.0),
3010            (SystemMetricRef::SafeAreaBottom, 9.0),
3011            (SystemMetricRef::SafeAreaLeft, 10.0),
3012            (SystemMetricRef::SafeAreaRight, 11.0),
3013        ];
3014        for (r, px) in expected {
3015            assert_eq!(
3016                r.resolve(&metrics),
3017                Some(PixelValue::px(px)),
3018                "{r:?} resolved to the wrong field"
3019            );
3020        }
3021
3022        // An unpopulated SystemMetrics yields None for every variant (no unwraps).
3023        let empty = SystemMetrics::default();
3024        for r in ALL_SYSTEM_REFS {
3025            assert_eq!(r.resolve(&empty), None, "{r:?} must be None when unset");
3026        }
3027    }
3028
3029    #[test]
3030    fn pixel_value_or_system_resolves_and_falls_back() {
3031        let metrics = populated_metrics();
3032        let empty = SystemMetrics::default();
3033        let fallback = PixelValue::px(99.0);
3034
3035        // A concrete value ignores the system metrics entirely.
3036        let concrete = PixelValueOrSystem::value(PixelValue::px(10.0));
3037        assert_eq!(concrete.resolve(&metrics, fallback), PixelValue::px(10.0));
3038        assert_eq!(concrete.resolve(&empty, fallback), PixelValue::px(10.0));
3039
3040        // A system ref takes the metric when present...
3041        let sys = PixelValueOrSystem::system(SystemMetricRef::ButtonRadius);
3042        assert_eq!(sys.resolve(&metrics, fallback), PixelValue::px(1.0));
3043        // ...and the fallback when absent, for every variant.
3044        for r in ALL_SYSTEM_REFS {
3045            assert_eq!(
3046                PixelValueOrSystem::system(r).resolve(&empty, fallback),
3047                fallback,
3048                "{r:?} must fall back when the metric is unset"
3049            );
3050        }
3051
3052        // Extreme fallbacks stay finite (they went through FloatValue too).
3053        let nan_fallback = PixelValue::px(f32::NAN);
3054        assert_eq!(sys.resolve(&empty, nan_fallback).number.get(), 0.0);
3055
3056        // Constructors / conversions / default.
3057        assert_eq!(
3058            PixelValueOrSystem::default(),
3059            PixelValueOrSystem::Value(PixelValue::zero())
3060        );
3061        assert_eq!(
3062            PixelValueOrSystem::from(PixelValue::em(2.0)),
3063            PixelValueOrSystem::Value(PixelValue::em(2.0))
3064        );
3065        assert_eq!(
3066            PixelValueOrSystem::default().resolve(&metrics, fallback),
3067            PixelValue::zero()
3068        );
3069    }
3070
3071    #[test]
3072    fn pixel_value_or_system_renders_both_arms() {
3073        let concrete = PixelValueOrSystem::value(PixelValue::px(10.0));
3074        assert_eq!(concrete.to_string(), "10px");
3075        assert_eq!(as_css_value(concrete), "10px");
3076
3077        let sys = PixelValueOrSystem::system(SystemMetricRef::TitlebarHeight);
3078        assert_eq!(sys.to_string(), "system:titlebar-height");
3079        assert_eq!(as_css_value(sys), "system:titlebar-height");
3080
3081        assert_eq!(PixelValueOrSystem::default().to_string(), "0px");
3082
3083        // No arm can serialize a non-finite number.
3084        for v in EXTREME_F32 {
3085            let s = PixelValueOrSystem::value(PixelValue::px(v)).to_string();
3086            assert!(!s.contains("NaN") && !s.contains("inf"), "leaked {s:?}");
3087        }
3088    }
3089}