Skip to main content

azul_css/props/basic/
length.rs

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