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