Skip to main content

azul_css/props/basic/
angle.rs

1//! CSS property types for angles (degrees, radians, etc.).
2
3use alloc::string::{String, ToString};
4use core::{fmt, num::ParseFloatError};
5use crate::corety::AzString;
6
7use crate::props::basic::error::ParseFloatErrorWithInput;
8
9use crate::props::{basic::length::FloatValue, formatter::PrintAsCssValue};
10
11/// Enum representing the metric associated with an angle (deg, rad, etc.)
12#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
13#[repr(C)]
14#[derive(Default)]
15pub enum AngleMetric {
16    #[default]
17    Degree,
18    Radians,
19    Grad,
20    Turn,
21    Percent,
22}
23
24
25impl fmt::Display for AngleMetric {
26    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
27        use self::AngleMetric::*;
28        match self {
29            Degree => write!(f, "deg"),
30            Radians => write!(f, "rad"),
31            Grad => write!(f, "grad"),
32            Turn => write!(f, "turn"),
33            Percent => write!(f, "%"),
34        }
35    }
36}
37
38/// FloatValue, but associated with a certain metric (i.e. deg, rad, etc.)
39#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
40#[repr(C)]
41pub struct AngleValue {
42    pub metric: AngleMetric,
43    pub number: FloatValue,
44}
45
46impl_option!(
47    AngleValue,
48    OptionAngleValue,
49    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
50);
51
52impl fmt::Debug for AngleValue {
53    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54        write!(f, "{}", self)
55    }
56}
57
58impl fmt::Display for AngleValue {
59    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60        write!(f, "{}{}", self.number, self.metric)
61    }
62}
63
64impl PrintAsCssValue for AngleValue {
65    fn print_as_css_value(&self) -> String {
66        format!("{}", self)
67    }
68}
69
70impl AngleValue {
71    /// Returns an angle of zero degrees.
72    #[inline]
73    pub const fn zero() -> Self {
74        const ZERO_DEG: AngleValue = AngleValue::const_deg(0);
75        ZERO_DEG
76    }
77
78    /// Creates a const angle value in degrees from an integer.
79    #[inline]
80    pub const fn const_deg(value: isize) -> Self {
81        Self::const_from_metric(AngleMetric::Degree, value)
82    }
83
84    /// Creates a const angle value in radians from an integer.
85    #[inline]
86    pub const fn const_rad(value: isize) -> Self {
87        Self::const_from_metric(AngleMetric::Radians, value)
88    }
89
90    /// Creates a const angle value in gradians from an integer.
91    #[inline]
92    pub const fn const_grad(value: isize) -> Self {
93        Self::const_from_metric(AngleMetric::Grad, value)
94    }
95
96    /// Creates a const angle value in turns from an integer.
97    #[inline]
98    pub const fn const_turn(value: isize) -> Self {
99        Self::const_from_metric(AngleMetric::Turn, value)
100    }
101
102    /// Creates a const angle value in percent from an integer.
103    #[inline]
104    pub const fn const_percent(value: isize) -> Self {
105        Self::const_from_metric(AngleMetric::Percent, value)
106    }
107
108    /// Creates a const angle value with the given metric from an integer.
109    #[inline]
110    pub const fn const_from_metric(metric: AngleMetric, value: isize) -> Self {
111        Self {
112            metric,
113            number: FloatValue::const_new(value),
114        }
115    }
116
117    /// Creates a const angle value with the given metric from a fractional number.
118    ///
119    /// # Arguments
120    /// * `metric` - The angle metric (Degree, Radians, etc.)
121    /// * `pre_comma` - The integer part (e.g., 45 for 45.5deg)
122    /// * `post_comma` - The fractional part as digits (e.g., 5 for 0.5deg)
123    #[inline]
124    pub const fn const_from_metric_fractional(metric: AngleMetric, pre_comma: isize, post_comma: isize) -> Self {
125        Self {
126            metric,
127            number: FloatValue::const_new_fractional(pre_comma, post_comma),
128        }
129    }
130
131    /// Creates an angle value in degrees.
132    #[inline]
133    pub fn deg(value: f32) -> Self {
134        Self::from_metric(AngleMetric::Degree, value)
135    }
136
137    /// Creates an angle value in radians.
138    #[inline]
139    pub fn rad(value: f32) -> Self {
140        Self::from_metric(AngleMetric::Radians, value)
141    }
142
143    /// Creates an angle value in gradians.
144    #[inline]
145    pub fn grad(value: f32) -> Self {
146        Self::from_metric(AngleMetric::Grad, value)
147    }
148
149    /// Creates an angle value in turns.
150    #[inline]
151    pub fn turn(value: f32) -> Self {
152        Self::from_metric(AngleMetric::Turn, value)
153    }
154
155    /// Creates an angle value in percent.
156    #[inline]
157    pub fn percent(value: f32) -> Self {
158        Self::from_metric(AngleMetric::Percent, value)
159    }
160
161    /// Creates an angle value with the given metric.
162    #[inline]
163    pub fn from_metric(metric: AngleMetric, value: f32) -> Self {
164        Self {
165            metric,
166            number: FloatValue::new(value),
167        }
168    }
169
170    /// Convert to degrees, normalized to [0, 360) range.
171    /// Note: 360.0 becomes 0.0 due to modulo operation.
172    /// For conic gradients where 360.0 is meaningful, use `to_degrees_raw()`.
173    #[inline]
174    pub fn to_degrees(&self) -> f32 {
175        let mut val = self.to_degrees_raw() % 360.0;
176        if val < 0.0 {
177            val += 360.0;
178        }
179        val
180    }
181
182    /// Convert to degrees without normalization (raw value).
183    /// Use this for conic gradients where 360.0 is a meaningful distinct value from 0.0.
184    #[inline]
185    pub fn to_degrees_raw(&self) -> f32 {
186        match self.metric {
187            AngleMetric::Degree => self.number.get(),
188            AngleMetric::Grad => self.number.get() / 400.0 * 360.0,
189            AngleMetric::Radians => self.number.get() * 180.0 / core::f32::consts::PI,
190            AngleMetric::Turn => self.number.get() * 360.0,
191            AngleMetric::Percent => self.number.get() / 100.0 * 360.0,
192        }
193    }
194}
195
196// -- Parser
197
198/// Error returned when parsing a CSS angle value from a string.
199#[derive(Clone, PartialEq)]
200pub enum CssAngleValueParseError<'a> {
201    EmptyString,
202    NoValueGiven(&'a str, AngleMetric),
203    ValueParseErr(ParseFloatError, &'a str),
204    InvalidAngle(&'a str),
205}
206
207impl_debug_as_display!(CssAngleValueParseError<'a>);
208impl_display! { CssAngleValueParseError<'a>, {
209    EmptyString => format!("Missing [rad / deg / turn / %] value"),
210    NoValueGiven(input, metric) => format!("Expected floating-point angle value, got: \"{}{}\"", input, metric),
211    ValueParseErr(err, number_str) => format!("Could not parse \"{}\" as floating-point value: \"{}\"", number_str, err),
212    InvalidAngle(s) => format!("Invalid angle value: \"{}\"", s),
213}}
214
215/// Wrapper for NoValueGiven error in angle parsing.
216#[derive(Debug, Clone, PartialEq)]
217#[repr(C)]
218pub struct AngleNoValueGivenError {
219    pub value: AzString,
220    pub metric: AngleMetric,
221}
222
223/// Owned version of [`CssAngleValueParseError`] for FFI and storage.
224#[derive(Debug, Clone, PartialEq)]
225#[repr(C, u8)]
226pub enum CssAngleValueParseErrorOwned {
227    EmptyString,
228    NoValueGiven(AngleNoValueGivenError),
229    ValueParseErr(ParseFloatErrorWithInput),
230    InvalidAngle(AzString),
231}
232
233impl<'a> CssAngleValueParseError<'a> {
234    pub fn to_contained(&self) -> CssAngleValueParseErrorOwned {
235        match self {
236            CssAngleValueParseError::EmptyString => CssAngleValueParseErrorOwned::EmptyString,
237            CssAngleValueParseError::NoValueGiven(s, metric) => {
238                CssAngleValueParseErrorOwned::NoValueGiven(AngleNoValueGivenError { value: s.to_string().into(), metric: *metric })
239            }
240            CssAngleValueParseError::ValueParseErr(err, s) => {
241                CssAngleValueParseErrorOwned::ValueParseErr(ParseFloatErrorWithInput { error: err.clone().into(), input: s.to_string().into() })
242            }
243            CssAngleValueParseError::InvalidAngle(s) => {
244                CssAngleValueParseErrorOwned::InvalidAngle(s.to_string().into())
245            }
246        }
247    }
248}
249
250impl CssAngleValueParseErrorOwned {
251    pub fn to_shared<'a>(&'a self) -> CssAngleValueParseError<'a> {
252        match self {
253            CssAngleValueParseErrorOwned::EmptyString => CssAngleValueParseError::EmptyString,
254            CssAngleValueParseErrorOwned::NoValueGiven(e) => {
255                CssAngleValueParseError::NoValueGiven(e.value.as_str(), e.metric)
256            }
257            CssAngleValueParseErrorOwned::ValueParseErr(e) => {
258                CssAngleValueParseError::ValueParseErr(e.error.to_std(), e.input.as_str())
259            }
260            CssAngleValueParseErrorOwned::InvalidAngle(s) => {
261                CssAngleValueParseError::InvalidAngle(s.as_str())
262            }
263        }
264    }
265}
266
267/// Parse a CSS angle value string (e.g. `"90deg"`, `"1.57rad"`, `"0.5turn"`, `"50%"`).
268/// A bare number without a unit suffix is interpreted as degrees.
269#[cfg(feature = "parser")]
270pub fn parse_angle_value<'a>(input: &'a str) -> Result<AngleValue, CssAngleValueParseError<'a>> {
271    let input = input.trim();
272
273    if input.is_empty() {
274        return Err(CssAngleValueParseError::EmptyString);
275    }
276
277    let match_values = &[
278        ("deg", AngleMetric::Degree),
279        ("turn", AngleMetric::Turn),
280        ("grad", AngleMetric::Grad),
281        ("rad", AngleMetric::Radians),
282        ("%", AngleMetric::Percent),
283    ];
284
285    for (match_val, metric) in match_values {
286        if input.ends_with(match_val) {
287            let value = &input[..input.len() - match_val.len()];
288            let value = value.trim();
289            if value.is_empty() {
290                return Err(CssAngleValueParseError::NoValueGiven(input, *metric));
291            }
292            match value.parse::<f32>() {
293                Ok(o) => return Ok(AngleValue::from_metric(*metric, o)),
294                Err(e) => return Err(CssAngleValueParseError::ValueParseErr(e, value)),
295            }
296        }
297    }
298
299    match input.parse::<f32>() {
300        Ok(o) => Ok(AngleValue::from_metric(AngleMetric::Degree, o)), // bare number is degrees
301        Err(_) => Err(CssAngleValueParseError::InvalidAngle(input)),
302    }
303}
304
305#[cfg(all(test, feature = "parser"))]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn test_parse_angle_value_deg() {
311        assert_eq!(parse_angle_value("90deg").unwrap(), AngleValue::deg(90.0));
312        assert_eq!(
313            parse_angle_value("-45.5deg").unwrap(),
314            AngleValue::deg(-45.5)
315        );
316        // Bare number defaults to degrees
317        assert_eq!(parse_angle_value("180").unwrap(), AngleValue::deg(180.0));
318    }
319
320    #[test]
321    fn test_parse_angle_value_rad() {
322        assert_eq!(parse_angle_value("1.57rad").unwrap(), AngleValue::rad(1.57));
323        assert_eq!(
324            parse_angle_value(" -3.14rad ").unwrap(),
325            AngleValue::rad(-3.14)
326        );
327    }
328
329    #[test]
330    fn test_parse_angle_value_grad() {
331        assert_eq!(
332            parse_angle_value("100grad").unwrap(),
333            AngleValue::grad(100.0)
334        );
335        assert_eq!(
336            parse_angle_value("400grad").unwrap(),
337            AngleValue::grad(400.0)
338        );
339    }
340
341    #[test]
342    fn test_parse_angle_value_turn() {
343        assert_eq!(
344            parse_angle_value("0.25turn").unwrap(),
345            AngleValue::turn(0.25)
346        );
347        assert_eq!(parse_angle_value("1turn").unwrap(), AngleValue::turn(1.0));
348    }
349
350    #[test]
351    fn test_parse_angle_value_percent() {
352        assert_eq!(parse_angle_value("50%").unwrap(), AngleValue::percent(50.0));
353    }
354
355    #[test]
356    fn test_parse_angle_value_errors() {
357        assert!(parse_angle_value("").is_err());
358        assert!(parse_angle_value("deg").is_err());
359        assert!(parse_angle_value("90 degs").is_err());
360        assert!(parse_angle_value("ninety-deg").is_err());
361        assert!(parse_angle_value("1.57 rads").is_err());
362    }
363
364    #[test]
365    fn test_to_degrees_conversion() {
366        assert_eq!(AngleValue::deg(90.0).to_degrees(), 90.0);
367        // Use 0.1 tolerance due to FloatValue fixed-point precision (multiplier = 1000.0)
368        assert!((AngleValue::rad(core::f32::consts::PI).to_degrees() - 180.0).abs() < 0.1);
369        assert_eq!(AngleValue::grad(100.0).to_degrees(), 90.0);
370        assert_eq!(AngleValue::turn(0.5).to_degrees(), 180.0);
371        assert_eq!(AngleValue::deg(-90.0).to_degrees(), 270.0);
372        assert_eq!(AngleValue::deg(450.0).to_degrees(), 90.0);
373    }
374}