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        // Note: We limit to values that fit in 32-bit isize for WASM compatibility
154        let (normalized_post, divisor) = if abs_post < 10 {
155            // 1 digit: 5 → 0.5
156            (abs_post, 10)
157        } else if abs_post < 100 {
158            // 2 digits: 83 → 0.83
159            (abs_post, 100)
160        } else if abs_post < 1000 {
161            // 3 digits: 523 → 0.523
162            (abs_post, 1000)
163        } else if abs_post < 10000 {
164            // 4+ digits: take first 3 (e.g., 5234 → 523 → 0.523)
165            (abs_post / 10, 1000)
166        } else if abs_post < 100000 {
167            (abs_post / 100, 1000)
168        } else if abs_post < 1000000 {
169            (abs_post / 1000, 1000)
170        } else if abs_post < 10000000 {
171            (abs_post / 10000, 1000)
172        } else if abs_post < 100000000 {
173            (abs_post / 100000, 1000)
174        } else if abs_post < 1000000000 {
175            (abs_post / 1000000, 1000)
176        } else {
177            // For very large values (>= 1 billion), cap at reasonable precision
178            // This ensures compatibility with 32-bit isize on WASM
179            (abs_post / 10000000, 1000)
180        };
181
182        // Calculate fractional part
183        let fractional_part = normalized_post * (FP_PRECISION_MULTIPLIER_CONST / divisor);
184
185        // Apply sign: if post_comma is negative, negate the fractional part
186        let signed_fractional = if post_comma < 0 {
187            -fractional_part
188        } else {
189            fractional_part
190        };
191
192        // For negative pre_comma, the fractional part should also be negative
193        // E.g., -1.5 = -1 + (-0.5), not -1 + 0.5
194        let final_fractional = if pre_comma < 0 && post_comma >= 0 {
195            -signed_fractional
196        } else {
197            signed_fractional
198        };
199
200        Self {
201            number: pre_comma * FP_PRECISION_MULTIPLIER_CONST + final_fractional,
202        }
203    }
204
205    #[inline]
206    pub fn new(value: f32) -> Self {
207        Self {
208            number: (value * FP_PRECISION_MULTIPLIER) as isize,
209        }
210    }
211
212    #[inline]
213    pub fn get(&self) -> f32 {
214        self.number as f32 / FP_PRECISION_MULTIPLIER
215    }
216
217    #[inline]
218    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
219        let self_val_f32 = self.get();
220        let other_val_f32 = other.get();
221        let interpolated = self_val_f32 + ((other_val_f32 - self_val_f32) * t);
222        Self::new(interpolated)
223    }
224}
225
226impl From<f32> for FloatValue {
227    #[inline]
228    fn from(val: f32) -> Self {
229        Self::new(val)
230    }
231}
232
233/// Enum representing the metric associated with a number (px, pt, em, etc.)
234#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
235#[repr(C)]
236pub enum SizeMetric {
237    Px,
238    Pt,
239    Em,
240    Rem,
241    In,
242    Cm,
243    Mm,
244    Percent,
245    /// Viewport width: 1vw = 1% of viewport width
246    Vw,
247    /// Viewport height: 1vh = 1% of viewport height
248    Vh,
249    /// Viewport minimum: 1vmin = 1% of smaller viewport dimension
250    Vmin,
251    /// Viewport maximum: 1vmax = 1% of larger viewport dimension
252    Vmax,
253}
254
255impl Default for SizeMetric {
256    fn default() -> Self {
257        SizeMetric::Px
258    }
259}
260
261impl fmt::Display for SizeMetric {
262    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
263        use self::SizeMetric::*;
264        match self {
265            Px => write!(f, "px"),
266            Pt => write!(f, "pt"),
267            Em => write!(f, "em"),
268            Rem => write!(f, "rem"),
269            In => write!(f, "in"),
270            Cm => write!(f, "cm"),
271            Mm => write!(f, "mm"),
272            Percent => write!(f, "%"),
273            Vw => write!(f, "vw"),
274            Vh => write!(f, "vh"),
275            Vmin => write!(f, "vmin"),
276            Vmax => write!(f, "vmax"),
277        }
278    }
279}
280
281pub fn parse_float_value(input: &str) -> Result<FloatValue, ParseFloatError> {
282    Ok(FloatValue::new(input.trim().parse::<f32>()?))
283}
284
285#[derive(Clone, PartialEq, Eq)]
286pub enum PercentageParseError {
287    ValueParseErr(ParseFloatError),
288    NoPercentSign,
289    InvalidUnit(AzString),
290}
291
292impl_debug_as_display!(PercentageParseError);
293impl_from!(ParseFloatError, PercentageParseError::ValueParseErr);
294
295impl_display! { PercentageParseError, {
296    ValueParseErr(e) => format!("\"{}\"", e),
297    NoPercentSign => format!("No percent sign after number"),
298    InvalidUnit(u) => format!("Error parsing percentage: invalid unit \"{}\"", u.as_str()),
299}}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
302pub enum PercentageParseErrorOwned {
303    ValueParseErr(ParseFloatError),
304    NoPercentSign,
305    InvalidUnit(String),
306}
307
308impl PercentageParseError {
309    pub fn to_contained(&self) -> PercentageParseErrorOwned {
310        match self {
311            Self::ValueParseErr(e) => PercentageParseErrorOwned::ValueParseErr(e.clone()),
312            Self::NoPercentSign => PercentageParseErrorOwned::NoPercentSign,
313            Self::InvalidUnit(u) => PercentageParseErrorOwned::InvalidUnit(u.as_str().to_string()),
314        }
315    }
316}
317
318impl PercentageParseErrorOwned {
319    pub fn to_shared(&self) -> PercentageParseError {
320        match self {
321            Self::ValueParseErr(e) => PercentageParseError::ValueParseErr(e.clone()),
322            Self::NoPercentSign => PercentageParseError::NoPercentSign,
323            Self::InvalidUnit(u) => PercentageParseError::InvalidUnit(u.clone().into()),
324        }
325    }
326}
327
328/// Parse "1.2" or "120%" (similar to parse_pixel_value)
329pub fn parse_percentage_value(input: &str) -> Result<PercentageValue, PercentageParseError> {
330    let input = input.trim();
331
332    if input.is_empty() {
333        return Err(PercentageParseError::ValueParseErr(
334            "empty string".parse::<f32>().unwrap_err(),
335        ));
336    }
337
338    let mut split_pos = 0;
339    let mut found_numeric = false;
340    for (idx, ch) in input.char_indices() {
341        if ch.is_numeric() || ch == '.' || ch == '-' {
342            split_pos = idx;
343            found_numeric = true;
344        }
345    }
346
347    if !found_numeric {
348        return Err(PercentageParseError::ValueParseErr(
349            "no numeric value".parse::<f32>().unwrap_err(),
350        ));
351    }
352
353    split_pos += 1;
354
355    let unit = input[split_pos..].trim();
356    let mut number = input[..split_pos]
357        .trim()
358        .parse::<f32>()
359        .map_err(|e| PercentageParseError::ValueParseErr(e))?;
360
361    match unit {
362        "" => {
363            number *= 100.0;
364        } // 0.5 => 50%
365        "%" => {} // 50% => PercentageValue(50.0)
366        other => {
367            return Err(PercentageParseError::InvalidUnit(other.to_string().into()));
368        }
369    }
370
371    Ok(PercentageValue::new(number))
372}
373
374#[cfg(all(test, feature = "parser"))]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn test_parse_float_value() {
380        assert_eq!(parse_float_value("10").unwrap().get(), 10.0);
381        assert_eq!(parse_float_value("2.5").unwrap().get(), 2.5);
382        assert_eq!(parse_float_value("-50.2").unwrap().get(), -50.2);
383        assert_eq!(parse_float_value("  0  ").unwrap().get(), 0.0);
384        assert!(parse_float_value("10a").is_err());
385        assert!(parse_float_value("").is_err());
386    }
387
388    #[test]
389    fn test_parse_percentage_value() {
390        // With percent sign
391        assert_eq!(parse_percentage_value("50%").unwrap().normalized(), 0.5);
392        assert_eq!(parse_percentage_value("120%").unwrap().normalized(), 1.2);
393        assert_eq!(parse_percentage_value("-25%").unwrap().normalized(), -0.25);
394        assert_eq!(
395            parse_percentage_value("  75.5%  ").unwrap().normalized(),
396            0.755
397        );
398
399        // As a ratio
400        assert!((parse_percentage_value("0.5").unwrap().normalized() - 0.5).abs() < 1e-6);
401        assert!((parse_percentage_value("1.2").unwrap().normalized() - 1.2).abs() < 1e-6);
402        assert!((parse_percentage_value("1").unwrap().normalized() - 1.0).abs() < 1e-6);
403
404        // Errors
405        assert!(matches!(
406            parse_percentage_value("50px").err().unwrap(),
407            PercentageParseError::InvalidUnit(_)
408        ));
409        assert!(parse_percentage_value("fifty%").is_err());
410        assert!(parse_percentage_value("").is_err());
411    }
412
413    #[test]
414    fn test_const_new_fractional_single_digit() {
415        // Single digit post_comma (1 decimal place)
416        let val = FloatValue::const_new_fractional(1, 5);
417        assert_eq!(val.get(), 1.5);
418
419        let val = FloatValue::const_new_fractional(0, 5);
420        assert_eq!(val.get(), 0.5);
421
422        let val = FloatValue::const_new_fractional(2, 3);
423        assert_eq!(val.get(), 2.3);
424
425        let val = FloatValue::const_new_fractional(0, 0);
426        assert_eq!(val.get(), 0.0);
427
428        let val = FloatValue::const_new_fractional(10, 9);
429        assert_eq!(val.get(), 10.9);
430    }
431
432    #[test]
433    fn test_const_new_fractional_two_digits() {
434        // Two digits post_comma (2 decimal places)
435        let val = FloatValue::const_new_fractional(0, 83);
436        assert!((val.get() - 0.83).abs() < 0.001);
437
438        let val = FloatValue::const_new_fractional(1, 17);
439        assert!((val.get() - 1.17).abs() < 0.001);
440
441        let val = FloatValue::const_new_fractional(1, 52);
442        assert!((val.get() - 1.52).abs() < 0.001);
443
444        let val = FloatValue::const_new_fractional(0, 33);
445        assert!((val.get() - 0.33).abs() < 0.001);
446
447        let val = FloatValue::const_new_fractional(2, 67);
448        assert!((val.get() - 2.67).abs() < 0.001);
449
450        let val = FloatValue::const_new_fractional(0, 10);
451        assert!((val.get() - 0.10).abs() < 0.001);
452
453        let val = FloatValue::const_new_fractional(0, 99);
454        assert!((val.get() - 0.99).abs() < 0.001);
455    }
456
457    #[test]
458    fn test_const_new_fractional_three_digits() {
459        // Three digits post_comma (3 decimal places)
460        let val = FloatValue::const_new_fractional(1, 523);
461        assert!((val.get() - 1.523).abs() < 0.001);
462
463        let val = FloatValue::const_new_fractional(0, 123);
464        assert!((val.get() - 0.123).abs() < 0.001);
465
466        let val = FloatValue::const_new_fractional(2, 999);
467        assert!((val.get() - 2.999).abs() < 0.001);
468
469        let val = FloatValue::const_new_fractional(0, 100);
470        assert!((val.get() - 0.100).abs() < 0.001);
471
472        let val = FloatValue::const_new_fractional(5, 1);
473        assert!((val.get() - 5.1).abs() < 0.001);
474    }
475
476    #[test]
477    fn test_const_new_fractional_truncation() {
478        // More than 3 digits should be truncated (not rounded)
479
480        // 4 digits: 5234 → 523 → 0.523
481        let val = FloatValue::const_new_fractional(0, 5234);
482        assert!((val.get() - 0.523).abs() < 0.001);
483
484        // 5 digits: 12345 → 123 → 0.123
485        let val = FloatValue::const_new_fractional(1, 12345);
486        assert!((val.get() - 1.123).abs() < 0.001);
487
488        // 6 digits: 123456 → 123 → 1.123
489        let val = FloatValue::const_new_fractional(1, 123456);
490        assert!((val.get() - 1.123).abs() < 0.001);
491
492        // 7 digits: 9876543 → 987 → 0.987
493        let val = FloatValue::const_new_fractional(0, 9876543);
494        assert!((val.get() - 0.987).abs() < 0.001);
495
496        // 10 digits
497        let val = FloatValue::const_new_fractional(2, 1234567890);
498        assert!((val.get() - 2.123).abs() < 0.001);
499    }
500
501    #[test]
502    fn test_const_new_fractional_negative() {
503        // Negative pre_comma values
504        let val = FloatValue::const_new_fractional(-1, 5);
505        assert_eq!(val.get(), -1.5);
506
507        let val = FloatValue::const_new_fractional(0, 83);
508        assert!((val.get() - 0.83).abs() < 0.001);
509
510        let val = FloatValue::const_new_fractional(-2, 123);
511        assert!((val.get() - -2.123).abs() < 0.001);
512
513        // Negative post_comma (unusual case - treated as negative fractional part)
514        let val = FloatValue::const_new_fractional(1, -5);
515        assert_eq!(val.get(), 0.5); // 1 + (-0.5) = 0.5
516
517        let val = FloatValue::const_new_fractional(0, -50);
518        assert!((val.get() - -0.5).abs() < 0.001); // 0 + (-0.5) = -0.5
519    }
520
521    #[test]
522    fn test_const_new_fractional_edge_cases() {
523        // Zero
524        let val = FloatValue::const_new_fractional(0, 0);
525        assert_eq!(val.get(), 0.0);
526
527        // Large integer part
528        let val = FloatValue::const_new_fractional(100, 5);
529        assert_eq!(val.get(), 100.5);
530
531        let val = FloatValue::const_new_fractional(1000, 99);
532        assert!((val.get() - 1000.99).abs() < 0.001);
533
534        // Maximum precision (3 digits)
535        let val = FloatValue::const_new_fractional(0, 999);
536        assert!((val.get() - 0.999).abs() < 0.001);
537
538        // Small fractional values
539        let val = FloatValue::const_new_fractional(1, 1);
540        assert!((val.get() - 1.1).abs() < 0.001);
541
542        let val = FloatValue::const_new_fractional(1, 10);
543        assert!((val.get() - 1.10).abs() < 0.001);
544    }
545
546    #[test]
547    fn test_const_new_fractional_ua_css_values() {
548        // Test actual values used in ua_css.rs
549
550        // H1: 2em
551        let val = FloatValue::const_new_fractional(2, 0);
552        assert_eq!(val.get(), 2.0);
553
554        // H2: 1.5em
555        let val = FloatValue::const_new_fractional(1, 5);
556        assert_eq!(val.get(), 1.5);
557
558        // H3: 1.17em
559        let val = FloatValue::const_new_fractional(1, 17);
560        assert!((val.get() - 1.17).abs() < 0.001);
561
562        // H4: 1em
563        let val = FloatValue::const_new_fractional(1, 0);
564        assert_eq!(val.get(), 1.0);
565
566        // H5: 0.83em
567        let val = FloatValue::const_new_fractional(0, 83);
568        assert!((val.get() - 0.83).abs() < 0.001);
569
570        // H6: 0.67em
571        let val = FloatValue::const_new_fractional(0, 67);
572        assert!((val.get() - 0.67).abs() < 0.001);
573
574        // Margins: 0.67em
575        let val = FloatValue::const_new_fractional(0, 67);
576        assert!((val.get() - 0.67).abs() < 0.001);
577
578        // Margins: 0.83em
579        let val = FloatValue::const_new_fractional(0, 83);
580        assert!((val.get() - 0.83).abs() < 0.001);
581
582        // Margins: 1.33em
583        let val = FloatValue::const_new_fractional(1, 33);
584        assert!((val.get() - 1.33).abs() < 0.001);
585
586        // Margins: 1.67em
587        let val = FloatValue::const_new_fractional(1, 67);
588        assert!((val.get() - 1.67).abs() < 0.001);
589
590        // Margins: 2.33em
591        let val = FloatValue::const_new_fractional(2, 33);
592        assert!((val.get() - 2.33).abs() < 0.001);
593    }
594
595    #[test]
596    fn test_const_new_fractional_consistency() {
597        // Verify consistency between const_new_fractional and new()
598
599        let const_val = FloatValue::const_new_fractional(1, 5);
600        let runtime_val = FloatValue::new(1.5);
601        assert_eq!(const_val.get(), runtime_val.get());
602
603        let const_val = FloatValue::const_new_fractional(0, 83);
604        let runtime_val = FloatValue::new(0.83);
605        assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
606
607        let const_val = FloatValue::const_new_fractional(1, 523);
608        let runtime_val = FloatValue::new(1.523);
609        assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
610
611        let const_val = FloatValue::const_new_fractional(2, 99);
612        let runtime_val = FloatValue::new(2.99);
613        assert!((const_val.get() - runtime_val.get()).abs() < 0.001);
614    }
615}