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};
5
6use crate::props::{basic::length::FloatValue, formatter::PrintAsCssValue};
7
8/// Enum representing the metric associated with an angle (deg, rad, etc.)
9#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
10#[repr(C)]
11pub enum AngleMetric {
12    Degree,
13    Radians,
14    Grad,
15    Turn,
16    Percent,
17}
18
19impl Default for AngleMetric {
20    fn default() -> AngleMetric {
21        AngleMetric::Degree
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    #[inline]
72    pub const fn zero() -> Self {
73        const ZERO_DEG: AngleValue = AngleValue::const_deg(0);
74        ZERO_DEG
75    }
76
77    #[inline]
78    pub const fn const_deg(value: isize) -> Self {
79        Self::const_from_metric(AngleMetric::Degree, value)
80    }
81
82    #[inline]
83    pub const fn const_rad(value: isize) -> Self {
84        Self::const_from_metric(AngleMetric::Radians, value)
85    }
86
87    #[inline]
88    pub const fn const_grad(value: isize) -> Self {
89        Self::const_from_metric(AngleMetric::Grad, value)
90    }
91
92    #[inline]
93    pub const fn const_turn(value: isize) -> Self {
94        Self::const_from_metric(AngleMetric::Turn, value)
95    }
96
97    #[inline]
98    pub fn const_percent(value: isize) -> Self {
99        Self::const_from_metric(AngleMetric::Percent, value)
100    }
101
102    #[inline]
103    pub const fn const_from_metric(metric: AngleMetric, value: isize) -> Self {
104        Self {
105            metric,
106            number: FloatValue::const_new(value),
107        }
108    }
109
110    #[inline]
111    pub fn deg(value: f32) -> Self {
112        Self::from_metric(AngleMetric::Degree, value)
113    }
114
115    #[inline]
116    pub fn rad(value: f32) -> Self {
117        Self::from_metric(AngleMetric::Radians, value)
118    }
119
120    #[inline]
121    pub fn grad(value: f32) -> Self {
122        Self::from_metric(AngleMetric::Grad, value)
123    }
124
125    #[inline]
126    pub fn turn(value: f32) -> Self {
127        Self::from_metric(AngleMetric::Turn, value)
128    }
129
130    #[inline]
131    pub fn percent(value: f32) -> Self {
132        Self::from_metric(AngleMetric::Percent, value)
133    }
134
135    #[inline]
136    pub fn from_metric(metric: AngleMetric, value: f32) -> Self {
137        Self {
138            metric,
139            number: FloatValue::new(value),
140        }
141    }
142
143    /// Convert to degrees, normalized to [0, 360) range.
144    /// Note: 360.0 becomes 0.0 due to modulo operation.
145    /// For conic gradients where 360.0 is meaningful, use `to_degrees_raw()`.
146    #[inline]
147    pub fn to_degrees(&self) -> f32 {
148        let val = match self.metric {
149            AngleMetric::Degree => self.number.get(),
150            AngleMetric::Grad => self.number.get() / 400.0 * 360.0,
151            AngleMetric::Radians => self.number.get() * 180.0 / core::f32::consts::PI,
152            AngleMetric::Turn => self.number.get() * 360.0,
153            AngleMetric::Percent => self.number.get() / 100.0 * 360.0,
154        };
155
156        let mut val = val % 360.0;
157        if val < 0.0 {
158            val = 360.0 + val;
159        }
160        val
161    }
162
163    /// Convert to degrees without normalization (raw value).
164    /// Use this for conic gradients where 360.0 is a meaningful distinct value from 0.0.
165    #[inline]
166    pub fn to_degrees_raw(&self) -> f32 {
167        match self.metric {
168            AngleMetric::Degree => self.number.get(),
169            AngleMetric::Grad => self.number.get() / 400.0 * 360.0,
170            AngleMetric::Radians => self.number.get() * 180.0 / core::f32::consts::PI,
171            AngleMetric::Turn => self.number.get() * 360.0,
172            AngleMetric::Percent => self.number.get() / 100.0 * 360.0,
173        }
174    }
175}
176
177// -- Parser
178
179#[derive(Clone, PartialEq)]
180pub enum CssAngleValueParseError<'a> {
181    EmptyString,
182    NoValueGiven(&'a str, AngleMetric),
183    ValueParseErr(ParseFloatError, &'a str),
184    InvalidAngle(&'a str),
185}
186
187impl_debug_as_display!(CssAngleValueParseError<'a>);
188impl_display! { CssAngleValueParseError<'a>, {
189    EmptyString => format!("Missing [rad / deg / turn / %] value"),
190    NoValueGiven(input, metric) => format!("Expected floating-point angle value, got: \"{}{}\"", input, metric),
191    ValueParseErr(err, number_str) => format!("Could not parse \"{}\" as floating-point value: \"{}\"", number_str, err),
192    InvalidAngle(s) => format!("Invalid angle value: \"{}\"", s),
193}}
194
195#[derive(Debug, Clone, PartialEq)]
196pub enum CssAngleValueParseErrorOwned {
197    EmptyString,
198    NoValueGiven(String, AngleMetric),
199    ValueParseErr(ParseFloatError, String),
200    InvalidAngle(String),
201}
202
203impl<'a> CssAngleValueParseError<'a> {
204    pub fn to_contained(&self) -> CssAngleValueParseErrorOwned {
205        match self {
206            CssAngleValueParseError::EmptyString => CssAngleValueParseErrorOwned::EmptyString,
207            CssAngleValueParseError::NoValueGiven(s, metric) => {
208                CssAngleValueParseErrorOwned::NoValueGiven(s.to_string(), *metric)
209            }
210            CssAngleValueParseError::ValueParseErr(err, s) => {
211                CssAngleValueParseErrorOwned::ValueParseErr(err.clone(), s.to_string())
212            }
213            CssAngleValueParseError::InvalidAngle(s) => {
214                CssAngleValueParseErrorOwned::InvalidAngle(s.to_string())
215            }
216        }
217    }
218}
219
220impl CssAngleValueParseErrorOwned {
221    pub fn to_shared<'a>(&'a self) -> CssAngleValueParseError<'a> {
222        match self {
223            CssAngleValueParseErrorOwned::EmptyString => CssAngleValueParseError::EmptyString,
224            CssAngleValueParseErrorOwned::NoValueGiven(s, metric) => {
225                CssAngleValueParseError::NoValueGiven(s.as_str(), *metric)
226            }
227            CssAngleValueParseErrorOwned::ValueParseErr(err, s) => {
228                CssAngleValueParseError::ValueParseErr(err.clone(), s.as_str())
229            }
230            CssAngleValueParseErrorOwned::InvalidAngle(s) => {
231                CssAngleValueParseError::InvalidAngle(s.as_str())
232            }
233        }
234    }
235}
236
237#[cfg(feature = "parser")]
238pub fn parse_angle_value<'a>(input: &'a str) -> Result<AngleValue, CssAngleValueParseError<'a>> {
239    let input = input.trim();
240
241    if input.is_empty() {
242        return Err(CssAngleValueParseError::EmptyString);
243    }
244
245    let match_values = &[
246        ("deg", AngleMetric::Degree),
247        ("turn", AngleMetric::Turn),
248        ("grad", AngleMetric::Grad),
249        ("rad", AngleMetric::Radians),
250        ("%", AngleMetric::Percent),
251    ];
252
253    for (match_val, metric) in match_values {
254        if input.ends_with(match_val) {
255            let value = &input[..input.len() - match_val.len()];
256            let value = value.trim();
257            if value.is_empty() {
258                return Err(CssAngleValueParseError::NoValueGiven(input, *metric));
259            }
260            match value.parse::<f32>() {
261                Ok(o) => return Ok(AngleValue::from_metric(*metric, o)),
262                Err(e) => return Err(CssAngleValueParseError::ValueParseErr(e, value)),
263            }
264        }
265    }
266
267    match input.parse::<f32>() {
268        Ok(o) => Ok(AngleValue::from_metric(AngleMetric::Degree, o)), // bare number is degrees
269        Err(_) => Err(CssAngleValueParseError::InvalidAngle(input)),
270    }
271}
272
273#[cfg(all(test, feature = "parser"))]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn test_parse_angle_value_deg() {
279        assert_eq!(parse_angle_value("90deg").unwrap(), AngleValue::deg(90.0));
280        assert_eq!(
281            parse_angle_value("-45.5deg").unwrap(),
282            AngleValue::deg(-45.5)
283        );
284        // Bare number defaults to degrees
285        assert_eq!(parse_angle_value("180").unwrap(), AngleValue::deg(180.0));
286    }
287
288    #[test]
289    fn test_parse_angle_value_rad() {
290        assert_eq!(parse_angle_value("1.57rad").unwrap(), AngleValue::rad(1.57));
291        assert_eq!(
292            parse_angle_value(" -3.14rad ").unwrap(),
293            AngleValue::rad(-3.14)
294        );
295    }
296
297    #[test]
298    fn test_parse_angle_value_grad() {
299        assert_eq!(
300            parse_angle_value("100grad").unwrap(),
301            AngleValue::grad(100.0)
302        );
303        assert_eq!(
304            parse_angle_value("400grad").unwrap(),
305            AngleValue::grad(400.0)
306        );
307    }
308
309    #[test]
310    fn test_parse_angle_value_turn() {
311        assert_eq!(
312            parse_angle_value("0.25turn").unwrap(),
313            AngleValue::turn(0.25)
314        );
315        assert_eq!(parse_angle_value("1turn").unwrap(), AngleValue::turn(1.0));
316    }
317
318    #[test]
319    fn test_parse_angle_value_percent() {
320        assert_eq!(parse_angle_value("50%").unwrap(), AngleValue::percent(50.0));
321    }
322
323    #[test]
324    fn test_parse_angle_value_errors() {
325        assert!(parse_angle_value("").is_err());
326        assert!(parse_angle_value("deg").is_err());
327        assert!(parse_angle_value("90 degs").is_err());
328        assert!(parse_angle_value("ninety-deg").is_err());
329        assert!(parse_angle_value("1.57 rads").is_err());
330    }
331
332    #[test]
333    fn test_to_degrees_conversion() {
334        assert_eq!(AngleValue::deg(90.0).to_degrees(), 90.0);
335        // Use 0.1 tolerance due to FloatValue fixed-point precision (multiplier = 1000.0)
336        assert!((AngleValue::rad(core::f32::consts::PI).to_degrees() - 180.0).abs() < 0.1);
337        assert_eq!(AngleValue::grad(100.0).to_degrees(), 90.0);
338        assert_eq!(AngleValue::turn(0.5).to_degrees(), 180.0);
339        assert_eq!(AngleValue::deg(-90.0).to_degrees(), 270.0);
340        assert_eq!(AngleValue::deg(450.0).to_degrees(), 90.0);
341    }
342}