svgtypes/
transform_origin.rs

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