svgrtypes/
transform_origin.rs

1use crate::directional_position::DirectionalPosition;
2use crate::stream::Stream;
3use crate::{Length, LengthUnit};
4
5#[derive(Clone, Copy, PartialEq, Debug)]
6#[allow(missing_docs)]
7enum Position {
8    Length(Length),
9    DirectionalPosition(DirectionalPosition),
10}
11
12impl Position {
13    fn is_vertical(&self) -> bool {
14        match self {
15            Position::Length(_) => true,
16            Position::DirectionalPosition(dp) => dp.is_vertical(),
17        }
18    }
19
20    fn is_horizontal(&self) -> bool {
21        match self {
22            Position::Length(_) => true,
23            Position::DirectionalPosition(dp) => dp.is_horizontal(),
24        }
25    }
26}
27
28impl From<Position> for Length {
29    fn from(value: Position) -> Self {
30        match value {
31            Position::Length(l) => l,
32            Position::DirectionalPosition(dp) => dp.into(),
33        }
34    }
35}
36
37/// Representation of the [`<transform-origin>`] type.
38///
39/// [`<transform-origin>`]: https://drafts.csswg.org/css-transforms/#transform-origin-property
40#[derive(Clone, Copy, PartialEq, Debug)]
41pub struct TransformOrigin {
42    /// The x offset of the transform origin.
43    pub x_offset: Length,
44    /// The y offset of the transform origin.
45    pub y_offset: Length,
46    /// The z offset of the transform origin.
47    pub z_offset: Length,
48}
49
50impl TransformOrigin {
51    /// Constructs a new transform origin.
52    #[inline]
53    pub fn new(x_offset: Length, y_offset: Length, z_offset: Length) -> Self {
54        TransformOrigin {
55            x_offset,
56            y_offset,
57            z_offset,
58        }
59    }
60}
61
62/// List of possible [`TransformOrigin`] parsing errors.
63#[derive(Clone, Copy, Debug)]
64pub enum TransformOriginError {
65    /// One of the numbers is invalid.
66    MissingParameters,
67    /// One of the parameters is invalid.
68    InvalidParameters,
69    /// z-index is not a percentage.
70    ZIndexIsPercentage,
71}
72
73impl std::fmt::Display for TransformOriginError {
74    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
75        match *self {
76            TransformOriginError::MissingParameters => {
77                write!(f, "transform origin doesn't have enough parameters")
78            }
79            TransformOriginError::InvalidParameters => {
80                write!(f, "transform origin has invalid parameters")
81            }
82            TransformOriginError::ZIndexIsPercentage => {
83                write!(f, "z-index cannot be a percentage")
84            }
85        }
86    }
87}
88
89impl std::error::Error for TransformOriginError {
90    fn description(&self) -> &str {
91        "a transform origin parsing error"
92    }
93}
94
95impl std::str::FromStr for TransformOrigin {
96    type Err = TransformOriginError;
97
98    fn from_str(text: &str) -> Result<Self, TransformOriginError> {
99        let mut stream = Stream::from(text);
100
101        if stream.at_end() {
102            return Err(TransformOriginError::MissingParameters);
103        }
104
105        let parse_part = |stream: &mut Stream| {
106            if let Ok(dp) = stream.parse_directional_position() {
107                Some(Position::DirectionalPosition(dp))
108            } else if let Ok(l) = stream.parse_length() {
109                Some(Position::Length(l))
110            } else {
111                None
112            }
113        };
114
115        let first_arg = parse_part(&mut stream);
116        let mut second_arg = None;
117        let mut third_arg = None;
118
119        if !stream.at_end() {
120            stream.skip_spaces();
121            stream.parse_list_separator();
122            second_arg =
123                Some(parse_part(&mut stream).ok_or(TransformOriginError::InvalidParameters)?);
124        }
125
126        if !stream.at_end() {
127            stream.skip_spaces();
128            stream.parse_list_separator();
129            third_arg = Some(
130                stream
131                    .parse_length()
132                    .map_err(|_| TransformOriginError::InvalidParameters)?,
133            );
134        }
135
136        stream.skip_spaces();
137
138        if !stream.at_end() {
139            return Err(TransformOriginError::InvalidParameters);
140        }
141
142        let result = match (first_arg, second_arg, third_arg) {
143            (Some(p), None, None) => {
144                let (x_offset, y_offset) = if p.is_horizontal() {
145                    (p.into(), DirectionalPosition::Center.into())
146                } else {
147                    (DirectionalPosition::Center.into(), p.into())
148                };
149
150                TransformOrigin::new(x_offset, y_offset, Length::new(0.0, LengthUnit::Px))
151            }
152            (Some(p1), Some(p2), length) => {
153                if let Some(length) = length {
154                    if length.unit == LengthUnit::Percent {
155                        return Err(TransformOriginError::ZIndexIsPercentage);
156                    }
157                }
158
159                let length = length.unwrap_or(Length::new(0.0, LengthUnit::Px));
160
161                let check = |pos| match pos {
162                    Position::Length(_) => true,
163                    Position::DirectionalPosition(dp) => dp == DirectionalPosition::Center,
164                };
165
166                let only_keyword_is_center = check(p1) && check(p2);
167
168                if only_keyword_is_center {
169                    TransformOrigin::new(p1.into(), p2.into(), length)
170                } else {
171                    // There is at least one of `left`, `right`, `top`, or `bottom`
172                    if p1.is_horizontal() && p2.is_vertical() {
173                        TransformOrigin::new(p1.into(), p2.into(), length)
174                    } else if p1.is_vertical() && p2.is_horizontal() {
175                        TransformOrigin::new(p2.into(), p1.into(), length)
176                    } else {
177                        return Err(TransformOriginError::InvalidParameters);
178                    }
179                }
180            }
181            _ => unreachable!(),
182        };
183
184        Ok(result)
185    }
186}
187
188#[rustfmt::skip]
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use std::str::FromStr;
193
194    macro_rules! test {
195        ($name:ident, $text:expr, $result:expr) => (
196            #[test]
197            fn $name() {
198                let v = TransformOrigin::from_str($text).unwrap();
199                assert_eq!(v, $result);
200            }
201        )
202    }
203
204    test!(parse_1, "center", TransformOrigin::new(Length::new(50.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px)));
205    test!(parse_2, "left", TransformOrigin::new(Length::new(0.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px)));
206    test!(parse_3, "right", TransformOrigin::new(Length::new(100.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px)));
207    test!(parse_4, "top", TransformOrigin::new(Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px)));
208    test!(parse_5, "bottom", TransformOrigin::new(Length::new(50.0, LengthUnit::Percent), Length::new(100.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px)));
209    test!(parse_6, "30px", TransformOrigin::new(Length::new(30.0, LengthUnit::Px), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px)));
210
211    test!(parse_7, "center left", TransformOrigin::new(Length::new(0.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px)));
212    test!(parse_8, "left center", TransformOrigin::new(Length::new(0.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px)));
213    test!(parse_9, "center bottom", TransformOrigin::new(Length::new(50.0, LengthUnit::Percent), Length::new(100.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px)));
214    test!(parse_10, "bottom center", TransformOrigin::new(Length::new(50.0, LengthUnit::Percent), Length::new(100.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px)));
215    test!(parse_11, "30%, center", TransformOrigin::new(Length::new(30.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px)));
216    test!(parse_12, " center, 30%", TransformOrigin::new(Length::new(50.0, LengthUnit::Percent), Length::new(30.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px)));
217    test!(parse_13, "left top", TransformOrigin::new(Length::new(0.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Percent), Length::new(0.0, LengthUnit::Px)));
218
219    test!(parse_14, "center right 3px", TransformOrigin::new(Length::new(100.0, LengthUnit::Percent), Length::new(50.0, LengthUnit::Percent), Length::new(3.0, LengthUnit::Px)));
220
221    macro_rules! test_err {
222        ($name:ident, $text:expr, $result:expr) => (
223            #[test]
224            fn $name() {
225                assert_eq!(TransformOrigin::from_str($text).unwrap_err().to_string(), $result);
226            }
227        )
228    }
229
230    test_err!(parse_err_1, "", "transform origin doesn't have enough parameters");
231    test_err!(parse_err_2, "some", "transform origin has invalid parameters");
232    test_err!(parse_err_3, "center some", "transform origin has invalid parameters");
233    test_err!(parse_err_4, "left right", "transform origin has invalid parameters");
234    test_err!(parse_err_5, "left top 3%", "z-index cannot be a percentage");
235}