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                DirectionCorner::Right => "right",
39                DirectionCorner::Left => "left",
40                DirectionCorner::Top => "top",
41                DirectionCorner::Bottom => "bottom",
42                DirectionCorner::TopRight => "top right",
43                DirectionCorner::TopLeft => "top left",
44                DirectionCorner::BottomRight => "bottom right",
45                DirectionCorner::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    pub const fn opposite(&self) -> Self {
59        use self::DirectionCorner::*;
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    pub const fn combine(&self, other: &Self) -> Option<Self> {
73        use self::DirectionCorner::*;
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    pub const fn to_point(&self, rect: &LayoutRect) -> LayoutPoint {
84        use self::DirectionCorner::*;
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        Direction::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            Direction::Angle(a) => format!("{}", a),
151            Direction::FromTo(d) => format!("to {}", d.dir_to), // simplified "from X to Y"
152        }
153    }
154}
155
156impl Direction {
157    pub fn to_points(&self, rect: &LayoutRect) -> (LayoutPoint, LayoutPoint) {
158        match self {
159            Direction::Angle(angle_value) => {
160                // Convert the angle to start/end points on the rectangle.
161                // TODO: does not handle negative angles or angles >= 360 correctly.
162                let deg = -angle_value.to_degrees();
163                let width_half = rect.size.width as f32 / 2.0;
164                let height_half = rect.size.height as f32 / 2.0;
165                let hypotenuse_len = libm::hypotf(width_half, height_half);
166                let angle_to_corner = libm::atanf(height_half / width_half).to_degrees();
167                let corner_angle = if deg < 90.0 {
168                    90.0 - angle_to_corner
169                } else if deg < 180.0 {
170                    90.0 + angle_to_corner
171                } else if deg < 270.0 {
172                    270.0 - angle_to_corner
173                } else {
174                    270.0 + angle_to_corner
175                };
176                let angle_diff = corner_angle - deg;
177                let line_length = libm::fabsf(hypotenuse_len * libm::cosf(angle_diff.to_radians()));
178                let dx = libm::sinf(deg.to_radians()) * line_length;
179                let dy = libm::cosf(deg.to_radians()) * line_length;
180                (
181                    LayoutPoint::new(
182                        libm::roundf(width_half - dx) as isize,
183                        libm::roundf(height_half + dy) as isize,
184                    ),
185                    LayoutPoint::new(
186                        libm::roundf(width_half + dx) as isize,
187                        libm::roundf(height_half - dy) as isize,
188                    ),
189                )
190            }
191            Direction::FromTo(ft) => (ft.dir_from.to_point(rect), ft.dir_to.to_point(rect)),
192        }
193    }
194}
195
196// -- Parser
197
198#[derive(Debug, Copy, Clone, PartialEq)]
199pub enum CssDirectionCornerParseError<'a> {
200    InvalidDirection(&'a str),
201}
202
203impl_display! { CssDirectionCornerParseError<'a>, {
204    InvalidDirection(val) => format!("Invalid direction: \"{}\"", val),
205}}
206
207#[derive(Debug, Clone, PartialEq)]
208#[repr(C, u8)]
209pub enum CssDirectionCornerParseErrorOwned {
210    InvalidDirection(AzString),
211}
212
213impl<'a> CssDirectionCornerParseError<'a> {
214    pub fn to_contained(&self) -> CssDirectionCornerParseErrorOwned {
215        match self {
216            CssDirectionCornerParseError::InvalidDirection(s) => {
217                CssDirectionCornerParseErrorOwned::InvalidDirection(s.to_string().into())
218            }
219        }
220    }
221}
222
223impl CssDirectionCornerParseErrorOwned {
224    pub fn to_shared<'a>(&'a self) -> CssDirectionCornerParseError<'a> {
225        match self {
226            CssDirectionCornerParseErrorOwned::InvalidDirection(s) => {
227                CssDirectionCornerParseError::InvalidDirection(s.as_str())
228            }
229        }
230    }
231}
232
233#[derive(Debug, Clone, PartialEq)]
234pub enum CssDirectionParseError<'a> {
235    Error(&'a str),
236    InvalidArguments(&'a str),
237    ParseFloat(ParseFloatError),
238    CornerError(CssDirectionCornerParseError<'a>),
239    AngleError(CssAngleValueParseError<'a>),
240}
241
242impl_display! {CssDirectionParseError<'a>, {
243    Error(e) => e,
244    InvalidArguments(val) => format!("Invalid arguments: \"{}\"", val),
245    ParseFloat(e) => format!("Invalid value: {}", e),
246    CornerError(e) => format!("Invalid corner value: {}", e),
247    AngleError(e) => format!("Invalid angle value: {}", e),
248}}
249
250impl<'a> From<ParseFloatError> for CssDirectionParseError<'a> {
251    fn from(e: ParseFloatError) -> Self {
252        CssDirectionParseError::ParseFloat(e)
253    }
254}
255impl_from! { CssDirectionCornerParseError<'a>, CssDirectionParseError::CornerError }
256impl_from! { CssAngleValueParseError<'a>, CssDirectionParseError::AngleError }
257
258#[derive(Debug, Clone, PartialEq)]
259#[repr(C, u8)]
260pub enum CssDirectionParseErrorOwned {
261    Error(AzString),
262    InvalidArguments(AzString),
263    ParseFloat(crate::props::basic::error::ParseFloatError),
264    CornerError(CssDirectionCornerParseErrorOwned),
265    AngleError(CssAngleValueParseErrorOwned),
266}
267
268impl<'a> CssDirectionParseError<'a> {
269    pub fn to_contained(&self) -> CssDirectionParseErrorOwned {
270        match self {
271            CssDirectionParseError::Error(s) => CssDirectionParseErrorOwned::Error(s.to_string().into()),
272            CssDirectionParseError::InvalidArguments(s) => {
273                CssDirectionParseErrorOwned::InvalidArguments(s.to_string().into())
274            }
275            CssDirectionParseError::ParseFloat(e) => {
276                CssDirectionParseErrorOwned::ParseFloat(e.clone().into())
277            }
278            CssDirectionParseError::CornerError(e) => {
279                CssDirectionParseErrorOwned::CornerError(e.to_contained())
280            }
281            CssDirectionParseError::AngleError(e) => {
282                CssDirectionParseErrorOwned::AngleError(e.to_contained())
283            }
284        }
285    }
286}
287
288impl CssDirectionParseErrorOwned {
289    pub fn to_shared<'a>(&'a self) -> CssDirectionParseError<'a> {
290        match self {
291            CssDirectionParseErrorOwned::Error(s) => CssDirectionParseError::Error(s.as_str()),
292            CssDirectionParseErrorOwned::InvalidArguments(s) => {
293                CssDirectionParseError::InvalidArguments(s.as_str())
294            }
295            CssDirectionParseErrorOwned::ParseFloat(e) => {
296                CssDirectionParseError::ParseFloat(e.to_std())
297            }
298            CssDirectionParseErrorOwned::CornerError(e) => {
299                CssDirectionParseError::CornerError(e.to_shared())
300            }
301            CssDirectionParseErrorOwned::AngleError(e) => {
302                CssDirectionParseError::AngleError(e.to_shared())
303            }
304        }
305    }
306}
307
308#[cfg(feature = "parser")]
309fn parse_direction_corner<'a>(
310    input: &'a str,
311) -> Result<DirectionCorner, CssDirectionCornerParseError<'a>> {
312    match input {
313        "right" => Ok(DirectionCorner::Right),
314        "left" => Ok(DirectionCorner::Left),
315        "top" => Ok(DirectionCorner::Top),
316        "bottom" => Ok(DirectionCorner::Bottom),
317        _ => Err(CssDirectionCornerParseError::InvalidDirection(input)),
318    }
319}
320
321#[cfg(feature = "parser")]
322pub fn parse_direction<'a>(input: &'a str) -> Result<Direction, CssDirectionParseError<'a>> {
323    let mut input_iter = input.split_whitespace();
324    let first_input = input_iter
325        .next()
326        .ok_or(CssDirectionParseError::Error(input))?;
327
328    if let Ok(angle) = parse_angle_value(first_input) {
329        return Ok(Direction::Angle(angle));
330    }
331
332    if first_input != "to" {
333        return Err(CssDirectionParseError::InvalidArguments(input));
334    }
335
336    let components = input_iter.collect::<Vec<_>>();
337    if components.is_empty() || components.len() > 2 {
338        return Err(CssDirectionParseError::InvalidArguments(input));
339    }
340
341    let first_corner = parse_direction_corner(components[0])?;
342    let end = if components.len() == 2 {
343        let second_corner = parse_direction_corner(components[1])?;
344        first_corner
345            .combine(&second_corner)
346            .ok_or(CssDirectionParseError::InvalidArguments(input))?
347    } else {
348        first_corner
349    };
350
351    Ok(Direction::FromTo(DirectionCorners {
352        dir_from: end.opposite(),
353        dir_to: end,
354    }))
355}
356
357#[cfg(all(test, feature = "parser"))]
358mod tests {
359    use super::*;
360    use crate::props::basic::angle::AngleValue;
361
362    #[test]
363    fn test_parse_direction_angle() {
364        assert_eq!(
365            parse_direction("45deg").unwrap(),
366            Direction::Angle(AngleValue::deg(45.0))
367        );
368        assert_eq!(
369            parse_direction("  -0.25turn  ").unwrap(),
370            Direction::Angle(AngleValue::turn(-0.25))
371        );
372    }
373
374    #[test]
375    fn test_parse_direction_corners() {
376        assert_eq!(
377            parse_direction("to right").unwrap(),
378            Direction::FromTo(DirectionCorners {
379                dir_from: DirectionCorner::Left,
380                dir_to: DirectionCorner::Right,
381            })
382        );
383        assert_eq!(
384            parse_direction("to top left").unwrap(),
385            Direction::FromTo(DirectionCorners {
386                dir_from: DirectionCorner::BottomRight,
387                dir_to: DirectionCorner::TopLeft,
388            })
389        );
390        assert_eq!(
391            parse_direction("to left top").unwrap(),
392            Direction::FromTo(DirectionCorners {
393                dir_from: DirectionCorner::BottomRight,
394                dir_to: DirectionCorner::TopLeft,
395            })
396        );
397    }
398
399    #[test]
400    fn test_parse_direction_errors() {
401        assert!(parse_direction("").is_err());
402        assert!(parse_direction("to").is_err());
403        assert!(parse_direction("right").is_err());
404        assert!(parse_direction("to center").is_err());
405        assert!(parse_direction("to top right bottom").is_err());
406        assert!(parse_direction("to top top").is_err());
407    }
408}