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