1
//! CSS property types for angles (degrees, radians, etc.).
2

            
3
use alloc::string::{String, ToString};
4
use core::{fmt, num::ParseFloatError};
5
use crate::corety::AzString;
6

            
7
use crate::props::basic::error::ParseFloatErrorWithInput;
8

            
9
use 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)]
15
pub enum AngleMetric {
16
    #[default]
17
    Degree,
18
    Radians,
19
    Grad,
20
    Turn,
21
    Percent,
22
}
23

            
24

            
25
impl 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)]
41
pub struct AngleValue {
42
    pub metric: AngleMetric,
43
    pub number: FloatValue,
44
}
45

            
46
impl_option!(
47
    AngleValue,
48
    OptionAngleValue,
49
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
50
);
51

            
52
impl fmt::Debug for AngleValue {
53
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54
        write!(f, "{}", self)
55
    }
56
}
57

            
58
impl fmt::Display for AngleValue {
59
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60
        write!(f, "{}{}", self.number, self.metric)
61
    }
62
}
63

            
64
impl PrintAsCssValue for AngleValue {
65
    fn print_as_css_value(&self) -> String {
66
        format!("{}", self)
67
    }
68
}
69

            
70
impl 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
35
    pub fn deg(value: f32) -> Self {
134
35
        Self::from_metric(AngleMetric::Degree, value)
135
35
    }
136

            
137
    /// Creates an angle value in radians.
138
    #[inline]
139
3
    pub fn rad(value: f32) -> Self {
140
3
        Self::from_metric(AngleMetric::Radians, value)
141
3
    }
142

            
143
    /// Creates an angle value in gradians.
144
    #[inline]
145
3
    pub fn grad(value: f32) -> Self {
146
3
        Self::from_metric(AngleMetric::Grad, value)
147
3
    }
148

            
149
    /// Creates an angle value in turns.
150
    #[inline]
151
5
    pub fn turn(value: f32) -> Self {
152
5
        Self::from_metric(AngleMetric::Turn, value)
153
5
    }
154

            
155
    /// Creates an angle value in percent.
156
    #[inline]
157
1
    pub fn percent(value: f32) -> Self {
158
1
        Self::from_metric(AngleMetric::Percent, value)
159
1
    }
160

            
161
    /// Creates an angle value with the given metric.
162
    #[inline]
163
86
    pub fn from_metric(metric: AngleMetric, value: f32) -> Self {
164
86
        Self {
165
86
            metric,
166
86
            number: FloatValue::new(value),
167
86
        }
168
86
    }
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
11
    pub fn to_degrees(&self) -> f32 {
175
11
        let mut val = self.to_degrees_raw() % 360.0;
176
11
        if val < 0.0 {
177
1
            val += 360.0;
178
10
        }
179
11
        val
180
11
    }
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
27
    pub fn to_degrees_raw(&self) -> f32 {
186
27
        match self.metric {
187
24
            AngleMetric::Degree => self.number.get(),
188
1
            AngleMetric::Grad => self.number.get() / 400.0 * 360.0,
189
1
            AngleMetric::Radians => self.number.get() * 180.0 / core::f32::consts::PI,
190
1
            AngleMetric::Turn => self.number.get() * 360.0,
191
            AngleMetric::Percent => self.number.get() / 100.0 * 360.0,
192
        }
193
27
    }
194
}
195

            
196
// -- Parser
197

            
198
/// Error returned when parsing a CSS angle value from a string.
199
#[derive(Clone, PartialEq)]
200
pub enum CssAngleValueParseError<'a> {
201
    EmptyString,
202
    NoValueGiven(&'a str, AngleMetric),
203
    ValueParseErr(ParseFloatError, &'a str),
204
    InvalidAngle(&'a str),
205
}
206

            
207
impl_debug_as_display!(CssAngleValueParseError<'a>);
208
impl_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)]
218
pub 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)]
226
pub enum CssAngleValueParseErrorOwned {
227
    EmptyString,
228
    NoValueGiven(AngleNoValueGivenError),
229
    ValueParseErr(ParseFloatErrorWithInput),
230
    InvalidAngle(AzString),
231
}
232

            
233
impl<'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

            
250
impl 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")]
270
115
pub fn parse_angle_value<'a>(input: &'a str) -> Result<AngleValue, CssAngleValueParseError<'a>> {
271
115
    let input = input.trim();
272

            
273
115
    if input.is_empty() {
274
1
        return Err(CssAngleValueParseError::EmptyString);
275
114
    }
276

            
277
114
    let match_values = &[
278
114
        ("deg", AngleMetric::Degree),
279
114
        ("turn", AngleMetric::Turn),
280
114
        ("grad", AngleMetric::Grad),
281
114
        ("rad", AngleMetric::Radians),
282
114
        ("%", AngleMetric::Percent),
283
114
    ];
284

            
285
527
    for (match_val, metric) in match_values {
286
448
        if input.ends_with(match_val) {
287
35
            let value = &input[..input.len() - match_val.len()];
288
35
            let value = value.trim();
289
35
            if value.is_empty() {
290
1
                return Err(CssAngleValueParseError::NoValueGiven(input, *metric));
291
34
            }
292
34
            match value.parse::<f32>() {
293
33
                Ok(o) => return Ok(AngleValue::from_metric(*metric, o)),
294
1
                Err(e) => return Err(CssAngleValueParseError::ValueParseErr(e, value)),
295
            }
296
413
        }
297
    }
298

            
299
79
    match input.parse::<f32>() {
300
6
        Ok(o) => Ok(AngleValue::from_metric(AngleMetric::Degree, o)), // bare number is degrees
301
73
        Err(_) => Err(CssAngleValueParseError::InvalidAngle(input)),
302
    }
303
115
}
304

            
305
#[cfg(all(test, feature = "parser"))]
306
mod tests {
307
    use super::*;
308

            
309
    #[test]
310
1
    fn test_parse_angle_value_deg() {
311
1
        assert_eq!(parse_angle_value("90deg").unwrap(), AngleValue::deg(90.0));
312
1
        assert_eq!(
313
1
            parse_angle_value("-45.5deg").unwrap(),
314
1
            AngleValue::deg(-45.5)
315
        );
316
        // Bare number defaults to degrees
317
1
        assert_eq!(parse_angle_value("180").unwrap(), AngleValue::deg(180.0));
318
1
    }
319

            
320
    #[test]
321
1
    fn test_parse_angle_value_rad() {
322
1
        assert_eq!(parse_angle_value("1.57rad").unwrap(), AngleValue::rad(1.57));
323
1
        assert_eq!(
324
1
            parse_angle_value(" -3.14rad ").unwrap(),
325
1
            AngleValue::rad(-3.14)
326
        );
327
1
    }
328

            
329
    #[test]
330
1
    fn test_parse_angle_value_grad() {
331
1
        assert_eq!(
332
1
            parse_angle_value("100grad").unwrap(),
333
1
            AngleValue::grad(100.0)
334
        );
335
1
        assert_eq!(
336
1
            parse_angle_value("400grad").unwrap(),
337
1
            AngleValue::grad(400.0)
338
        );
339
1
    }
340

            
341
    #[test]
342
1
    fn test_parse_angle_value_turn() {
343
1
        assert_eq!(
344
1
            parse_angle_value("0.25turn").unwrap(),
345
1
            AngleValue::turn(0.25)
346
        );
347
1
        assert_eq!(parse_angle_value("1turn").unwrap(), AngleValue::turn(1.0));
348
1
    }
349

            
350
    #[test]
351
1
    fn test_parse_angle_value_percent() {
352
1
        assert_eq!(parse_angle_value("50%").unwrap(), AngleValue::percent(50.0));
353
1
    }
354

            
355
    #[test]
356
1
    fn test_parse_angle_value_errors() {
357
1
        assert!(parse_angle_value("").is_err());
358
1
        assert!(parse_angle_value("deg").is_err());
359
1
        assert!(parse_angle_value("90 degs").is_err());
360
1
        assert!(parse_angle_value("ninety-deg").is_err());
361
1
        assert!(parse_angle_value("1.57 rads").is_err());
362
1
    }
363

            
364
    #[test]
365
1
    fn test_to_degrees_conversion() {
366
1
        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
1
        assert!((AngleValue::rad(core::f32::consts::PI).to_degrees() - 180.0).abs() < 0.1);
369
1
        assert_eq!(AngleValue::grad(100.0).to_degrees(), 90.0);
370
1
        assert_eq!(AngleValue::turn(0.5).to_degrees(), 180.0);
371
1
        assert_eq!(AngleValue::deg(-90.0).to_degrees(), 270.0);
372
1
        assert_eq!(AngleValue::deg(450.0).to_degrees(), 90.0);
373
1
    }
374
}