Skip to main content

azul_css/props/basic/
pixel.rs

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