1 | use crate::directional_position::DirectionalPosition; |
2 | use crate::stream::Stream; |
3 | use crate::{Length, LengthUnit}; |
4 | |
5 | #[derive (Clone, Copy, PartialEq, Debug)] |
6 | #[allow (missing_docs)] |
7 | enum Position { |
8 | Length(Length), |
9 | DirectionalPosition(DirectionalPosition), |
10 | } |
11 | |
12 | impl Position { |
13 | fn is_vertical(&self) -> bool { |
14 | match self { |
15 | Position::Length(_) => true, |
16 | Position::DirectionalPosition(dp: &DirectionalPosition) => dp.is_vertical(), |
17 | } |
18 | } |
19 | |
20 | fn is_horizontal(&self) -> bool { |
21 | match self { |
22 | Position::Length(_) => true, |
23 | Position::DirectionalPosition(dp: &DirectionalPosition) => dp.is_horizontal(), |
24 | } |
25 | } |
26 | } |
27 | |
28 | impl From<Position> for Length { |
29 | fn from(value: Position) -> Self { |
30 | match value { |
31 | Position::Length(l: Length) => l, |
32 | Position::DirectionalPosition(dp: DirectionalPosition) => 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)] |
41 | pub 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 | |
50 | impl 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)] |
64 | pub 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 | |
73 | impl 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 | |
89 | impl std::error::Error for TransformOriginError { |
90 | fn description(&self) -> &str { |
91 | "a transform origin parsing error" |
92 | } |
93 | } |
94 | |
95 | impl 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)] |
190 | mod 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 | } |
236 | |