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: &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
28impl 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)]
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}
236