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