Skip to main content

azul_css/props/basic/
direction.rs

1//! CSS property types for direction (for gradients).
2
3use alloc::string::String;
4use core::{fmt, num::ParseFloatError};
5use crate::corety::AzString;
6
7use crate::props::{
8    basic::{
9        angle::{
10            parse_angle_value, AngleValue, CssAngleValueParseError, CssAngleValueParseErrorOwned,
11        },
12        geometry::{LayoutPoint, LayoutRect},
13    },
14    formatter::PrintAsCssValue,
15};
16
17/// Corner or side of a rectangle, used to specify CSS gradient directions
18/// (e.g. `to top right`).
19#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[repr(C)]
21pub enum DirectionCorner {
22    Right,
23    Left,
24    Top,
25    Bottom,
26    TopRight,
27    TopLeft,
28    BottomRight,
29    BottomLeft,
30}
31
32impl fmt::Display for DirectionCorner {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        write!(
35            f,
36            "{}",
37            match self {
38                Self::Right => "right",
39                Self::Left => "left",
40                Self::Top => "top",
41                Self::Bottom => "bottom",
42                Self::TopRight => "top right",
43                Self::TopLeft => "top left",
44                Self::BottomRight => "bottom right",
45                Self::BottomLeft => "bottom left",
46            }
47        )
48    }
49}
50
51impl PrintAsCssValue for DirectionCorner {
52    fn print_as_css_value(&self) -> String {
53        format!("{self}")
54    }
55}
56
57impl DirectionCorner {
58    #[must_use] pub const fn opposite(&self) -> Self {
59        use self::DirectionCorner::{Right, Left, Top, Bottom, TopRight, BottomLeft, TopLeft, BottomRight};
60        match *self {
61            Right => Left,
62            Left => Right,
63            Top => Bottom,
64            Bottom => Top,
65            TopRight => BottomLeft,
66            BottomLeft => TopRight,
67            TopLeft => BottomRight,
68            BottomRight => TopLeft,
69        }
70    }
71
72    #[must_use] pub const fn combine(&self, other: &Self) -> Option<Self> {
73        use self::DirectionCorner::{Right, Top, TopRight, Left, TopLeft, Bottom, BottomRight, BottomLeft};
74        match (*self, *other) {
75            (Right, Top) | (Top, Right) => Some(TopRight),
76            (Left, Top) | (Top, Left) => Some(TopLeft),
77            (Right, Bottom) | (Bottom, Right) => Some(BottomRight),
78            (Left, Bottom) | (Bottom, Left) => Some(BottomLeft),
79            _ => None,
80        }
81    }
82
83    #[must_use] pub const fn to_point(&self, rect: &LayoutRect) -> LayoutPoint {
84        use self::DirectionCorner::{Right, Left, Top, Bottom, TopRight, TopLeft, BottomRight, BottomLeft};
85        match *self {
86            Right => LayoutPoint {
87                x: rect.size.width,
88                y: rect.size.height / 2,
89            },
90            Left => LayoutPoint {
91                x: 0,
92                y: rect.size.height / 2,
93            },
94            Top => LayoutPoint {
95                x: rect.size.width / 2,
96                y: 0,
97            },
98            Bottom => LayoutPoint {
99                x: rect.size.width / 2,
100                y: rect.size.height,
101            },
102            TopRight => LayoutPoint {
103                x: rect.size.width,
104                y: 0,
105            },
106            TopLeft => LayoutPoint { x: 0, y: 0 },
107            BottomRight => LayoutPoint {
108                x: rect.size.width,
109                y: rect.size.height,
110            },
111            BottomLeft => LayoutPoint {
112                x: 0,
113                y: rect.size.height,
114            },
115        }
116    }
117}
118
119/// A pair of corners representing the start and end of a CSS gradient direction.
120#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
121#[repr(C)]
122pub struct DirectionCorners {
123    /// The corner or side from which the gradient starts.
124    pub dir_from: DirectionCorner,
125    /// The corner or side at which the gradient ends.
126    pub dir_to: DirectionCorner,
127}
128
129/// CSS direction (necessary for gradients). Can either be a fixed angle or
130/// a direction ("to right" / "to left", etc.).
131#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
132#[repr(C, u8)]
133pub enum Direction {
134    Angle(AngleValue),
135    FromTo(DirectionCorners),
136}
137
138impl Default for Direction {
139    fn default() -> Self {
140        Self::FromTo(DirectionCorners {
141            dir_from: DirectionCorner::Top,
142            dir_to: DirectionCorner::Bottom,
143        })
144    }
145}
146
147impl PrintAsCssValue for Direction {
148    fn print_as_css_value(&self) -> String {
149        match self {
150            Self::Angle(a) => format!("{a}"),
151            Self::FromTo(d) => format!("to {}", d.dir_to), // simplified "from X to Y"
152        }
153    }
154}
155
156impl Direction {
157    #[must_use] pub fn to_points(&self, rect: &LayoutRect) -> (LayoutPoint, LayoutPoint) {
158        match self {
159            Self::Angle(angle_value) => {
160                // Convert the angle to start/end points on the rectangle.
161                // Normalize to [0, 360) so negative angles and angles >= 360 fall
162                // into the same quadrant branches below (rem_euclid is always >= 0).
163                let deg = (-angle_value.to_degrees()).rem_euclid(360.0);
164                let width_half = crate::cast::isize_to_f32(rect.size.width) / 2.0;
165                let height_half = crate::cast::isize_to_f32(rect.size.height) / 2.0;
166                let hypotenuse_len = libm::hypotf(width_half, height_half);
167                let angle_to_corner = libm::atanf(height_half / width_half).to_degrees();
168                let corner_angle = if deg < 90.0 {
169                    90.0 - angle_to_corner
170                } else if deg < 180.0 {
171                    90.0 + angle_to_corner
172                } else if deg < 270.0 {
173                    270.0 - angle_to_corner
174                } else {
175                    270.0 + angle_to_corner
176                };
177                let angle_diff = corner_angle - deg;
178                let line_length = libm::fabsf(hypotenuse_len * libm::cosf(angle_diff.to_radians()));
179                let dx = libm::sinf(deg.to_radians()) * line_length;
180                let dy = libm::cosf(deg.to_radians()) * line_length;
181                (
182                    LayoutPoint::new(
183                        crate::cast::f32_to_isize(libm::roundf(width_half - dx)),
184                        crate::cast::f32_to_isize(libm::roundf(height_half + dy)),
185                    ),
186                    LayoutPoint::new(
187                        crate::cast::f32_to_isize(libm::roundf(width_half + dx)),
188                        crate::cast::f32_to_isize(libm::roundf(height_half - dy)),
189                    ),
190                )
191            }
192            Self::FromTo(ft) => (ft.dir_from.to_point(rect), ft.dir_to.to_point(rect)),
193        }
194    }
195}
196
197// -- Parser
198
199#[derive(Debug, Copy, Clone, PartialEq, Eq)]
200pub enum CssDirectionCornerParseError<'a> {
201    InvalidDirection(&'a str),
202}
203
204impl_display! { CssDirectionCornerParseError<'a>, {
205    InvalidDirection(val) => format!("Invalid direction: \"{}\"", val),
206}}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
209#[repr(C, u8)]
210pub enum CssDirectionCornerParseErrorOwned {
211    InvalidDirection(AzString),
212}
213
214impl CssDirectionCornerParseError<'_> {
215    #[must_use] pub fn to_contained(&self) -> CssDirectionCornerParseErrorOwned {
216        match self {
217            CssDirectionCornerParseError::InvalidDirection(s) => {
218                CssDirectionCornerParseErrorOwned::InvalidDirection((*s).to_string().into())
219            }
220        }
221    }
222}
223
224impl CssDirectionCornerParseErrorOwned {
225    #[must_use] pub fn to_shared(&self) -> CssDirectionCornerParseError<'_> {
226        match self {
227            Self::InvalidDirection(s) => {
228                CssDirectionCornerParseError::InvalidDirection(s.as_str())
229            }
230        }
231    }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub enum CssDirectionParseError<'a> {
236    Error(&'a str),
237    InvalidArguments(&'a str),
238    ParseFloat(ParseFloatError),
239    CornerError(CssDirectionCornerParseError<'a>),
240    AngleError(CssAngleValueParseError<'a>),
241}
242
243impl_display! {CssDirectionParseError<'a>, {
244    Error(e) => e,
245    InvalidArguments(val) => format!("Invalid arguments: \"{}\"", val),
246    ParseFloat(e) => format!("Invalid value: {}", e),
247    CornerError(e) => format!("Invalid corner value: {}", e),
248    AngleError(e) => format!("Invalid angle value: {}", e),
249}}
250
251impl From<ParseFloatError> for CssDirectionParseError<'_> {
252    fn from(e: ParseFloatError) -> Self {
253        CssDirectionParseError::ParseFloat(e)
254    }
255}
256impl_from! { CssDirectionCornerParseError<'a>, CssDirectionParseError::CornerError }
257impl_from! { CssAngleValueParseError<'a>, CssDirectionParseError::AngleError }
258
259#[derive(Debug, Clone, PartialEq, Eq)]
260#[repr(C, u8)]
261pub enum CssDirectionParseErrorOwned {
262    Error(AzString),
263    InvalidArguments(AzString),
264    ParseFloat(crate::props::basic::error::ParseFloatError),
265    CornerError(CssDirectionCornerParseErrorOwned),
266    AngleError(CssAngleValueParseErrorOwned),
267}
268
269impl CssDirectionParseError<'_> {
270    #[must_use] pub fn to_contained(&self) -> CssDirectionParseErrorOwned {
271        match self {
272            CssDirectionParseError::Error(s) => CssDirectionParseErrorOwned::Error((*s).to_string().into()),
273            CssDirectionParseError::InvalidArguments(s) => {
274                CssDirectionParseErrorOwned::InvalidArguments((*s).to_string().into())
275            }
276            CssDirectionParseError::ParseFloat(e) => {
277                CssDirectionParseErrorOwned::ParseFloat(e.clone().into())
278            }
279            CssDirectionParseError::CornerError(e) => {
280                CssDirectionParseErrorOwned::CornerError(e.to_contained())
281            }
282            CssDirectionParseError::AngleError(e) => {
283                CssDirectionParseErrorOwned::AngleError(e.to_contained())
284            }
285        }
286    }
287}
288
289impl CssDirectionParseErrorOwned {
290    #[must_use] pub fn to_shared(&self) -> CssDirectionParseError<'_> {
291        match self {
292            Self::Error(s) => CssDirectionParseError::Error(s.as_str()),
293            Self::InvalidArguments(s) => {
294                CssDirectionParseError::InvalidArguments(s.as_str())
295            }
296            Self::ParseFloat(e) => {
297                CssDirectionParseError::ParseFloat(e.to_std())
298            }
299            Self::CornerError(e) => {
300                CssDirectionParseError::CornerError(e.to_shared())
301            }
302            Self::AngleError(e) => {
303                CssDirectionParseError::AngleError(e.to_shared())
304            }
305        }
306    }
307}
308
309#[cfg(feature = "parser")]
310fn parse_direction_corner(
311    input: &str,
312) -> Result<DirectionCorner, CssDirectionCornerParseError<'_>> {
313    match input {
314        "right" => Ok(DirectionCorner::Right),
315        "left" => Ok(DirectionCorner::Left),
316        "top" => Ok(DirectionCorner::Top),
317        "bottom" => Ok(DirectionCorner::Bottom),
318        _ => Err(CssDirectionCornerParseError::InvalidDirection(input)),
319    }
320}
321
322#[cfg(feature = "parser")]
323/// # Errors
324///
325/// Returns an error if `input` is not a valid CSS `direction` value.
326pub fn parse_direction(input: &str) -> Result<Direction, CssDirectionParseError<'_>> {
327    let mut input_iter = input.split_whitespace();
328    let first_input = input_iter
329        .next()
330        .ok_or(CssDirectionParseError::Error(input))?;
331
332    if let Ok(angle) = parse_angle_value(first_input) {
333        return Ok(Direction::Angle(angle));
334    }
335
336    if first_input != "to" {
337        return Err(CssDirectionParseError::InvalidArguments(input));
338    }
339
340    let components = input_iter.collect::<Vec<_>>();
341    if components.is_empty() || components.len() > 2 {
342        return Err(CssDirectionParseError::InvalidArguments(input));
343    }
344
345    let first_corner = parse_direction_corner(components[0])?;
346    let end = if components.len() == 2 {
347        let second_corner = parse_direction_corner(components[1])?;
348        first_corner
349            .combine(&second_corner)
350            .ok_or(CssDirectionParseError::InvalidArguments(input))?
351    } else {
352        first_corner
353    };
354
355    Ok(Direction::FromTo(DirectionCorners {
356        dir_from: end.opposite(),
357        dir_to: end,
358    }))
359}
360
361#[cfg(all(test, feature = "parser"))]
362mod tests {
363    use super::*;
364    use crate::props::basic::angle::AngleValue;
365
366    #[test]
367    fn test_parse_direction_angle() {
368        assert_eq!(
369            parse_direction("45deg").unwrap(),
370            Direction::Angle(AngleValue::deg(45.0))
371        );
372        assert_eq!(
373            parse_direction("  -0.25turn  ").unwrap(),
374            Direction::Angle(AngleValue::turn(-0.25))
375        );
376    }
377
378    #[test]
379    fn test_parse_direction_corners() {
380        assert_eq!(
381            parse_direction("to right").unwrap(),
382            Direction::FromTo(DirectionCorners {
383                dir_from: DirectionCorner::Left,
384                dir_to: DirectionCorner::Right,
385            })
386        );
387        assert_eq!(
388            parse_direction("to top left").unwrap(),
389            Direction::FromTo(DirectionCorners {
390                dir_from: DirectionCorner::BottomRight,
391                dir_to: DirectionCorner::TopLeft,
392            })
393        );
394        assert_eq!(
395            parse_direction("to left top").unwrap(),
396            Direction::FromTo(DirectionCorners {
397                dir_from: DirectionCorner::BottomRight,
398                dir_to: DirectionCorner::TopLeft,
399            })
400        );
401    }
402
403    #[test]
404    fn test_parse_direction_errors() {
405        assert!(parse_direction("").is_err());
406        assert!(parse_direction("to").is_err());
407        assert!(parse_direction("right").is_err());
408        assert!(parse_direction("to center").is_err());
409        assert!(parse_direction("to top right bottom").is_err());
410        assert!(parse_direction("to top top").is_err());
411    }
412}