1 | // Copyright 2023 the SVG Types Authors |
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | |
4 | use crate::directional_position::DirectionalPosition; |
5 | use crate::stream::Stream; |
6 | use crate::{Length, LengthUnit}; |
7 | |
8 | #[derive (Clone, Copy, PartialEq, Debug)] |
9 | #[allow (missing_docs)] |
10 | enum Position { |
11 | Length(Length), |
12 | DirectionalPosition(DirectionalPosition), |
13 | } |
14 | |
15 | impl Position { |
16 | fn is_vertical(&self) -> bool { |
17 | match self { |
18 | Position::Length(_) => true, |
19 | Position::DirectionalPosition(dp: &DirectionalPosition) => dp.is_vertical(), |
20 | } |
21 | } |
22 | |
23 | fn is_horizontal(&self) -> bool { |
24 | match self { |
25 | Position::Length(_) => true, |
26 | Position::DirectionalPosition(dp: &DirectionalPosition) => dp.is_horizontal(), |
27 | } |
28 | } |
29 | } |
30 | |
31 | impl From<Position> for Length { |
32 | fn from(value: Position) -> Self { |
33 | match value { |
34 | Position::Length(l: Length) => l, |
35 | Position::DirectionalPosition(dp: DirectionalPosition) => 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)] |
44 | pub 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 | |
53 | impl 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)] |
67 | pub 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 | |
76 | impl 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 | |
92 | impl std::error::Error for TransformOriginError { |
93 | fn description(&self) -> &str { |
94 | "a transform origin parsing error" |
95 | } |
96 | } |
97 | |
98 | impl 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)] |
193 | mod 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 | } |
239 | |