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