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    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}
204
205/// Specifies which property context we're resolving for, to determine correct reference values
206#[derive(Debug, Copy, Clone, PartialEq, Eq)]
207pub enum PropertyContext {
208    /// Resolving for the font-size property itself (em refers to parent)
209    FontSize,
210    /// Resolving for margin properties (% refers to containing block width)
211    Margin,
212    /// Resolving for padding properties (% refers to containing block width)
213    Padding,
214    /// Resolving for width or horizontal properties (% refers to containing block width)
215    Width,
216    /// Resolving for height or vertical properties (% refers to containing block height)
217    Height,
218    /// Resolving for border-width properties (only absolute lengths + em/rem, no % support)
219    BorderWidth,
220    /// Resolving for border-radius (% refers to element's own dimensions)
221    BorderRadius,
222    /// Resolving for transforms (% refers to element's own dimensions)
223    Transform,
224    /// Resolving for other properties (em refers to element font-size)
225    Other,
226}
227
228/// A CSS length value consisting of a numeric value and a unit (px, em, rem, %, etc.).
229#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
230#[repr(C)]
231pub struct PixelValue {
232    pub metric: SizeMetric,
233    pub number: FloatValue,
234}
235
236impl PixelValue {
237    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
238        self.number = FloatValue::new(self.number.get() * scale_factor);
239    }
240}
241
242impl FormatAsCssValue for PixelValue {
243    fn format_as_css_value(&self, f: &mut fmt::Formatter) -> fmt::Result {
244        write!(f, "{}{}", self.number, self.metric)
245    }
246}
247
248impl crate::css::PrintAsCssValue for PixelValue {
249    fn print_as_css_value(&self) -> String {
250        format!("{}{}", self.number, self.metric)
251    }
252}
253
254impl crate::format_rust_code::FormatAsRustCode for PixelValue {
255    fn format_as_rust_code(&self, _tabs: usize) -> String {
256        format!(
257            "PixelValue {{ metric: {:?}, number: FloatValue::new({}) }}",
258            self.metric,
259            self.number.get()
260        )
261    }
262}
263
264// Manual Debug implementation, because the auto-generated one is nearly unreadable
265impl fmt::Debug for PixelValue {
266    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
267        write!(f, "{}{}", self.number, self.metric)
268    }
269}
270
271impl fmt::Display for PixelValue {
272    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
273        write!(f, "{}{}", self.number, self.metric)
274    }
275}
276
277impl PixelValue {
278    #[inline]
279    pub const fn zero() -> Self {
280        const ZERO_PX: PixelValue = PixelValue::const_px(0);
281        ZERO_PX
282    }
283
284    /// Same as `PixelValue::px()`, but only accepts whole numbers,
285    /// since using `f32` in const fn is not yet stabilized.
286    #[inline]
287    pub const fn const_px(value: isize) -> Self {
288        Self::const_from_metric(SizeMetric::Px, value)
289    }
290
291    /// Same as `PixelValue::em()`, but only accepts whole numbers,
292    /// since using `f32` in const fn is not yet stabilized.
293    #[inline]
294    pub const fn const_em(value: isize) -> Self {
295        Self::const_from_metric(SizeMetric::Em, value)
296    }
297
298    /// Creates an em value from a fractional number in const context.
299    ///
300    /// # Arguments
301    /// * `pre_comma` - The integer part (e.g., 1 for 1.5em)
302    /// * `post_comma` - The fractional part as digits (e.g., 5 for 0.5em, 83 for 0.83em)
303    ///
304    /// # Examples
305    /// ```
306    /// // 1.5em = const_em_fractional(1, 5)
307    /// // 0.83em = const_em_fractional(0, 83)
308    /// // 1.17em = const_em_fractional(1, 17)
309    /// ```
310    #[inline]
311    pub const fn const_em_fractional(pre_comma: isize, post_comma: isize) -> Self {
312        Self::const_from_metric_fractional(SizeMetric::Em, pre_comma, post_comma)
313    }
314
315    /// Same as `PixelValue::pt()`, but only accepts whole numbers,
316    /// since using `f32` in const fn is not yet stabilized.
317    #[inline]
318    pub const fn const_pt(value: isize) -> Self {
319        Self::const_from_metric(SizeMetric::Pt, value)
320    }
321
322    /// Creates a pt value from a fractional number in const context.
323    #[inline]
324    pub const fn const_pt_fractional(pre_comma: isize, post_comma: isize) -> Self {
325        Self::const_from_metric_fractional(SizeMetric::Pt, pre_comma, post_comma)
326    }
327
328    /// Same as `PixelValue::percent()`, but only accepts whole numbers,
329    /// since using `f32` in const fn is not yet stabilized.
330    #[inline]
331    pub const fn const_percent(value: isize) -> Self {
332        Self::const_from_metric(SizeMetric::Percent, value)
333    }
334
335    /// Same as `PixelValue::in()`, but only accepts whole numbers,
336    /// since using `f32` in const fn is not yet stabilized.
337    #[inline]
338    pub const fn const_in(value: isize) -> Self {
339        Self::const_from_metric(SizeMetric::In, value)
340    }
341
342    /// Same as `PixelValue::cm()`, but only accepts whole numbers,
343    /// since using `f32` in const fn is not yet stabilized.
344    #[inline]
345    pub const fn const_cm(value: isize) -> Self {
346        Self::const_from_metric(SizeMetric::Cm, value)
347    }
348
349    /// Same as `PixelValue::mm()`, but only accepts whole numbers,
350    /// since using `f32` in const fn is not yet stabilized.
351    #[inline]
352    pub const fn const_mm(value: isize) -> Self {
353        Self::const_from_metric(SizeMetric::Mm, value)
354    }
355
356    #[inline]
357    pub const fn const_from_metric(metric: SizeMetric, value: isize) -> Self {
358        Self {
359            metric,
360            number: FloatValue::const_new(value),
361        }
362    }
363
364    /// Creates a PixelValue from a fractional number in const context.
365    ///
366    /// # Arguments
367    /// * `metric` - The size metric (Px, Em, Pt, etc.)
368    /// * `pre_comma` - The integer part
369    /// * `post_comma` - The fractional part as digits
370    #[inline]
371    pub const fn const_from_metric_fractional(
372        metric: SizeMetric,
373        pre_comma: isize,
374        post_comma: isize,
375    ) -> Self {
376        Self {
377            metric,
378            number: FloatValue::const_new_fractional(pre_comma, post_comma),
379        }
380    }
381
382    #[inline]
383    pub fn px(value: f32) -> Self {
384        Self::from_metric(SizeMetric::Px, value)
385    }
386
387    #[inline]
388    pub fn em(value: f32) -> Self {
389        Self::from_metric(SizeMetric::Em, value)
390    }
391
392    #[inline]
393    pub fn inch(value: f32) -> Self {
394        Self::from_metric(SizeMetric::In, value)
395    }
396
397    #[inline]
398    pub fn cm(value: f32) -> Self {
399        Self::from_metric(SizeMetric::Cm, value)
400    }
401
402    #[inline]
403    pub fn mm(value: f32) -> Self {
404        Self::from_metric(SizeMetric::Mm, value)
405    }
406
407    #[inline]
408    pub fn pt(value: f32) -> Self {
409        Self::from_metric(SizeMetric::Pt, value)
410    }
411
412    #[inline]
413    pub fn percent(value: f32) -> Self {
414        Self::from_metric(SizeMetric::Percent, value)
415    }
416
417    #[inline]
418    pub fn rem(value: f32) -> Self {
419        Self::from_metric(SizeMetric::Rem, value)
420    }
421
422    #[inline]
423    pub fn from_metric(metric: SizeMetric, value: f32) -> Self {
424        Self {
425            metric,
426            number: FloatValue::new(value),
427        }
428    }
429
430    #[inline]
431    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
432        if self.metric == other.metric {
433            Self {
434                metric: self.metric,
435                number: self.number.interpolate(&other.number, t),
436            }
437        } else {
438            // Interpolate between different metrics by converting to px
439            // Note: Uses DEFAULT_FONT_SIZE for em/rem - acceptable for animation fallback
440            let self_px_interp = self.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
441            let other_px_interp = other.to_pixels_internal(0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
442            Self::from_metric(
443                SizeMetric::Px,
444                self_px_interp + (other_px_interp - self_px_interp) * t,
445            )
446        }
447    }
448
449    /// Returns the value of the SizeMetric as a normalized percentage (0.0 = 0%, 1.0 = 100%)
450    ///
451    /// Returns `Some(NormalizedPercentage)` if this is a percentage value, `None` otherwise.
452    /// The returned `NormalizedPercentage` is already normalized to 0.0-1.0 range,
453    /// so you should multiply it directly with the containing block size.
454    #[inline]
455    pub fn to_percent(&self) -> Option<NormalizedPercentage> {
456        match self.metric {
457            SizeMetric::Percent => Some(NormalizedPercentage::from_unnormalized(self.number.get())),
458            _ => None,
459        }
460    }
461
462    /// Internal fallback method for converting to pixels with manual % resolution.
463    ///
464    /// Used internally by prop_cache.rs resolve_property_dependency().
465    ///
466    /// **DO NOT USE directly!** Use `resolve_with_context()` instead for new code.
467    #[doc(hidden)]
468    #[inline]
469    pub fn to_pixels_internal(&self, percent_resolve: f32, em_resolve: f32, rem_resolve: f32) -> f32 {
470        match self.metric {
471            SizeMetric::Px => self.number.get(),
472            SizeMetric::Pt => self.number.get() * PT_TO_PX,
473            SizeMetric::In => self.number.get() * 96.0,
474            SizeMetric::Cm => self.number.get() * 96.0 / 2.54,
475            SizeMetric::Mm => self.number.get() * 96.0 / 25.4,
476            SizeMetric::Em => self.number.get() * em_resolve,
477            SizeMetric::Rem => self.number.get() * rem_resolve,
478            SizeMetric::Percent => {
479                NormalizedPercentage::from_unnormalized(self.number.get()).resolve(percent_resolve)
480            }
481            // Viewport units: Cannot resolve without viewport context, return 0
482            // These should use resolve_with_context() instead
483            SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => 0.0,
484        }
485    }
486
487    /// Resolve this value to pixels using proper CSS context.
488    ///
489    /// This is the **CORRECT** way to resolve CSS units. It properly handles:
490    /// - em units: Uses element's own font-size (or parent's for font-size property)
491    /// - rem units: Uses root element's font-size
492    /// - % units: Uses property-appropriate reference (containing block width/height, element size,
493    ///   etc.)
494    /// - Absolute units: px, pt, in, cm, mm (already correct)
495    ///
496    /// # Arguments
497    /// * `context` - Resolution context with font sizes and dimensions
498    /// * `property_context` - Which property we're resolving for (affects % and em resolution)
499    #[inline]
500    pub fn resolve_with_context(
501        &self,
502        context: &ResolutionContext,
503        property_context: PropertyContext,
504    ) -> f32 {
505        match self.metric {
506            // Absolute units - already correct
507            SizeMetric::Px => self.number.get(),
508            SizeMetric::Pt => self.number.get() * PT_TO_PX,
509            SizeMetric::In => self.number.get() * 96.0,
510            SizeMetric::Cm => self.number.get() * 96.0 / 2.54,
511            SizeMetric::Mm => self.number.get() * 96.0 / 25.4,
512
513            // Em units - CRITICAL: different resolution for font-size vs other properties
514            SizeMetric::Em => {
515                let reference_font_size = if property_context == PropertyContext::FontSize {
516                    // Em on font-size refers to parent's font-size (CSS 2.1 §15.7)
517                    context.parent_font_size
518                } else {
519                    // Em on other properties refers to element's own font-size (CSS 2.1 §10.5)
520                    context.element_font_size
521                };
522                self.number.get() * reference_font_size
523            }
524
525            // Rem units - ALWAYS refer to root font-size (CSS Values 3)
526            SizeMetric::Rem => self.number.get() * context.root_font_size,
527
528            // Viewport units - refer to viewport dimensions (CSS Values 3 §6.2)
529            // 1vw = 1% of viewport width, 1vh = 1% of viewport height
530            SizeMetric::Vw => self.number.get() * context.viewport_size.width / 100.0,
531            SizeMetric::Vh => self.number.get() * context.viewport_size.height / 100.0,
532            // vmin = smaller of vw or vh
533            SizeMetric::Vmin => {
534                let min_dimension = context
535                    .viewport_size
536                    .width
537                    .min(context.viewport_size.height);
538                self.number.get() * min_dimension / 100.0
539            }
540            // vmax = larger of vw or vh
541            SizeMetric::Vmax => {
542                let max_dimension = context
543                    .viewport_size
544                    .width
545                    .max(context.viewport_size.height);
546                self.number.get() * max_dimension / 100.0
547            }
548
549            // Percent units - reference depends on property type
550            SizeMetric::Percent => {
551                let reference = match property_context {
552                    // Font-size %: refers to parent's font-size (CSS 2.1 §15.7)
553                    PropertyContext::FontSize => context.parent_font_size,
554
555                    // Width and horizontal properties: containing block width (CSS 2.1 §10.3)
556                    PropertyContext::Width => context.containing_block_size.width,
557
558                    // Height and vertical properties: containing block height (CSS 2.1 §10.5)
559                    PropertyContext::Height => context.containing_block_size.height,
560
561                    // +spec:box-model:66e123 - margin/padding % resolved against inline size (= width in horizontal-tb)
562                    // +spec:width-calculation:bef810 - margin percentages refer to containing block width (even top/bottom)
563                    // Margins: ALWAYS containing block WIDTH, even for top/bottom! (CSS 2.1 §8.3)
564                    // +spec:width-calculation:d78514 - margin percentages refer to width of containing block
565                    // Padding: ALWAYS containing block WIDTH, even for top/bottom! (CSS 2.1 §8.4)
566                    PropertyContext::Margin | PropertyContext::Padding => {
567                        context.containing_block_size.width
568                    }
569
570                    // Border-width: % is NOT valid per CSS spec (CSS Backgrounds 3 §4.1)
571                    // Return 0.0 if someone tries to use % on border-width
572                    PropertyContext::BorderWidth => 0.0,
573
574                    // Border-radius: element's own dimensions (CSS Backgrounds 3 §5.1)
575                    // Note: More complex - horizontal % uses width, vertical % uses height
576                    // For now, use width as default
577                    PropertyContext::BorderRadius => {
578                        context.element_size.map(|s| s.width).unwrap_or(0.0)
579                    }
580
581                    // Transforms: element's own dimensions (CSS Transforms §20.1)
582                    PropertyContext::Transform => {
583                        context.element_size.map(|s| s.width).unwrap_or(0.0)
584                    }
585
586                    // Other properties: default to containing block width
587                    PropertyContext::Other => context.containing_block_size.width,
588                };
589
590                NormalizedPercentage::from_unnormalized(self.number.get()).resolve(reference)
591            }
592        }
593    }
594}
595
596// border-width: thin / medium / thick keyword values
597// These are the canonical CSS definitions and should be used consistently
598// across parsing and resolution.
599
600/// border-width: thin = 1px (per CSS spec)
601pub const THIN_BORDER_THICKNESS: PixelValue = PixelValue {
602    metric: SizeMetric::Px,
603    number: FloatValue { number: 1000 },
604};
605
606/// border-width: medium = 3px (per CSS spec, default)
607pub const MEDIUM_BORDER_THICKNESS: PixelValue = PixelValue {
608    metric: SizeMetric::Px,
609    number: FloatValue { number: 3000 },
610};
611
612/// border-width: thick = 5px (per CSS spec)
613pub const THICK_BORDER_THICKNESS: PixelValue = PixelValue {
614    metric: SizeMetric::Px,
615    number: FloatValue { number: 5000 },
616};
617
618/// Same as PixelValue, but doesn't allow a "%" sign
619#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
620#[repr(C)]
621pub struct PixelValueNoPercent {
622    pub inner: PixelValue,
623}
624
625impl PixelValueNoPercent {
626    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
627        self.inner.scale_for_dpi(scale_factor);
628    }
629}
630
631impl_option!(
632    PixelValueNoPercent,
633    OptionPixelValueNoPercent,
634    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
635);
636
637impl_option!(
638    PixelValue,
639    OptionPixelValue,
640    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
641);
642
643impl fmt::Display for PixelValueNoPercent {
644    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
645        write!(f, "{}", self.inner)
646    }
647}
648
649impl ::core::fmt::Debug for PixelValueNoPercent {
650    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
651        write!(f, "{}", self)
652    }
653}
654
655impl PixelValueNoPercent {
656    /// Internal conversion to pixels (no percent support).
657    ///
658    /// Used internally by prop_cache.rs.
659    ///
660    /// **DO NOT USE directly!** Use `resolve_with_context()` on inner value instead.
661    #[doc(hidden)]
662    #[inline]
663    pub fn to_pixels_internal(&self, em_resolve: f32, rem_resolve: f32) -> f32 {
664        self.inner.to_pixels_internal(0.0, em_resolve, rem_resolve)
665    }
666
667    #[inline]
668    pub const fn zero() -> Self {
669        const ZERO_PXNP: PixelValueNoPercent = PixelValueNoPercent {
670            inner: PixelValue::zero(),
671        };
672        ZERO_PXNP
673    }
674}
675impl From<PixelValue> for PixelValueNoPercent {
676    fn from(e: PixelValue) -> Self {
677        Self { inner: e }
678    }
679}
680
681#[derive(Clone, PartialEq)]
682pub enum CssPixelValueParseError<'a> {
683    EmptyString,
684    NoValueGiven(&'a str, SizeMetric),
685    ValueParseErr(ParseFloatError, &'a str),
686    InvalidPixelValue(&'a str),
687}
688
689impl_debug_as_display!(CssPixelValueParseError<'a>);
690
691impl_display! { CssPixelValueParseError<'a>, {
692    EmptyString => format!("Missing [px / pt / em / %] value"),
693    NoValueGiven(input, metric) => format!("Expected floating-point pixel value, got: \"{}{}\"", input, metric),
694    ValueParseErr(err, number_str) => format!("Could not parse \"{}\" as floating-point value: \"{}\"", number_str, err),
695    InvalidPixelValue(s) => format!("Invalid pixel value: \"{}\"", s),
696}}
697
698/// Wrapper for NoValueGiven error in pixel value parsing.
699#[derive(Debug, Clone, PartialEq)]
700#[repr(C)]
701pub struct PixelNoValueGivenError {
702    pub value: AzString,
703    pub metric: SizeMetric,
704}
705
706/// Owned version of CssPixelValueParseError.
707#[derive(Debug, Clone, PartialEq)]
708#[repr(C, u8)]
709pub enum CssPixelValueParseErrorOwned {
710    EmptyString,
711    NoValueGiven(PixelNoValueGivenError),
712    ValueParseErr(ParseFloatErrorWithInput),
713    InvalidPixelValue(AzString),
714}
715
716impl<'a> CssPixelValueParseError<'a> {
717    pub fn to_contained(&self) -> CssPixelValueParseErrorOwned {
718        match self {
719            CssPixelValueParseError::EmptyString => CssPixelValueParseErrorOwned::EmptyString,
720            CssPixelValueParseError::NoValueGiven(s, metric) => {
721                CssPixelValueParseErrorOwned::NoValueGiven(PixelNoValueGivenError { value: s.to_string().into(), metric: *metric })
722            }
723            CssPixelValueParseError::ValueParseErr(err, s) => {
724                CssPixelValueParseErrorOwned::ValueParseErr(ParseFloatErrorWithInput { error: err.clone().into(), input: s.to_string().into() })
725            }
726            CssPixelValueParseError::InvalidPixelValue(s) => {
727                CssPixelValueParseErrorOwned::InvalidPixelValue(s.to_string().into())
728            }
729        }
730    }
731}
732
733impl CssPixelValueParseErrorOwned {
734    pub fn to_shared<'a>(&'a self) -> CssPixelValueParseError<'a> {
735        match self {
736            CssPixelValueParseErrorOwned::EmptyString => CssPixelValueParseError::EmptyString,
737            CssPixelValueParseErrorOwned::NoValueGiven(e) => {
738                CssPixelValueParseError::NoValueGiven(e.value.as_str(), e.metric)
739            }
740            CssPixelValueParseErrorOwned::ValueParseErr(e) => {
741                CssPixelValueParseError::ValueParseErr(e.error.to_std(), e.input.as_str())
742            }
743            CssPixelValueParseErrorOwned::InvalidPixelValue(s) => {
744                CssPixelValueParseError::InvalidPixelValue(s.as_str())
745            }
746        }
747    }
748}
749
750/// parses an angle value like `30deg`, `1.64rad`, `100%`, etc.
751fn parse_pixel_value_inner<'a>(
752    input: &'a str,
753    match_values: &[(&'static str, SizeMetric)],
754) -> Result<PixelValue, CssPixelValueParseError<'a>> {
755    let input = input.trim();
756
757    if input.is_empty() {
758        return Err(CssPixelValueParseError::EmptyString);
759    }
760
761    for (match_val, metric) in match_values {
762        if input.ends_with(match_val) {
763            let value = &input[..input.len() - match_val.len()];
764            let value = value.trim();
765            if value.is_empty() {
766                return Err(CssPixelValueParseError::NoValueGiven(input, *metric));
767            }
768            match value.parse::<f32>() {
769                Ok(o) => {
770                    return Ok(PixelValue::from_metric(*metric, o));
771                }
772                Err(e) => {
773                    return Err(CssPixelValueParseError::ValueParseErr(e, value));
774                }
775            }
776        }
777    }
778
779    match input.trim().parse::<f32>() {
780        Ok(o) => Ok(PixelValue::px(o)),
781        Err(_) => Err(CssPixelValueParseError::InvalidPixelValue(input)),
782    }
783}
784
785pub fn parse_pixel_value<'a>(input: &'a str) -> Result<PixelValue, CssPixelValueParseError<'a>> {
786    parse_pixel_value_inner(
787        input,
788        &[
789            ("px", SizeMetric::Px),
790            ("rem", SizeMetric::Rem), // Must be before "em" to match correctly
791            ("em", SizeMetric::Em),
792            ("pt", SizeMetric::Pt),
793            ("in", SizeMetric::In),
794            ("mm", SizeMetric::Mm),
795            ("cm", SizeMetric::Cm),
796            ("vmax", SizeMetric::Vmax), // Must be before "vw" to match correctly
797            ("vmin", SizeMetric::Vmin), // Must be before "vw" to match correctly
798            ("vw", SizeMetric::Vw),
799            ("vh", SizeMetric::Vh),
800            ("%", SizeMetric::Percent),
801        ],
802    )
803}
804
805pub fn parse_pixel_value_no_percent<'a>(
806    input: &'a str,
807) -> Result<PixelValueNoPercent, CssPixelValueParseError<'a>> {
808    Ok(PixelValueNoPercent {
809        inner: parse_pixel_value_inner(
810            input,
811            &[
812                ("px", SizeMetric::Px),
813                ("rem", SizeMetric::Rem), // Must be before "em" to match correctly
814                ("em", SizeMetric::Em),
815                ("pt", SizeMetric::Pt),
816                ("in", SizeMetric::In),
817                ("mm", SizeMetric::Mm),
818                ("cm", SizeMetric::Cm),
819                ("vmax", SizeMetric::Vmax), // Must be before "vw" to match correctly
820                ("vmin", SizeMetric::Vmin), // Must be before "vw" to match correctly
821                ("vw", SizeMetric::Vw),
822                ("vh", SizeMetric::Vh),
823            ],
824        )?,
825    })
826}
827
828#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
829pub enum PixelValueWithAuto {
830    None,
831    Initial,
832    Inherit,
833    Auto,
834    Exact(PixelValue),
835}
836
837/// Parses a pixel value, but also tries values like "auto", "initial", "inherit" and "none"
838pub fn parse_pixel_value_with_auto<'a>(
839    input: &'a str,
840) -> Result<PixelValueWithAuto, CssPixelValueParseError<'a>> {
841    let input = input.trim();
842    match input {
843        "none" => Ok(PixelValueWithAuto::None),
844        "initial" => Ok(PixelValueWithAuto::Initial),
845        "inherit" => Ok(PixelValueWithAuto::Inherit),
846        "auto" => Ok(PixelValueWithAuto::Auto),
847        e => Ok(PixelValueWithAuto::Exact(parse_pixel_value(e)?)),
848    }
849}
850
851// ============================================================================
852// System Metric References (system:button-padding, system:button-radius, etc.)
853// ============================================================================
854
855/// Reference to a specific system metric value.
856/// These are resolved at runtime based on the user's system preferences.
857/// 
858/// CSS syntax: `system:button-padding`, `system:button-radius`, `system:titlebar-height`, etc.
859#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
860#[repr(C)]
861#[derive(Default)]
862pub enum SystemMetricRef {
863    /// Button corner radius (system:button-radius)
864    #[default]
865    ButtonRadius,
866    /// Button horizontal padding (system:button-padding-horizontal)
867    ButtonPaddingHorizontal,
868    /// Button vertical padding (system:button-padding-vertical)
869    ButtonPaddingVertical,
870    /// Button border width (system:button-border-width)
871    ButtonBorderWidth,
872    /// Titlebar height (system:titlebar-height)
873    TitlebarHeight,
874    /// Titlebar button area width (system:titlebar-button-width)
875    TitlebarButtonWidth,
876    /// Titlebar horizontal padding (system:titlebar-padding)
877    TitlebarPadding,
878    /// Safe area top inset for notched devices (system:safe-area-top)
879    SafeAreaTop,
880    /// Safe area bottom inset (system:safe-area-bottom)
881    SafeAreaBottom,
882    /// Safe area left inset (system:safe-area-left)
883    SafeAreaLeft,
884    /// Safe area right inset (system:safe-area-right)
885    SafeAreaRight,
886}
887
888
889impl SystemMetricRef {
890    /// Resolve this system metric reference against actual system metrics.
891    pub fn resolve(&self, metrics: &crate::system::SystemMetrics) -> Option<PixelValue> {
892        match self {
893            SystemMetricRef::ButtonRadius => metrics.corner_radius.as_option().copied(),
894            SystemMetricRef::ButtonPaddingHorizontal => metrics.button_padding_horizontal.as_option().copied(),
895            SystemMetricRef::ButtonPaddingVertical => metrics.button_padding_vertical.as_option().copied(),
896            SystemMetricRef::ButtonBorderWidth => metrics.border_width.as_option().copied(),
897            SystemMetricRef::TitlebarHeight => metrics.titlebar.height.as_option().copied(),
898            SystemMetricRef::TitlebarButtonWidth => metrics.titlebar.button_area_width.as_option().copied(),
899            SystemMetricRef::TitlebarPadding => metrics.titlebar.padding_horizontal.as_option().copied(),
900            SystemMetricRef::SafeAreaTop => metrics.titlebar.safe_area.top.as_option().copied(),
901            SystemMetricRef::SafeAreaBottom => metrics.titlebar.safe_area.bottom.as_option().copied(),
902            SystemMetricRef::SafeAreaLeft => metrics.titlebar.safe_area.left.as_option().copied(),
903            SystemMetricRef::SafeAreaRight => metrics.titlebar.safe_area.right.as_option().copied(),
904        }
905    }
906
907    /// Returns the CSS string representation of this system metric reference.
908    pub fn as_css_str(&self) -> &'static str {
909        match self {
910            SystemMetricRef::ButtonRadius => "system:button-radius",
911            SystemMetricRef::ButtonPaddingHorizontal => "system:button-padding-horizontal",
912            SystemMetricRef::ButtonPaddingVertical => "system:button-padding-vertical",
913            SystemMetricRef::ButtonBorderWidth => "system:button-border-width",
914            SystemMetricRef::TitlebarHeight => "system:titlebar-height",
915            SystemMetricRef::TitlebarButtonWidth => "system:titlebar-button-width",
916            SystemMetricRef::TitlebarPadding => "system:titlebar-padding",
917            SystemMetricRef::SafeAreaTop => "system:safe-area-top",
918            SystemMetricRef::SafeAreaBottom => "system:safe-area-bottom",
919            SystemMetricRef::SafeAreaLeft => "system:safe-area-left",
920            SystemMetricRef::SafeAreaRight => "system:safe-area-right",
921        }
922    }
923
924    /// Parse a system metric reference from a CSS string (without the "system:" prefix).
925    pub fn from_css_str(s: &str) -> Option<Self> {
926        match s {
927            "button-radius" => Some(SystemMetricRef::ButtonRadius),
928            "button-padding-horizontal" => Some(SystemMetricRef::ButtonPaddingHorizontal),
929            "button-padding-vertical" => Some(SystemMetricRef::ButtonPaddingVertical),
930            "button-border-width" => Some(SystemMetricRef::ButtonBorderWidth),
931            "titlebar-height" => Some(SystemMetricRef::TitlebarHeight),
932            "titlebar-button-width" => Some(SystemMetricRef::TitlebarButtonWidth),
933            "titlebar-padding" => Some(SystemMetricRef::TitlebarPadding),
934            "safe-area-top" => Some(SystemMetricRef::SafeAreaTop),
935            "safe-area-bottom" => Some(SystemMetricRef::SafeAreaBottom),
936            "safe-area-left" => Some(SystemMetricRef::SafeAreaLeft),
937            "safe-area-right" => Some(SystemMetricRef::SafeAreaRight),
938            _ => None,
939        }
940    }
941}
942
943impl fmt::Display for SystemMetricRef {
944    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
945        write!(f, "{}", self.as_css_str())
946    }
947}
948
949impl FormatAsCssValue for SystemMetricRef {
950    fn format_as_css_value(&self, f: &mut fmt::Formatter) -> fmt::Result {
951        write!(f, "{}", self.as_css_str())
952    }
953}
954
955/// A pixel value reference that can be either a concrete value or a system metric.
956/// System metrics are lazily evaluated at runtime based on the user's system theme.
957/// 
958/// CSS syntax: `10px`, `1.5em`, `system:button-padding`, etc.
959#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
960#[repr(C, u8)]
961pub enum PixelValueOrSystem {
962    /// A concrete pixel value.
963    Value(PixelValue),
964    /// A reference to a system metric, resolved at runtime.
965    System(SystemMetricRef),
966}
967
968impl Default for PixelValueOrSystem {
969    fn default() -> Self {
970        PixelValueOrSystem::Value(PixelValue::zero())
971    }
972}
973
974impl From<PixelValue> for PixelValueOrSystem {
975    fn from(value: PixelValue) -> Self {
976        PixelValueOrSystem::Value(value)
977    }
978}
979
980impl PixelValueOrSystem {
981    /// Create a new PixelValueOrSystem from a concrete value.
982    pub const fn value(v: PixelValue) -> Self {
983        PixelValueOrSystem::Value(v)
984    }
985    
986    /// Create a new PixelValueOrSystem from a system metric reference.
987    pub const fn system(s: SystemMetricRef) -> Self {
988        PixelValueOrSystem::System(s)
989    }
990    
991    /// Resolve the pixel value against a SystemMetrics struct.
992    /// Returns the system metric if available, or falls back to the provided default.
993    pub fn resolve(&self, system_metrics: &crate::system::SystemMetrics, fallback: PixelValue) -> PixelValue {
994        match self {
995            PixelValueOrSystem::Value(v) => *v,
996            PixelValueOrSystem::System(ref_type) => ref_type.resolve(system_metrics).unwrap_or(fallback),
997        }
998    }
999    
1000}
1001
1002impl fmt::Display for PixelValueOrSystem {
1003    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1004        match self {
1005            PixelValueOrSystem::Value(v) => write!(f, "{}", v),
1006            PixelValueOrSystem::System(s) => write!(f, "{}", s),
1007        }
1008    }
1009}
1010
1011impl FormatAsCssValue for PixelValueOrSystem {
1012    fn format_as_css_value(&self, f: &mut fmt::Formatter) -> fmt::Result {
1013        match self {
1014            PixelValueOrSystem::Value(v) => v.format_as_css_value(f),
1015            PixelValueOrSystem::System(s) => s.format_as_css_value(f),
1016        }
1017    }
1018}
1019
1020/// Parse a pixel value that may include system metric references.
1021/// 
1022/// Accepts: `10px`, `1.5em`, `system:button-padding`, etc.
1023#[cfg(feature = "parser")]
1024pub fn parse_pixel_value_or_system<'a>(
1025    input: &'a str,
1026) -> Result<PixelValueOrSystem, CssPixelValueParseError<'a>> {
1027    let input = input.trim();
1028    
1029    // Check for system metric reference
1030    if let Some(metric_name) = input.strip_prefix("system:") {
1031        if let Some(metric_ref) = SystemMetricRef::from_css_str(metric_name) {
1032            return Ok(PixelValueOrSystem::System(metric_ref));
1033        } else {
1034            return Err(CssPixelValueParseError::InvalidPixelValue(input));
1035        }
1036    }
1037    
1038    // Parse as regular pixel value
1039    Ok(PixelValueOrSystem::Value(parse_pixel_value(input)?))
1040}
1041
1042#[cfg(all(test, feature = "parser"))]
1043mod tests {
1044    use super::*;
1045
1046    #[test]
1047    fn test_parse_pixel_value() {
1048        assert_eq!(parse_pixel_value("10px").unwrap(), PixelValue::px(10.0));
1049        assert_eq!(parse_pixel_value("1.5em").unwrap(), PixelValue::em(1.5));
1050        assert_eq!(parse_pixel_value("2rem").unwrap(), PixelValue::rem(2.0));
1051        assert_eq!(parse_pixel_value("-20pt").unwrap(), PixelValue::pt(-20.0));
1052        assert_eq!(parse_pixel_value("50%").unwrap(), PixelValue::percent(50.0));
1053        assert_eq!(parse_pixel_value("1in").unwrap(), PixelValue::inch(1.0));
1054        assert_eq!(parse_pixel_value("2.54cm").unwrap(), PixelValue::cm(2.54));
1055        assert_eq!(parse_pixel_value("10mm").unwrap(), PixelValue::mm(10.0));
1056        assert_eq!(parse_pixel_value("  0  ").unwrap(), PixelValue::px(0.0));
1057    }
1058
1059    #[test]
1060    fn test_resolve_with_context_em() {
1061        // Element has font-size: 32px, margin: 0.67em
1062        let context = ResolutionContext {
1063            element_font_size: 32.0,
1064            parent_font_size: 16.0,
1065            ..Default::default()
1066        };
1067
1068        // Margin em uses element's own font-size
1069        let margin = PixelValue::em(0.67);
1070        assert!(
1071            (margin.resolve_with_context(&context, PropertyContext::Margin) - 21.44).abs() < 0.01
1072        );
1073
1074        // Font-size em uses parent's font-size
1075        let font_size = PixelValue::em(2.0);
1076        assert_eq!(
1077            font_size.resolve_with_context(&context, PropertyContext::FontSize),
1078            32.0
1079        );
1080    }
1081
1082    #[test]
1083    fn test_resolve_with_context_rem() {
1084        // Root has font-size: 18px
1085        let context = ResolutionContext {
1086            element_font_size: 32.0,
1087            parent_font_size: 16.0,
1088            root_font_size: 18.0,
1089            ..Default::default()
1090        };
1091
1092        // Rem always uses root font-size, regardless of property
1093        let margin = PixelValue::rem(2.0);
1094        assert_eq!(
1095            margin.resolve_with_context(&context, PropertyContext::Margin),
1096            36.0
1097        );
1098
1099        let font_size = PixelValue::rem(1.5);
1100        assert_eq!(
1101            font_size.resolve_with_context(&context, PropertyContext::FontSize),
1102            27.0
1103        );
1104    }
1105
1106    #[test]
1107    fn test_resolve_with_context_percent_margin() {
1108        // Margin % uses containing block WIDTH (even for top/bottom!)
1109        let context = ResolutionContext {
1110            element_font_size: 16.0,
1111            parent_font_size: 16.0,
1112            root_font_size: 16.0,
1113            containing_block_size: PhysicalSize::new(800.0, 600.0),
1114            element_size: None,
1115            viewport_size: PhysicalSize::new(1920.0, 1080.0),
1116        };
1117
1118        let margin = PixelValue::percent(10.0); // 10%
1119        assert_eq!(
1120            margin.resolve_with_context(&context, PropertyContext::Margin),
1121            80.0
1122        ); // 10% of 800
1123    }
1124
1125    #[test]
1126    fn test_parse_pixel_value_no_percent() {
1127        assert_eq!(
1128            parse_pixel_value_no_percent("10px").unwrap().inner,
1129            PixelValue::px(10.0)
1130        );
1131        assert!(parse_pixel_value_no_percent("50%").is_err());
1132    }
1133
1134    #[test]
1135    fn test_parse_pixel_value_with_auto() {
1136        assert_eq!(
1137            parse_pixel_value_with_auto("10px").unwrap(),
1138            PixelValueWithAuto::Exact(PixelValue::px(10.0))
1139        );
1140        assert_eq!(
1141            parse_pixel_value_with_auto("auto").unwrap(),
1142            PixelValueWithAuto::Auto
1143        );
1144        assert_eq!(
1145            parse_pixel_value_with_auto("initial").unwrap(),
1146            PixelValueWithAuto::Initial
1147        );
1148        assert_eq!(
1149            parse_pixel_value_with_auto("inherit").unwrap(),
1150            PixelValueWithAuto::Inherit
1151        );
1152        assert_eq!(
1153            parse_pixel_value_with_auto("none").unwrap(),
1154            PixelValueWithAuto::None
1155        );
1156    }
1157
1158    #[test]
1159    fn test_parse_pixel_value_errors() {
1160        assert!(parse_pixel_value("").is_err());
1161        // Modern CSS parsers can be liberal - unitless numbers treated as px
1162        assert!(parse_pixel_value("10").is_ok()); // Parsed as 10px
1163                                                  // This parser is liberal and trims whitespace, so "10 px" is accepted
1164        assert!(parse_pixel_value("10 px").is_ok()); // Liberal parsing accepts this
1165        assert!(parse_pixel_value("px").is_err());
1166        assert!(parse_pixel_value("ten-px").is_err());
1167    }
1168}