Skip to main content

azul_css/props/basic/
pixel.rs

1use core::fmt;
2use std::num::ParseFloatError;
3
4use crate::props::{
5    basic::{FloatValue, SizeMetric},
6    formatter::FormatAsCssValue,
7};
8
9/// Default/fallback font size in pixels, used when no font-size is specified.
10///
11/// This is the same as the CSS "medium" keyword and matches browser defaults:
12/// - CSS 2.1 §15.7: "medium" is the user's preferred font size
13/// - All major browsers default to 16px
14/// - W3C HTML5: The default font-size of the root element is 16px
15///
16/// This constant is used in two scenarios:
17/// 1. As fallback when no explicit font-size is found in the cascade
18/// 2. In legacy `to_pixels()` for em/rem conversion when no context available
19///
20/// **Research:**
21/// - Chrome/Firefox/Safari: 16px default
22/// - CSS font-size keywords: medium = 16px (derived from 13.33px * 1.2)
23/// - Can be overridden by user preferences (browser settings)
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    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    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    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    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    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    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    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    pub const fn to_logical(self) -> CssLogicalSize {
125        CssLogicalSize {
126            inline_size: self.width,
127            block_size: self.height,
128        }
129    }
130}
131
132/// Context information needed to properly resolve CSS units (em, rem, %) to pixels.
133///
134/// This struct contains all the contextual information that `PixelValue::resolve()`
135/// needs to correctly convert relative units according to the CSS specification:
136///
137/// - **em** units: For most properties, em refers to the element's own computed font-size. For the
138///   font-size property itself, em refers to the parent's computed font-size.
139///
140/// - **rem** units: Always refer to the root element's computed font-size.
141///
142/// - **%** units: Percentage resolution depends on the property:
143///   - Width/height: relative to containing block dimensions
144///   - Margin/padding: relative to containing block width (even top/bottom!)
145///   - Border-radius: relative to element's own border box dimensions
146///   - Font-size: relative to parent's font-size
147#[derive(Debug, Copy, Clone)]
148pub struct ResolutionContext {
149    /// The computed font-size of the current element (for em in non-font properties)
150    pub element_font_size: f32,
151
152    /// The computed font-size of the parent element (for em in font-size property)
153    pub parent_font_size: f32,
154
155    /// The computed font-size of the root element (for rem units)
156    pub root_font_size: f32,
157
158    /// The containing block dimensions (for % in width/height/margins/padding)
159    pub containing_block_size: PhysicalSize,
160
161    /// The element's own border box size (for % in border-radius, transforms)
162    /// May be None during first layout pass before size is determined
163    pub element_size: Option<PhysicalSize>,
164
165    /// The viewport size in CSS pixels (for vw, vh, vmin, vmax units)
166    /// This is the layout viewport size, not physical screen size
167    pub viewport_size: PhysicalSize,
168}
169
170impl Default for ResolutionContext {
171    fn default() -> Self {
172        Self {
173            element_font_size: 16.0,
174            parent_font_size: 16.0,
175            root_font_size: 16.0,
176            containing_block_size: PhysicalSize::new(0.0, 0.0),
177            element_size: None,
178            viewport_size: PhysicalSize::new(0.0, 0.0),
179        }
180    }
181}
182
183impl ResolutionContext {
184    /// Create a minimal context for testing or default resolution
185    #[inline]
186    pub const fn default_const() -> Self {
187        Self {
188            element_font_size: 16.0,
189            parent_font_size: 16.0,
190            root_font_size: 16.0,
191            containing_block_size: PhysicalSize {
192                width: 0.0,
193                height: 0.0,
194            },
195            element_size: None,
196            viewport_size: PhysicalSize {
197                width: 0.0,
198                height: 0.0,
199            },
200        }
201    }
202
203    /// Create a context with only font-size information (for font-relative units)
204    #[inline]
205    pub const fn for_fonts(
206        element_font_size: f32,
207        parent_font_size: f32,
208        root_font_size: f32,
209    ) -> Self {
210        Self {
211            element_font_size,
212            parent_font_size,
213            root_font_size,
214            containing_block_size: PhysicalSize {
215                width: 0.0,
216                height: 0.0,
217            },
218            element_size: None,
219            viewport_size: PhysicalSize {
220                width: 0.0,
221                height: 0.0,
222            },
223        }
224    }
225
226    /// Create a context with containing block information (for percentage units)
227    #[inline]
228    pub const fn with_containing_block(mut self, containing_block_size: PhysicalSize) -> Self {
229        self.containing_block_size = containing_block_size;
230        self
231    }
232
233    /// Create a context with element size information (for border-radius, transforms)
234    #[inline]
235    pub const fn with_element_size(mut self, element_size: PhysicalSize) -> Self {
236        self.element_size = Some(element_size);
237        self
238    }
239
240    /// Create a context with viewport size information (for vw, vh, vmin, vmax units)
241    #[inline]
242    pub const fn with_viewport_size(mut self, viewport_size: PhysicalSize) -> Self {
243        self.viewport_size = viewport_size;
244        self
245    }
246}
247
248/// Specifies which property context we're resolving for, to determine correct reference values
249#[derive(Debug, Copy, Clone, PartialEq, Eq)]
250pub enum PropertyContext {
251    /// Resolving for the font-size property itself (em refers to parent)
252    FontSize,
253    /// Resolving for margin properties (% refers to containing block width)
254    Margin,
255    /// Resolving for padding properties (% refers to containing block width)
256    Padding,
257    /// Resolving for width or horizontal properties (% refers to containing block width)
258    Width,
259    /// Resolving for height or vertical properties (% refers to containing block height)
260    Height,
261    /// Resolving for border-width properties (only absolute lengths + em/rem, no % support)
262    BorderWidth,
263    /// Resolving for border-radius (% refers to element's own dimensions)
264    BorderRadius,
265    /// Resolving for transforms (% refers to element's own dimensions)
266    Transform,
267    /// Resolving for other properties (em refers to element font-size)
268    Other,
269}
270
271#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
272#[repr(C)]
273pub struct PixelValue {
274    pub metric: SizeMetric,
275    pub number: FloatValue,
276}
277
278impl PixelValue {
279    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
280        self.number = FloatValue::new(self.number.get() * scale_factor);
281    }
282}
283
284impl FormatAsCssValue for PixelValue {
285    fn format_as_css_value(&self, f: &mut fmt::Formatter) -> fmt::Result {
286        write!(f, "{}{}", self.number, self.metric)
287    }
288}
289
290impl crate::css::PrintAsCssValue for PixelValue {
291    fn print_as_css_value(&self) -> String {
292        format!("{}{}", self.number, self.metric)
293    }
294}
295
296impl crate::format_rust_code::FormatAsRustCode for PixelValue {
297    fn format_as_rust_code(&self, _tabs: usize) -> String {
298        format!(
299            "PixelValue {{ metric: {:?}, number: FloatValue::new({}) }}",
300            self.metric,
301            self.number.get()
302        )
303    }
304}
305
306impl fmt::Debug for PixelValue {
307    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
308        write!(f, "{}{}", self.number, self.metric)
309    }
310}
311
312// Manual Debug implementation, because the auto-generated one is nearly unreadable
313impl fmt::Display for PixelValue {
314    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
315        write!(f, "{}{}", self.number, self.metric)
316    }
317}
318
319impl PixelValue {
320    #[inline]
321    pub const fn zero() -> Self {
322        const ZERO_PX: PixelValue = PixelValue::const_px(0);
323        ZERO_PX
324    }
325
326    /// Same as `PixelValue::px()`, but only accepts whole numbers,
327    /// since using `f32` in const fn is not yet stabilized.
328    #[inline]
329    pub const fn const_px(value: isize) -> Self {
330        Self::const_from_metric(SizeMetric::Px, value)
331    }
332
333    /// Same as `PixelValue::em()`, but only accepts whole numbers,
334    /// since using `f32` in const fn is not yet stabilized.
335    #[inline]
336    pub const fn const_em(value: isize) -> Self {
337        Self::const_from_metric(SizeMetric::Em, value)
338    }
339
340    /// Creates an em value from a fractional number in const context.
341    ///
342    /// # Arguments
343    /// * `pre_comma` - The integer part (e.g., 1 for 1.5em)
344    /// * `post_comma` - The fractional part as digits (e.g., 5 for 0.5em, 83 for 0.83em)
345    ///
346    /// # Examples
347    /// ```
348    /// // 1.5em = const_em_fractional(1, 5)
349    /// // 0.83em = const_em_fractional(0, 83)
350    /// // 1.17em = const_em_fractional(1, 17)
351    /// ```
352    #[inline]
353    pub const fn const_em_fractional(pre_comma: isize, post_comma: isize) -> Self {
354        Self::const_from_metric_fractional(SizeMetric::Em, pre_comma, post_comma)
355    }
356
357    /// Same as `PixelValue::pt()`, but only accepts whole numbers,
358    /// since using `f32` in const fn is not yet stabilized.
359    #[inline]
360    pub const fn const_pt(value: isize) -> Self {
361        Self::const_from_metric(SizeMetric::Pt, value)
362    }
363
364    /// Creates a pt value from a fractional number in const context.
365    #[inline]
366    pub const fn const_pt_fractional(pre_comma: isize, post_comma: isize) -> Self {
367        Self::const_from_metric_fractional(SizeMetric::Pt, pre_comma, post_comma)
368    }
369
370    /// Same as `PixelValue::pt()`, but only accepts whole numbers,
371    /// since using `f32` in const fn is not yet stabilized.
372    #[inline]
373    pub const fn const_percent(value: isize) -> Self {
374        Self::const_from_metric(SizeMetric::Percent, value)
375    }
376
377    /// Same as `PixelValue::in()`, but only accepts whole numbers,
378    /// since using `f32` in const fn is not yet stabilized.
379    #[inline]
380    pub const fn const_in(value: isize) -> Self {
381        Self::const_from_metric(SizeMetric::In, value)
382    }
383
384    /// Same as `PixelValue::in()`, but only accepts whole numbers,
385    /// since using `f32` in const fn is not yet stabilized.
386    #[inline]
387    pub const fn const_cm(value: isize) -> Self {
388        Self::const_from_metric(SizeMetric::Cm, value)
389    }
390
391    /// Same as `PixelValue::in()`, but only accepts whole numbers,
392    /// since using `f32` in const fn is not yet stabilized.
393    #[inline]
394    pub const fn const_mm(value: isize) -> Self {
395        Self::const_from_metric(SizeMetric::Mm, value)
396    }
397
398    #[inline]
399    pub const fn const_from_metric(metric: SizeMetric, value: isize) -> Self {
400        Self {
401            metric,
402            number: FloatValue::const_new(value),
403        }
404    }
405
406    /// Creates a PixelValue from a fractional number in const context.
407    ///
408    /// # Arguments
409    /// * `metric` - The size metric (Px, Em, Pt, etc.)
410    /// * `pre_comma` - The integer part
411    /// * `post_comma` - The fractional part as digits
412    #[inline]
413    pub const fn const_from_metric_fractional(
414        metric: SizeMetric,
415        pre_comma: isize,
416        post_comma: isize,
417    ) -> Self {
418        Self {
419            metric,
420            number: FloatValue::const_new_fractional(pre_comma, post_comma),
421        }
422    }
423
424    #[inline]
425    pub fn px(value: f32) -> Self {
426        Self::from_metric(SizeMetric::Px, value)
427    }
428
429    #[inline]
430    pub fn em(value: f32) -> Self {
431        Self::from_metric(SizeMetric::Em, value)
432    }
433
434    #[inline]
435    pub fn inch(value: f32) -> Self {
436        Self::from_metric(SizeMetric::In, value)
437    }
438
439    #[inline]
440    pub fn cm(value: f32) -> Self {
441        Self::from_metric(SizeMetric::Cm, value)
442    }
443
444    #[inline]
445    pub fn mm(value: f32) -> Self {
446        Self::from_metric(SizeMetric::Mm, value)
447    }
448
449    #[inline]
450    pub fn pt(value: f32) -> Self {
451        Self::from_metric(SizeMetric::Pt, value)
452    }
453
454    #[inline]
455    pub fn percent(value: f32) -> Self {
456        Self::from_metric(SizeMetric::Percent, value)
457    }
458
459    #[inline]
460    pub fn rem(value: f32) -> Self {
461        Self::from_metric(SizeMetric::Rem, value)
462    }
463
464    #[inline]
465    pub fn from_metric(metric: SizeMetric, value: f32) -> Self {
466        Self {
467            metric,
468            number: FloatValue::new(value),
469        }
470    }
471
472    #[inline]
473    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
474        if self.metric == other.metric {
475            Self {
476                metric: self.metric,
477                number: self.number.interpolate(&other.number, t),
478            }
479        } else {
480            // Interpolate between different metrics by converting to px
481            // Note: Uses DEFAULT_FONT_SIZE for em/rem - acceptable for animation fallback
482            let self_px_interp = self.to_pixels_internal(0.0, DEFAULT_FONT_SIZE);
483            let other_px_interp = other.to_pixels_internal(0.0, DEFAULT_FONT_SIZE);
484            Self::from_metric(
485                SizeMetric::Px,
486                self_px_interp + (other_px_interp - self_px_interp) * t,
487            )
488        }
489    }
490
491    /// Returns the value of the SizeMetric as a normalized percentage (0.0 = 0%, 1.0 = 100%)
492    ///
493    /// Returns `Some(NormalizedPercentage)` if this is a percentage value, `None` otherwise.
494    /// The returned `NormalizedPercentage` is already normalized to 0.0-1.0 range,
495    /// so you should multiply it directly with the containing block size.
496    #[inline]
497    pub fn to_percent(&self) -> Option<NormalizedPercentage> {
498        match self.metric {
499            SizeMetric::Percent => Some(NormalizedPercentage::from_unnormalized(self.number.get())),
500            _ => None,
501        }
502    }
503
504    /// Internal fallback method for converting to pixels with manual % resolution.
505    ///
506    /// Used internally by prop_cache.rs resolve_property_dependency().
507    ///
508    /// **DO NOT USE directly!** Use `resolve_with_context()` instead for new code.
509    #[doc(hidden)]
510    #[inline]
511    pub fn to_pixels_internal(&self, percent_resolve: f32, em_resolve: f32) -> f32 {
512        match self.metric {
513            SizeMetric::Px => self.number.get(),
514            SizeMetric::Pt => self.number.get() * PT_TO_PX,
515            SizeMetric::In => self.number.get() * 96.0,
516            SizeMetric::Cm => self.number.get() * 96.0 / 2.54,
517            SizeMetric::Mm => self.number.get() * 96.0 / 25.4,
518            SizeMetric::Em => self.number.get() * em_resolve,
519            SizeMetric::Rem => self.number.get() * em_resolve,
520            SizeMetric::Percent => {
521                NormalizedPercentage::from_unnormalized(self.number.get()).resolve(percent_resolve)
522            }
523            // Viewport units: Cannot resolve without viewport context, return 0
524            // These should use resolve_with_context() instead
525            SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => 0.0,
526        }
527    }
528
529    /// Resolve this value to pixels using proper CSS context.
530    ///
531    /// This is the **CORRECT** way to resolve CSS units. It properly handles:
532    /// - em units: Uses element's own font-size (or parent's for font-size property)
533    /// - rem units: Uses root element's font-size
534    /// - % units: Uses property-appropriate reference (containing block width/height, element size,
535    ///   etc.)
536    /// - Absolute units: px, pt, in, cm, mm (already correct)
537    ///
538    /// # Arguments
539    /// * `context` - Resolution context with font sizes and dimensions
540    /// * `property_context` - Which property we're resolving for (affects % and em resolution)
541    #[inline]
542    pub fn resolve_with_context(
543        &self,
544        context: &ResolutionContext,
545        property_context: PropertyContext,
546    ) -> f32 {
547        match self.metric {
548            // Absolute units - already correct
549            SizeMetric::Px => self.number.get(),
550            SizeMetric::Pt => self.number.get() * PT_TO_PX,
551            SizeMetric::In => self.number.get() * 96.0,
552            SizeMetric::Cm => self.number.get() * 96.0 / 2.54,
553            SizeMetric::Mm => self.number.get() * 96.0 / 25.4,
554
555            // Em units - CRITICAL: different resolution for font-size vs other properties
556            SizeMetric::Em => {
557                let reference_font_size = if property_context == PropertyContext::FontSize {
558                    // Em on font-size refers to parent's font-size (CSS 2.1 §15.7)
559                    context.parent_font_size
560                } else {
561                    // Em on other properties refers to element's own font-size (CSS 2.1 §10.5)
562                    context.element_font_size
563                };
564                self.number.get() * reference_font_size
565            }
566
567            // Rem units - ALWAYS refer to root font-size (CSS Values 3)
568            SizeMetric::Rem => self.number.get() * context.root_font_size,
569
570            // Viewport units - refer to viewport dimensions (CSS Values 3 §6.2)
571            // 1vw = 1% of viewport width, 1vh = 1% of viewport height
572            SizeMetric::Vw => self.number.get() * context.viewport_size.width / 100.0,
573            SizeMetric::Vh => self.number.get() * context.viewport_size.height / 100.0,
574            // vmin = smaller of vw or vh
575            SizeMetric::Vmin => {
576                let min_dimension = context
577                    .viewport_size
578                    .width
579                    .min(context.viewport_size.height);
580                self.number.get() * min_dimension / 100.0
581            }
582            // vmax = larger of vw or vh
583            SizeMetric::Vmax => {
584                let max_dimension = context
585                    .viewport_size
586                    .width
587                    .max(context.viewport_size.height);
588                self.number.get() * max_dimension / 100.0
589            }
590
591            // Percent units - reference depends on property type
592            SizeMetric::Percent => {
593                let reference = match property_context {
594                    // Font-size %: refers to parent's font-size (CSS 2.1 §15.7)
595                    PropertyContext::FontSize => context.parent_font_size,
596
597                    // Width and horizontal properties: containing block width (CSS 2.1 §10.3)
598                    PropertyContext::Width => context.containing_block_size.width,
599
600                    // Height and vertical properties: containing block height (CSS 2.1 §10.5)
601                    PropertyContext::Height => context.containing_block_size.height,
602
603                    // Margins: ALWAYS containing block WIDTH, even for top/bottom! (CSS 2.1 §8.3)
604                    // Padding: ALWAYS containing block WIDTH, even for top/bottom! (CSS 2.1 §8.4)
605                    PropertyContext::Margin | PropertyContext::Padding => {
606                        context.containing_block_size.width
607                    }
608
609                    // Border-width: % is NOT valid per CSS spec (CSS Backgrounds 3 §4.1)
610                    // Return 0.0 if someone tries to use % on border-width
611                    PropertyContext::BorderWidth => 0.0,
612
613                    // Border-radius: element's own dimensions (CSS Backgrounds 3 §5.1)
614                    // Note: More complex - horizontal % uses width, vertical % uses height
615                    // For now, use width as default
616                    PropertyContext::BorderRadius => {
617                        context.element_size.map(|s| s.width).unwrap_or(0.0)
618                    }
619
620                    // Transforms: element's own dimensions (CSS Transforms §20.1)
621                    PropertyContext::Transform => {
622                        context.element_size.map(|s| s.width).unwrap_or(0.0)
623                    }
624
625                    // Other properties: default to containing block width
626                    PropertyContext::Other => context.containing_block_size.width,
627                };
628
629                NormalizedPercentage::from_unnormalized(self.number.get()).resolve(reference)
630            }
631        }
632    }
633}
634
635// border-width: thin / medium / thick keyword values
636// These are the canonical CSS definitions and should be used consistently
637// across parsing and resolution.
638
639/// border-width: thin = 1px (per CSS spec)
640pub const THIN_BORDER_THICKNESS: PixelValue = PixelValue {
641    metric: SizeMetric::Px,
642    number: FloatValue { number: 1000 },
643};
644
645/// border-width: medium = 3px (per CSS spec, default)
646pub const MEDIUM_BORDER_THICKNESS: PixelValue = PixelValue {
647    metric: SizeMetric::Px,
648    number: FloatValue { number: 3000 },
649};
650
651/// border-width: thick = 5px (per CSS spec)
652pub const THICK_BORDER_THICKNESS: PixelValue = PixelValue {
653    metric: SizeMetric::Px,
654    number: FloatValue { number: 5000 },
655};
656
657/// Same as PixelValue, but doesn't allow a "%" sign
658#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
659#[repr(C)]
660pub struct PixelValueNoPercent {
661    pub inner: PixelValue,
662}
663
664impl PixelValueNoPercent {
665    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
666        self.inner.scale_for_dpi(scale_factor);
667    }
668}
669
670impl_option!(
671    PixelValueNoPercent,
672    OptionPixelValueNoPercent,
673    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
674);
675
676impl_option!(
677    PixelValue,
678    OptionPixelValue,
679    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
680);
681
682impl fmt::Display for PixelValueNoPercent {
683    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
684        write!(f, "{}", self.inner)
685    }
686}
687
688impl ::core::fmt::Debug for PixelValueNoPercent {
689    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
690        write!(f, "{}", self)
691    }
692}
693
694impl PixelValueNoPercent {
695    /// Internal conversion to pixels (no percent support).
696    ///
697    /// Used internally by prop_cache.rs.
698    ///
699    /// **DO NOT USE directly!** Use `resolve_with_context()` on inner value instead.
700    #[doc(hidden)]
701    #[inline]
702    pub fn to_pixels_internal(&self, em_resolve: f32) -> f32 {
703        self.inner.to_pixels_internal(0.0, em_resolve)
704    }
705
706    #[inline]
707    pub const fn zero() -> Self {
708        const ZERO_PXNP: PixelValueNoPercent = PixelValueNoPercent {
709            inner: PixelValue::zero(),
710        };
711        ZERO_PXNP
712    }
713}
714impl From<PixelValue> for PixelValueNoPercent {
715    fn from(e: PixelValue) -> Self {
716        Self { inner: e }
717    }
718}
719
720#[derive(Clone, PartialEq)]
721pub enum CssPixelValueParseError<'a> {
722    EmptyString,
723    NoValueGiven(&'a str, SizeMetric),
724    ValueParseErr(ParseFloatError, &'a str),
725    InvalidPixelValue(&'a str),
726}
727
728impl_debug_as_display!(CssPixelValueParseError<'a>);
729
730impl_display! { CssPixelValueParseError<'a>, {
731    EmptyString => format!("Missing [px / pt / em / %] value"),
732    NoValueGiven(input, metric) => format!("Expected floating-point pixel value, got: \"{}{}\"", input, metric),
733    ValueParseErr(err, number_str) => format!("Could not parse \"{}\" as floating-point value: \"{}\"", number_str, err),
734    InvalidPixelValue(s) => format!("Invalid pixel value: \"{}\"", s),
735}}
736
737/// Owned version of CssPixelValueParseError.
738#[derive(Debug, Clone, PartialEq)]
739pub enum CssPixelValueParseErrorOwned {
740    EmptyString,
741    NoValueGiven(String, SizeMetric),
742    ValueParseErr(ParseFloatError, String),
743    InvalidPixelValue(String),
744}
745
746impl<'a> CssPixelValueParseError<'a> {
747    pub fn to_contained(&self) -> CssPixelValueParseErrorOwned {
748        match self {
749            CssPixelValueParseError::EmptyString => CssPixelValueParseErrorOwned::EmptyString,
750            CssPixelValueParseError::NoValueGiven(s, metric) => {
751                CssPixelValueParseErrorOwned::NoValueGiven(s.to_string(), *metric)
752            }
753            CssPixelValueParseError::ValueParseErr(err, s) => {
754                CssPixelValueParseErrorOwned::ValueParseErr(err.clone(), s.to_string())
755            }
756            CssPixelValueParseError::InvalidPixelValue(s) => {
757                CssPixelValueParseErrorOwned::InvalidPixelValue(s.to_string())
758            }
759        }
760    }
761}
762
763impl CssPixelValueParseErrorOwned {
764    pub fn to_shared<'a>(&'a self) -> CssPixelValueParseError<'a> {
765        match self {
766            CssPixelValueParseErrorOwned::EmptyString => CssPixelValueParseError::EmptyString,
767            CssPixelValueParseErrorOwned::NoValueGiven(s, metric) => {
768                CssPixelValueParseError::NoValueGiven(s.as_str(), *metric)
769            }
770            CssPixelValueParseErrorOwned::ValueParseErr(err, s) => {
771                CssPixelValueParseError::ValueParseErr(err.clone(), s.as_str())
772            }
773            CssPixelValueParseErrorOwned::InvalidPixelValue(s) => {
774                CssPixelValueParseError::InvalidPixelValue(s.as_str())
775            }
776        }
777    }
778}
779
780/// parses an angle value like `30deg`, `1.64rad`, `100%`, etc.
781fn parse_pixel_value_inner<'a>(
782    input: &'a str,
783    match_values: &[(&'static str, SizeMetric)],
784) -> Result<PixelValue, CssPixelValueParseError<'a>> {
785    let input = input.trim();
786
787    if input.is_empty() {
788        return Err(CssPixelValueParseError::EmptyString);
789    }
790
791    for (match_val, metric) in match_values {
792        if input.ends_with(match_val) {
793            let value = &input[..input.len() - match_val.len()];
794            let value = value.trim();
795            if value.is_empty() {
796                return Err(CssPixelValueParseError::NoValueGiven(input, *metric));
797            }
798            match value.parse::<f32>() {
799                Ok(o) => {
800                    return Ok(PixelValue::from_metric(*metric, o));
801                }
802                Err(e) => {
803                    return Err(CssPixelValueParseError::ValueParseErr(e, value));
804                }
805            }
806        }
807    }
808
809    match input.trim().parse::<f32>() {
810        Ok(o) => Ok(PixelValue::px(o)),
811        Err(_) => Err(CssPixelValueParseError::InvalidPixelValue(input)),
812    }
813}
814
815pub fn parse_pixel_value<'a>(input: &'a str) -> Result<PixelValue, CssPixelValueParseError<'a>> {
816    parse_pixel_value_inner(
817        input,
818        &[
819            ("px", SizeMetric::Px),
820            ("rem", SizeMetric::Rem), // Must be before "em" to match correctly
821            ("em", SizeMetric::Em),
822            ("pt", SizeMetric::Pt),
823            ("in", SizeMetric::In),
824            ("mm", SizeMetric::Mm),
825            ("cm", SizeMetric::Cm),
826            ("vmax", SizeMetric::Vmax), // Must be before "vw" to match correctly
827            ("vmin", SizeMetric::Vmin), // Must be before "vw" to match correctly
828            ("vw", SizeMetric::Vw),
829            ("vh", SizeMetric::Vh),
830            ("%", SizeMetric::Percent),
831        ],
832    )
833}
834
835pub fn parse_pixel_value_no_percent<'a>(
836    input: &'a str,
837) -> Result<PixelValueNoPercent, CssPixelValueParseError<'a>> {
838    Ok(PixelValueNoPercent {
839        inner: parse_pixel_value_inner(
840            input,
841            &[
842                ("px", SizeMetric::Px),
843                ("rem", SizeMetric::Rem), // Must be before "em" to match correctly
844                ("em", SizeMetric::Em),
845                ("pt", SizeMetric::Pt),
846                ("in", SizeMetric::In),
847                ("mm", SizeMetric::Mm),
848                ("cm", SizeMetric::Cm),
849                ("vmax", SizeMetric::Vmax), // Must be before "vw" to match correctly
850                ("vmin", SizeMetric::Vmin), // Must be before "vw" to match correctly
851                ("vw", SizeMetric::Vw),
852                ("vh", SizeMetric::Vh),
853            ],
854        )?,
855    })
856}
857
858#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
859pub enum PixelValueWithAuto {
860    None,
861    Initial,
862    Inherit,
863    Auto,
864    Exact(PixelValue),
865}
866
867/// Parses a pixel value, but also tries values like "auto", "initial", "inherit" and "none"
868pub fn parse_pixel_value_with_auto<'a>(
869    input: &'a str,
870) -> Result<PixelValueWithAuto, CssPixelValueParseError<'a>> {
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#[cfg(all(test, feature = "parser"))]
882mod tests {
883    use super::*;
884
885    #[test]
886    fn test_parse_pixel_value() {
887        assert_eq!(parse_pixel_value("10px").unwrap(), PixelValue::px(10.0));
888        assert_eq!(parse_pixel_value("1.5em").unwrap(), PixelValue::em(1.5));
889        assert_eq!(parse_pixel_value("2rem").unwrap(), PixelValue::rem(2.0));
890        assert_eq!(parse_pixel_value("-20pt").unwrap(), PixelValue::pt(-20.0));
891        assert_eq!(parse_pixel_value("50%").unwrap(), PixelValue::percent(50.0));
892        assert_eq!(parse_pixel_value("1in").unwrap(), PixelValue::inch(1.0));
893        assert_eq!(parse_pixel_value("2.54cm").unwrap(), PixelValue::cm(2.54));
894        assert_eq!(parse_pixel_value("10mm").unwrap(), PixelValue::mm(10.0));
895        assert_eq!(parse_pixel_value("  0  ").unwrap(), PixelValue::px(0.0));
896    }
897
898    #[test]
899    fn test_resolve_with_context_em() {
900        // Element has font-size: 32px, margin: 0.67em
901        let context = ResolutionContext {
902            element_font_size: 32.0,
903            parent_font_size: 16.0,
904            ..Default::default()
905        };
906
907        // Margin em uses element's own font-size
908        let margin = PixelValue::em(0.67);
909        assert!(
910            (margin.resolve_with_context(&context, PropertyContext::Margin) - 21.44).abs() < 0.01
911        );
912
913        // Font-size em uses parent's font-size
914        let font_size = PixelValue::em(2.0);
915        assert_eq!(
916            font_size.resolve_with_context(&context, PropertyContext::FontSize),
917            32.0
918        );
919    }
920
921    #[test]
922    fn test_resolve_with_context_rem() {
923        // Root has font-size: 18px
924        let context = ResolutionContext {
925            element_font_size: 32.0,
926            parent_font_size: 16.0,
927            root_font_size: 18.0,
928            ..Default::default()
929        };
930
931        // Rem always uses root font-size, regardless of property
932        let margin = PixelValue::rem(2.0);
933        assert_eq!(
934            margin.resolve_with_context(&context, PropertyContext::Margin),
935            36.0
936        );
937
938        let font_size = PixelValue::rem(1.5);
939        assert_eq!(
940            font_size.resolve_with_context(&context, PropertyContext::FontSize),
941            27.0
942        );
943    }
944
945    #[test]
946    fn test_resolve_with_context_percent_margin() {
947        // Margin % uses containing block WIDTH (even for top/bottom!)
948        let context = ResolutionContext {
949            element_font_size: 16.0,
950            parent_font_size: 16.0,
951            root_font_size: 16.0,
952            containing_block_size: PhysicalSize::new(800.0, 600.0),
953            element_size: None,
954            viewport_size: PhysicalSize::new(1920.0, 1080.0),
955        };
956
957        let margin = PixelValue::percent(10.0); // 10%
958        assert_eq!(
959            margin.resolve_with_context(&context, PropertyContext::Margin),
960            80.0
961        ); // 10% of 800
962    }
963
964    #[test]
965    fn test_parse_pixel_value_no_percent() {
966        assert_eq!(
967            parse_pixel_value_no_percent("10px").unwrap().inner,
968            PixelValue::px(10.0)
969        );
970        assert!(parse_pixel_value_no_percent("50%").is_err());
971    }
972
973    #[test]
974    fn test_parse_pixel_value_with_auto() {
975        assert_eq!(
976            parse_pixel_value_with_auto("10px").unwrap(),
977            PixelValueWithAuto::Exact(PixelValue::px(10.0))
978        );
979        assert_eq!(
980            parse_pixel_value_with_auto("auto").unwrap(),
981            PixelValueWithAuto::Auto
982        );
983        assert_eq!(
984            parse_pixel_value_with_auto("initial").unwrap(),
985            PixelValueWithAuto::Initial
986        );
987        assert_eq!(
988            parse_pixel_value_with_auto("inherit").unwrap(),
989            PixelValueWithAuto::Inherit
990        );
991        assert_eq!(
992            parse_pixel_value_with_auto("none").unwrap(),
993            PixelValueWithAuto::None
994        );
995    }
996
997    #[test]
998    fn test_parse_pixel_value_errors() {
999        assert!(parse_pixel_value("").is_err());
1000        // Modern CSS parsers can be liberal - unitless numbers treated as px
1001        assert!(parse_pixel_value("10").is_ok()); // Parsed as 10px
1002                                                  // This parser is liberal and trims whitespace, so "10 px" is accepted
1003        assert!(parse_pixel_value("10 px").is_ok()); // Liberal parsing accepts this
1004        assert!(parse_pixel_value("px").is_err());
1005        assert!(parse_pixel_value("ten-px").is_err());
1006    }
1007}