| 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 | |