1use 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#[derive(Clone, Copy, PartialEq, Debug)]
44pub struct TransformOrigin {
45 pub x_offset: Length,
47 pub y_offset: Length,
49 pub z_offset: Length,
51}
52
53impl TransformOrigin {
54 #[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#[derive(Clone, Copy, Debug)]
67pub enum TransformOriginError {
68 MissingParameters,
70 InvalidParameters,
72 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 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}