Skip to main content

azul_css/props/basic/
length.rs

1//! Hash-able floating-point wrappers, percentage values, and CSS size
2//! metric types used by the CSS property system.
3
4use core::fmt;
5use std::num::ParseFloatError;
6
7use crate::corety::AzString;
8
9/// Multiplier for floating point accuracy. Elements such as px or %
10/// are only accurate until a certain number of decimal points, therefore
11/// they have to be casted to isizes in order to make the f32 values
12/// hash-able: Css has a relatively low precision here, roughly 3 digits, i.e
13/// `1.001 == 1.0`
14pub const FP_PRECISION_MULTIPLIER: f32 = 1000.0;
15const FP_PRECISION_MULTIPLIER_CONST: isize = FP_PRECISION_MULTIPLIER as isize;
16
17/// Wrapper around FloatValue, represents a percentage instead
18/// of just being a regular floating-point value, i.e `5` = `5%`
19#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[repr(C)]
21pub struct PercentageValue {
22    number: FloatValue,
23}
24
25impl_option!(
26    PercentageValue,
27    OptionPercentageValue,
28    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
29);
30
31impl fmt::Display for PercentageValue {
32    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
33        write!(f, "{}%", self.normalized() * 100.0)
34    }
35}
36
37impl PercentageValue {
38    /// Same as `PercentageValue::new()`, but only accepts whole numbers.
39    /// Uses isize arithmetic to avoid floating-point in const context.
40    #[inline]
41    pub const fn const_new(value: isize) -> Self {
42        Self {
43            number: FloatValue::const_new(value),
44        }
45    }
46
47    /// Creates a PercentageValue from a fractional number in const context.
48    ///
49    /// # Arguments
50    /// * `pre_comma` - The integer part (e.g., 100 for 100.5%)
51    /// * `post_comma` - The fractional part as digits (e.g., 5 for 0.5%)
52    ///
53    /// # Examples
54    /// ```
55    /// // 100% = const_new_fractional(100, 0)
56    /// // 50.5% = const_new_fractional(50, 5)
57    /// ```
58    #[inline]
59    pub const fn const_new_fractional(pre_comma: isize, post_comma: isize) -> Self {
60        Self {
61            number: FloatValue::const_new_fractional(pre_comma, post_comma),
62        }
63    }
64
65    #[inline]
66    pub fn new(value: f32) -> Self {
67        Self {
68            number: value.into(),
69        }
70    }
71
72    // NOTE: no get() function, to avoid confusion with "150%"
73
74    #[inline]
75    pub fn normalized(&self) -> f32 {
76        self.number.get() / 100.0
77    }
78
79    #[inline]
80    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
81        Self {
82            number: self.number.interpolate(&other.number, t),
83        }
84    }
85}
86
87/// Wrapper around an f32 value that is internally casted to an isize,
88/// in order to provide hash-ability (to avoid numerical instability).
89#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
90#[repr(C)]
91pub struct FloatValue {
92    pub(crate) number: isize,
93}
94
95impl fmt::Display for FloatValue {
96    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
97        write!(f, "{}", self.get())
98    }
99}
100
101impl ::core::fmt::Debug for FloatValue {
102    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
103        write!(f, "{}", self)
104    }
105}
106
107impl Default for FloatValue {
108    fn default() -> Self {
109        const DEFAULT_FLV: FloatValue = FloatValue::const_new(0);
110        DEFAULT_FLV
111    }
112}
113
114impl FloatValue {
115    /// Same as `FloatValue::new()`, but only accepts whole numbers.
116    /// Uses isize arithmetic to avoid floating-point in const context.
117    #[inline]
118    pub const fn const_new(value: isize) -> Self {
119        Self {
120            number: value * FP_PRECISION_MULTIPLIER_CONST,
121        }
122    }
123
124    /// Creates a FloatValue from a fractional number in const context.
125    ///
126    /// This uses integer arithmetic to represent fractional values like 1.5, 0.83, etc.
127    /// in const context without relying on f32 operations.
128    ///
129    /// The function automatically detects the number of decimal places in `post_comma`
130    /// and supports up to 3 decimal places. If more digits are provided, only the first
131    /// 3 are used (truncation, not rounding).
132    ///
133    /// # Arguments
134    /// * `pre_comma` - The integer part (e.g., 1 for 1.5)
135    /// * `post_comma` - The fractional part as digits (e.g., 5 for 0.5, 52 for 0.52, 523 for 0.523)
136    ///
137    /// # Examples
138    /// ```
139    /// // 1.5 = const_new_fractional(1, 5)
140    /// // 1.52 = const_new_fractional(1, 52)
141    /// // 1.523 = const_new_fractional(1, 523)
142    /// // 0.83 = const_new_fractional(0, 83)
143    /// // 1.17 = const_new_fractional(1, 17)
144    /// // 2.123456 -> 2.123 (truncated to 3 decimal places)
145    /// ```
146    #[inline]
147    pub const fn const_new_fractional(pre_comma: isize, post_comma: isize) -> Self {
148        // Get absolute value for digit counting
149        let abs_post = if post_comma < 0 {
150            -post_comma
151        } else {
152            post_comma
153        };
154
155        // Determine the number of digits and extract only the first 3
156        // Note: We limit to values that fit in 32-bit isize for WASM compatibility
157        let (normalized_post, divisor) = if abs_post < 10 {
158            // 1 digit: 5 → 0.5
159            (abs_post, 10)
160        } else if abs_post < 100 {
161            // 2 digits: 83 → 0.83
162            (abs_post, 100)
163        } else if abs_post < 1000 {
164            // 3 digits: 523 → 0.523
165            (abs_post, 1000)
166        } else if abs_post < 10000 {
167            // 4+ digits: take first 3 (e.g., 5234 → 523 → 0.523)
168            (abs_post / 10, 1000)
169        } else if abs_post < 100000 {
170            (abs_post / 100, 1000)
171        } else if abs_post < 1000000 {
172            (abs_post / 1000, 1000)
173        } else if abs_post < 10000000 {
174            (abs_post / 10000, 1000)
175        } else if abs_post < 100000000 {
176            (abs_post / 100000, 1000)
177        } else if abs_post < 1000000000 {
178            (abs_post / 1000000, 1000)
179        } else {
180            // For very large values (>= 1 billion), cap at reasonable precision
181            // This ensures compatibility with 32-bit isize on WASM
182            (abs_post / 10000000, 1000)
183        };
184
185        // Calculate fractional part
186        let fractional_part = normalized_post * (FP_PRECISION_MULTIPLIER_CONST / divisor);
187
188        // Apply sign: if post_comma is negative, negate the fractional part
189        let signed_fractional = if post_comma < 0 {
190            -fractional_part
191        } else {
192            fractional_part
193        };
194
195        // For negative pre_comma, the fractional part should also be negative
196        // E.g., -1.5 = -1 + (-0.5), not -1 + 0.5
197        let final_fractional = if pre_comma < 0 && post_comma >= 0 {
198            -signed_fractional
199        } else {
200            signed_fractional
201        };
202
203        Self {
204            number: pre_comma * FP_PRECISION_MULTIPLIER_CONST + final_fractional,
205        }
206    }
207
208    #[inline]
209    pub fn new(value: f32) -> Self {
210        Self {
211            number: (value * FP_PRECISION_MULTIPLIER) as isize,
212        }
213    }
214
215    #[inline]
216    pub fn get(&self) -> f32 {
217        self.number as f32 / FP_PRECISION_MULTIPLIER
218    }
219
220    /// Returns the raw encoded `isize` (the f32 value scaled by
221    /// `FP_PRECISION_MULTIPLIER`). Exposed so external callers can
222    /// round-trip the value through the compact-cache encoding without
223    /// re-multiplying through f32.
224    #[inline]
225    pub fn number(&self) -> isize {
226        self.number
227    }
228
229    #[inline]
230    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
231        let self_val_f32 = self.get();
232        let other_val_f32 = other.get();
233        let interpolated = self_val_f32 + ((other_val_f32 - self_val_f32) * t);
234        Self::new(interpolated)
235    }
236}
237
238impl From<f32> for FloatValue {
239    #[inline]
240    fn from(val: f32) -> Self {
241        Self::new(val)
242    }
243}
244
245/// Enum representing the metric associated with a number (px, pt, em, etc.)
246#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
247#[repr(C)]
248#[derive(Default)]
249pub enum SizeMetric {
250    #[default]
251    Px,
252    Pt,
253    Em,
254    Rem,
255    In,
256    Cm,
257    Mm,
258    Percent,
259    /// Viewport width: 1vw = 1% of viewport width
260    Vw,
261    /// Viewport height: 1vh = 1% of viewport height
262    Vh,
263    /// Viewport minimum: 1vmin = 1% of smaller viewport dimension
264    Vmin,
265    /// Viewport maximum: 1vmax = 1% of larger viewport dimension
266    Vmax,
267}
268
269
270impl fmt::Display for SizeMetric {
271    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
272        use self::SizeMetric::*;
273        match self {
274            Px => write!(f, "px"),
275            Pt => write!(f, "pt"),
276            Em => write!(f, "em"),
277            Rem => write!(f, "rem"),
278            In => write!(f, "in"),
279            Cm => write!(f, "cm"),
280            Mm => write!(f, "mm"),
281            Percent => write!(f, "%"),
282            Vw => write!(f, "vw"),
283            Vh => write!(f, "vh"),
284            Vmin => write!(f, "vmin"),
285            Vmax => write!(f, "vmax"),
286        }
287    }
288}
289
290pub fn parse_float_value(input: &str) -> Result<FloatValue, ParseFloatError> {
291    Ok(FloatValue::new(input.trim().parse::<f32>()?))
292}
293
294#[derive(Clone, PartialEq, Eq)]
295#[repr(C, u8)]
296pub enum PercentageParseError {
297    ValueParseErr(crate::props::basic::error::ParseFloatError),
298    NoPercentSign,
299    InvalidUnit(AzString),
300}
301
302impl_debug_as_display!(PercentageParseError);
303
304impl From<ParseFloatError> for PercentageParseError {
305    fn from(e: ParseFloatError) -> Self {
306        PercentageParseError::ValueParseErr(crate::props::basic::error::ParseFloatError::from(e))
307    }
308}
309
310impl_display! { PercentageParseError, {
311    ValueParseErr(e) => format!("\"{}\"", e),
312    NoPercentSign => format!("No percent sign after number"),
313    InvalidUnit(u) => format!("Error parsing percentage: invalid unit \"{}\"", u.as_str()),
314}}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
317#[repr(C, u8)]
318pub enum PercentageParseErrorOwned {
319    ValueParseErr(crate::props::basic::error::ParseFloatError),
320    NoPercentSign,
321    InvalidUnit(AzString),
322}
323
324impl PercentageParseError {
325    pub fn to_contained(&self) -> PercentageParseErrorOwned {
326        match self {
327            Self::ValueParseErr(e) => PercentageParseErrorOwned::ValueParseErr(*e),
328            Self::NoPercentSign => PercentageParseErrorOwned::NoPercentSign,
329            Self::InvalidUnit(u) => PercentageParseErrorOwned::InvalidUnit(u.clone()),
330        }
331    }
332}
333
334impl PercentageParseErrorOwned {
335    pub fn to_shared(&self) -> PercentageParseError {
336        match self {
337            Self::ValueParseErr(e) => PercentageParseError::ValueParseErr(*e),
338            Self::NoPercentSign => PercentageParseError::NoPercentSign,
339            Self::InvalidUnit(u) => PercentageParseError::InvalidUnit(u.clone()),
340        }
341    }
342}
343
344/// Parse "1.2" or "120%" (similar to parse_pixel_value)
345pub fn parse_percentage_value(input: &str) -> Result<PercentageValue, PercentageParseError> {
346    let input = input.trim();
347
348    if input.is_empty() {
349        return Err(PercentageParseError::ValueParseErr(
350            crate::props::basic::error::ParseFloatError::from("empty string".parse::<f32>().unwrap_err()),
351        ));
352    }
353
354    let mut split_pos = 0;
355    let mut found_numeric = false;
356    for (idx, ch) in input.char_indices() {
357        if ch.is_numeric() || ch == '.' || ch == '-' {
358            split_pos = idx;
359            found_numeric = true;
360        }
361    }
362
363    if !found_numeric {
364        return Err(PercentageParseError::ValueParseErr(
365            crate::props::basic::error::ParseFloatError::from("no numeric value".parse::<f32>().unwrap_err()),
366        ));
367    }
368
369    split_pos += 1;
370
371    let unit = input[split_pos..].trim();
372    let mut number = input[..split_pos]
373        .trim()
374        .parse::<f32>()
375        .map_err(|e| PercentageParseError::ValueParseErr(crate::props::basic::error::ParseFloatError::from(e)))?;
376
377    match unit {
378        "" => {
379            number *= 100.0;
380        } // 0.5 => 50%
381        "%" => {} // 50% => PercentageValue(50.0)
382        other => {
383            return Err(PercentageParseError::InvalidUnit(other.to_string().into()));
384        }
385    }
386
387    Ok(PercentageValue::new(number))
388}
389
390#[cfg(all(test, feature = "parser"))]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn test_parse_float_value() {
396        assert_eq!(parse_float_value("10").unwrap().get(), 10.0);
397        assert_eq!(parse_float_value("2.5").unwrap().get(), 2.5);
398        assert_eq!(parse_float_value("-50.2").unwrap().get(), -50.2);
399        assert_eq!(parse_float_value("  0  ").unwrap().get(), 0.0);
400        assert!(parse_float_value("10a").is_err());
401        assert!(parse_float_value("").is_err());
402    }
403
404    #[test]
405    fn test_parse_percentage_value() {
406        // With percent sign
407        assert_eq!(parse_percentage_value("50%").unwrap().normalized(), 0.5);
408        assert_eq!(parse_percentage_value("120%").unwrap().normalized(), 1.2);
409        assert_eq!(parse_percentage_value("-25%").unwrap().normalized(), -0.25);
410        assert_eq!(
411            parse_percentage_value("  75.5%  ").unwrap().normalized(),
412            0.755
413        );
414
415        // As a ratio
416        assert!((parse_percentage_value("0.5").unwrap().normalized() - 0.5).abs() < 1e-6);
417        assert!((parse_percentage_value("1.2").unwrap().normalized() - 1.2).abs() < 1e-6);
418        assert!((parse_percentage_value("1").unwrap().normalized() - 1.0).abs() < 1e-6);
419
420        // Errors
421        assert!(matches!(
422            parse_percentage_value("50px").err().unwrap(),
423            PercentageParseError::InvalidUnit(_)
424        ));
425        assert!(parse_percentage_value("fifty%").is_err());
426        assert!(parse_percentage_value("").is_err());
427    }
428
429    #[test]
430    fn test_const_new_fractional_single_digit() {
431        // Single digit post_comma (1 decimal place)
432        let val = FloatValue::const_new_fractional(1, 5);
433        assert_eq!(val.get(), 1.5);
434
435        let val = FloatValue::const_new_fractional(0, 5);
436        assert_eq!(val.get(), 0.5);
437
438        let val = FloatValue::const_new_fractional(2, 3);
439        assert_eq!(val.get(), 2.3);
440
441        let val = FloatValue::const_new_fractional(0, 0);
442        assert_eq!(val.get(), 0.0);
443
444        let val = FloatValue::const_new_fractional(10, 9);
445        assert_eq!(val.get(), 10.9);
446    }
447
448    #[test]
449    fn test_const_new_fractional_two_digits() {
450        // Two digits post_comma (2 decimal places)
451        let val = FloatValue::const_new_fractional(0, 83);
452        assert!((val.get() - 0.83).abs() < 0.001);
453
454        let val = FloatValue::const_new_fractional(1, 17);
455        assert!((val.get() - 1.17).abs() < 0.001);
456
457        let val = FloatValue::const_new_fractional(1, 52);
458        assert!((val.get() - 1.52).abs() < 0.001);
459
460        let val = FloatValue::const_new_fractional(0, 33);
461        assert!((val.get() - 0.33).abs() < 0.001);
462
463        let val = FloatValue::const_new_fractional(2, 67);
464        assert!((val.get() - 2.67).abs() < 0.001);
465
466        let val = FloatValue::const_new_fractional(0, 10);
467        assert!((val.get() - 0.10).abs() < 0.001);
468
469        let val = FloatValue::const_new_fractional(0, 99);
470        assert!((val.get() - 0.99).abs() < 0.001);
471    }
472
473    #[test]
474    fn test_const_new_fractional_three_digits() {
475        // Three digits post_comma (3 decimal places)
476        let val = FloatValue::const_new_fractional(1, 523);
477        assert!((val.get() - 1.523).abs() < 0.001);
478
479        let val = FloatValue::const_new_fractional(0, 123);
480        assert!((val.get() - 0.123).abs() < 0.001);
481
482        let val = FloatValue::const_new_fractional(2, 999);
483        assert!((val.get() - 2.999).abs() < 0.001);
484
485        let val = FloatValue::const_new_fractional(0, 100);
486        assert!((val.get() - 0.100).abs() < 0.001);
487
488        let val = FloatValue::const_new_fractional(5, 1);
489        assert!((val.get() - 5.1).abs() < 0.001);
490    }
491
492    #[test]
493    fn test_const_new_fractional_truncation() {
494        // More than 3 digits should be truncated (not rounded)
495
496        // 4 digits: 5234 → 523 → 0.523
497        let val = FloatValue::const_new_fractional(0, 5234);
498        assert!((val.get() - 0.523).abs() < 0.001);
499
500        // 5 digits: 12345 → 123 → 0.123
501        let val = FloatValue::const_new_fractional(1, 12345);
502        assert!((val.get() - 1.123).abs() < 0.001);
503
504        // 6 digits: 123456 → 123 → 1.123
505        let val = FloatValue::const_new_fractional(1, 123456);
506        assert!((val.get() - 1.123).abs() < 0.001);
507
508        // 7 digits: 9876543 → 987 → 0.987
509        let val = FloatValue::const_new_fractional(0, 9876543);
510        assert!((val.get() - 0.987).abs() < 0.001);
511
512        // 10 digits
513        let val = FloatValue::const_new_fractional(2, 1234567890);
514        assert!((val.get() - 2.123).abs() < 0.001);
515    }
516
517    #[test]
518    fn test_const_new_fractional_negative() {
519        // Negative pre_comma values
520        let val = FloatValue::const_new_fractional(-1, 5);
521        assert_eq!(val.get(), -1.5);
522
523        let val = FloatValue::const_new_fractional(0, 83);
524        assert!((val.get() - 0.83).abs() < 0.001);
525
526        let val = FloatValue::const_new_fractional(-2, 123);
527        assert!((val.get() - -2.123).abs() < 0.001);
528
529        // Negative post_comma (unusual case - treated as negative fractional part)
530        let val = FloatValue::const_new_fractional(1, -5);
531        assert_eq!(val.get(), 0.5); // 1 + (-0.5) = 0.5
532
533        let val = FloatValue::const_new_fractional(0, -50);
534        assert!((val.get() - -0.5).abs() < 0.001); // 0 + (-0.5) = -0.5
535    }
536
537    #[test]
538    fn test_const_new_fractional_edge_cases() {
539        // Zero
540        let val = FloatValue::const_new_fractional(0, 0);
541        assert_eq!(val.get(), 0.0);
542
543        // Large integer part
544        let val = FloatValue::const_new_fractional(100, 5);
545        assert_eq!(val.get(), 100.5);
546
547        let val = FloatValue::const_new_fractional(1000, 99);
548        assert!((val.get() - 1000.99).abs() < 0.001);
549
550        // Maximum precision (3 digits)
551        let val = FloatValue::const_new_fractional(0, 999);
552        assert!((val.get() - 0.999).abs() < 0.001);
553
554        // Small fractional values
555        let val = FloatValue::const_new_fractional(1, 1);
556        assert!((val.get() - 1.1).abs() < 0.001);
557
558        let val = FloatValue::const_new_fractional(1, 10);
559        assert!((val.get() - 1.10).abs() < 0.001);
560    }
561
562    #[test]
563    fn test_const_new_fractional_ua_css_values() {
564        // Test actual values used in ua_css.rs
565
566        // H1: 2em
567        let val = FloatValue::const_new_fractional(2, 0);
568        assert_eq!(val.get(), 2.0);
569
570        // H2: 1.5em
571        let val = FloatValue::const_new_fractional(1, 5);
572        assert_eq!(val.get(), 1.5);
573
574        // H3: 1.17em
575        let val = FloatValue::const_new_fractional(1, 17);
576        assert!((val.get() - 1.17).abs() < 0.001);
577
578        // H4: 1em
579        let val = FloatValue::const_new_fractional(1, 0);
580        assert_eq!(val.get(), 1.0);
581
582        // H5: 0.83em
583        let val = FloatValue::const_new_fractional(0, 83);
584        assert!((val.get() - 0.83).abs() < 0.001);
585
586        // H6: 0.67em
587        let val = FloatValue::const_new_fractional(0, 67);
588        assert!((val.get() - 0.67).abs() < 0.001);
589
590        // Margins: 0.67em
591        let val = FloatValue::const_new_fractional(0, 67);
592        assert!((val.get() - 0.67).abs() < 0.001);
593
594        // Margins: 0.83em
595        let val = FloatValue::const_new_fractional(0, 83);
596        assert!((val.get() - 0.83).abs() < 0.001);
597
598        // Margins: 1.33em
599        let val = FloatValue::const_new_fractional(1, 33);
600        assert!((val.get() - 1.33).abs() < 0.001);
601
602        // Margins: 1.67em
603        let val = FloatValue::const_new_fractional(1, 67);
604        assert!((val.get() - 1.67).abs() < 0.001);
605
606        // Margins: 2.33em
607        let val = FloatValue::const_new_fractional(2, 33);
608        assert!((val.get() - 2.33).abs() < 0.001);
609    }
610
611    #[test]
612    fn test_const_new_fractional_consistency() {
613        // Verify consistency between const_new_fractional and new()
614
615        let const_val = FloatValue::const_new_fractional(1, 5);
616        let runtime_val = FloatValue::new(1.5);
617        assert_eq!(const_val.get(), runtime_val.get());
618
619        let const_val = FloatValue::const_new_fractional(0, 83);
620        let runtime_val = FloatValue::new(0.83);
621        assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
622
623        let const_val = FloatValue::const_new_fractional(1, 523);
624        let runtime_val = FloatValue::new(1.523);
625        assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
626
627        let const_val = FloatValue::const_new_fractional(2, 99);
628        let runtime_val = FloatValue::new(2.99);
629        assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
630    }
631}