style/values/specified/
transform.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Specified types for CSS values that are related to transformations.
6
7use crate::parser::{Parse, ParserContext};
8use crate::values::computed::{Context, LengthPercentage as ComputedLengthPercentage};
9use crate::values::computed::{Percentage as ComputedPercentage, ToComputedValue};
10use crate::values::generics::transform as generic;
11use crate::values::generics::transform::{Matrix, Matrix3D};
12use crate::values::specified::position::{
13    HorizontalPositionKeyword, Side, VerticalPositionKeyword,
14};
15use crate::values::specified::{
16    self, Angle, Integer, Length, LengthPercentage, Number, NumberOrPercentage,
17};
18use crate::Zero;
19use cssparser::Parser;
20use style_traits::{ParseError, StyleParseErrorKind};
21
22pub use crate::values::generics::transform::TransformStyle;
23
24/// A single operation in a specified CSS `transform`
25pub type TransformOperation =
26    generic::TransformOperation<Angle, Number, Length, Integer, LengthPercentage>;
27
28/// A specified CSS `transform`
29pub type Transform = generic::Transform<TransformOperation>;
30
31/// The specified value of a CSS `<transform-origin>`
32pub type TransformOrigin = generic::TransformOrigin<
33    OriginComponent<HorizontalPositionKeyword>,
34    OriginComponent<VerticalPositionKeyword>,
35    Length,
36>;
37
38#[cfg(feature = "gecko")]
39fn all_transform_boxes_are_enabled(_context: &ParserContext) -> bool {
40    true
41}
42
43#[cfg(feature = "servo")]
44fn all_transform_boxes_are_enabled(_context: &ParserContext) -> bool {
45    false
46}
47
48/// The specified value of `transform-box`.
49/// https://drafts.csswg.org/css-transforms-1/#transform-box
50// Note: Once we ship everything, we can drop this and just use single_keyword for tranform-box.
51#[allow(missing_docs)]
52#[derive(
53    Animate,
54    Clone,
55    ComputeSquaredDistance,
56    Copy,
57    Debug,
58    Deserialize,
59    MallocSizeOf,
60    Parse,
61    PartialEq,
62    Serialize,
63    SpecifiedValueInfo,
64    ToAnimatedValue,
65    ToComputedValue,
66    ToCss,
67    ToResolvedValue,
68    ToShmem,
69)]
70#[repr(u8)]
71pub enum TransformBox {
72    #[parse(condition = "all_transform_boxes_are_enabled")]
73    ContentBox,
74    BorderBox,
75    FillBox,
76    #[parse(condition = "all_transform_boxes_are_enabled")]
77    StrokeBox,
78    ViewBox,
79}
80
81impl TransformOrigin {
82    /// Returns the initial specified value for `transform-origin`.
83    #[inline]
84    pub fn initial_value() -> Self {
85        Self::new(
86            OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))),
87            OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))),
88            Length::zero(),
89        )
90    }
91
92    /// Returns the `0 0` value.
93    pub fn zero_zero() -> Self {
94        Self::new(
95            OriginComponent::Length(LengthPercentage::zero()),
96            OriginComponent::Length(LengthPercentage::zero()),
97            Length::zero(),
98        )
99    }
100}
101
102impl Transform {
103    /// Internal parse function for deciding if we wish to accept prefixed values or not
104    ///
105    /// `transform` allows unitless zero angles as an exception, see:
106    /// https://github.com/w3c/csswg-drafts/issues/1162
107    fn parse_internal<'i, 't>(
108        context: &ParserContext,
109        input: &mut Parser<'i, 't>,
110    ) -> Result<Self, ParseError<'i>> {
111        use style_traits::{Separator, Space};
112
113        if input
114            .try_parse(|input| input.expect_ident_matching("none"))
115            .is_ok()
116        {
117            return Ok(generic::Transform::none());
118        }
119
120        Ok(generic::Transform(
121            Space::parse(input, |input| {
122                let function = input.expect_function()?.clone();
123                input.parse_nested_block(|input| {
124                    let location = input.current_source_location();
125                    let result = match_ignore_ascii_case! { &function,
126                        "matrix" => {
127                            let a = Number::parse(context, input)?;
128                            input.expect_comma()?;
129                            let b = Number::parse(context, input)?;
130                            input.expect_comma()?;
131                            let c = Number::parse(context, input)?;
132                            input.expect_comma()?;
133                            let d = Number::parse(context, input)?;
134                            input.expect_comma()?;
135                            // Standard matrix parsing.
136                            let e = Number::parse(context, input)?;
137                            input.expect_comma()?;
138                            let f = Number::parse(context, input)?;
139                            Ok(generic::TransformOperation::Matrix(Matrix { a, b, c, d, e, f }))
140                        },
141                        "matrix3d" => {
142                            let m11 = Number::parse(context, input)?;
143                            input.expect_comma()?;
144                            let m12 = Number::parse(context, input)?;
145                            input.expect_comma()?;
146                            let m13 = Number::parse(context, input)?;
147                            input.expect_comma()?;
148                            let m14 = Number::parse(context, input)?;
149                            input.expect_comma()?;
150                            let m21 = Number::parse(context, input)?;
151                            input.expect_comma()?;
152                            let m22 = Number::parse(context, input)?;
153                            input.expect_comma()?;
154                            let m23 = Number::parse(context, input)?;
155                            input.expect_comma()?;
156                            let m24 = Number::parse(context, input)?;
157                            input.expect_comma()?;
158                            let m31 = Number::parse(context, input)?;
159                            input.expect_comma()?;
160                            let m32 = Number::parse(context, input)?;
161                            input.expect_comma()?;
162                            let m33 = Number::parse(context, input)?;
163                            input.expect_comma()?;
164                            let m34 = Number::parse(context, input)?;
165                            input.expect_comma()?;
166                            // Standard matrix3d parsing.
167                            let m41 = Number::parse(context, input)?;
168                            input.expect_comma()?;
169                            let m42 = Number::parse(context, input)?;
170                            input.expect_comma()?;
171                            let m43 = Number::parse(context, input)?;
172                            input.expect_comma()?;
173                            let m44 = Number::parse(context, input)?;
174                            Ok(generic::TransformOperation::Matrix3D(Matrix3D {
175                                m11, m12, m13, m14,
176                                m21, m22, m23, m24,
177                                m31, m32, m33, m34,
178                                m41, m42, m43, m44,
179                            }))
180                        },
181                        "translate" => {
182                            let sx = specified::LengthPercentage::parse(context, input)?;
183                            if input.try_parse(|input| input.expect_comma()).is_ok() {
184                                let sy = specified::LengthPercentage::parse(context, input)?;
185                                Ok(generic::TransformOperation::Translate(sx, sy))
186                            } else {
187                                Ok(generic::TransformOperation::Translate(sx, Zero::zero()))
188                            }
189                        },
190                        "translatex" => {
191                            let tx = specified::LengthPercentage::parse(context, input)?;
192                            Ok(generic::TransformOperation::TranslateX(tx))
193                        },
194                        "translatey" => {
195                            let ty = specified::LengthPercentage::parse(context, input)?;
196                            Ok(generic::TransformOperation::TranslateY(ty))
197                        },
198                        "translatez" => {
199                            let tz = specified::Length::parse(context, input)?;
200                            Ok(generic::TransformOperation::TranslateZ(tz))
201                        },
202                        "translate3d" => {
203                            let tx = specified::LengthPercentage::parse(context, input)?;
204                            input.expect_comma()?;
205                            let ty = specified::LengthPercentage::parse(context, input)?;
206                            input.expect_comma()?;
207                            let tz = specified::Length::parse(context, input)?;
208                            Ok(generic::TransformOperation::Translate3D(tx, ty, tz))
209                        },
210                        "scale" => {
211                            let sx = NumberOrPercentage::parse(context, input)?.to_number();
212                            if input.try_parse(|input| input.expect_comma()).is_ok() {
213                                let sy = NumberOrPercentage::parse(context, input)?.to_number();
214                                Ok(generic::TransformOperation::Scale(sx, sy))
215                            } else {
216                                Ok(generic::TransformOperation::Scale(sx, sx))
217                            }
218                        },
219                        "scalex" => {
220                            let sx = NumberOrPercentage::parse(context, input)?.to_number();
221                            Ok(generic::TransformOperation::ScaleX(sx))
222                        },
223                        "scaley" => {
224                            let sy = NumberOrPercentage::parse(context, input)?.to_number();
225                            Ok(generic::TransformOperation::ScaleY(sy))
226                        },
227                        "scalez" => {
228                            let sz = NumberOrPercentage::parse(context, input)?.to_number();
229                            Ok(generic::TransformOperation::ScaleZ(sz))
230                        },
231                        "scale3d" => {
232                            let sx = NumberOrPercentage::parse(context, input)?.to_number();
233                            input.expect_comma()?;
234                            let sy = NumberOrPercentage::parse(context, input)?.to_number();
235                            input.expect_comma()?;
236                            let sz = NumberOrPercentage::parse(context, input)?.to_number();
237                            Ok(generic::TransformOperation::Scale3D(sx, sy, sz))
238                        },
239                        "rotate" => {
240                            let theta = specified::Angle::parse_with_unitless(context, input)?;
241                            Ok(generic::TransformOperation::Rotate(theta))
242                        },
243                        "rotatex" => {
244                            let theta = specified::Angle::parse_with_unitless(context, input)?;
245                            Ok(generic::TransformOperation::RotateX(theta))
246                        },
247                        "rotatey" => {
248                            let theta = specified::Angle::parse_with_unitless(context, input)?;
249                            Ok(generic::TransformOperation::RotateY(theta))
250                        },
251                        "rotatez" => {
252                            let theta = specified::Angle::parse_with_unitless(context, input)?;
253                            Ok(generic::TransformOperation::RotateZ(theta))
254                        },
255                        "rotate3d" => {
256                            let ax = Number::parse(context, input)?;
257                            input.expect_comma()?;
258                            let ay = Number::parse(context, input)?;
259                            input.expect_comma()?;
260                            let az = Number::parse(context, input)?;
261                            input.expect_comma()?;
262                            let theta = specified::Angle::parse_with_unitless(context, input)?;
263                            // TODO(gw): Check that the axis can be normalized.
264                            Ok(generic::TransformOperation::Rotate3D(ax, ay, az, theta))
265                        },
266                        "skew" => {
267                            let ax = specified::Angle::parse_with_unitless(context, input)?;
268                            if input.try_parse(|input| input.expect_comma()).is_ok() {
269                                let ay = specified::Angle::parse_with_unitless(context, input)?;
270                                Ok(generic::TransformOperation::Skew(ax, ay))
271                            } else {
272                                Ok(generic::TransformOperation::Skew(ax, Zero::zero()))
273                            }
274                        },
275                        "skewx" => {
276                            let theta = specified::Angle::parse_with_unitless(context, input)?;
277                            Ok(generic::TransformOperation::SkewX(theta))
278                        },
279                        "skewy" => {
280                            let theta = specified::Angle::parse_with_unitless(context, input)?;
281                            Ok(generic::TransformOperation::SkewY(theta))
282                        },
283                        "perspective" => {
284                            let p = match input.try_parse(|input| specified::Length::parse_non_negative(context, input)) {
285                                Ok(p) => generic::PerspectiveFunction::Length(p),
286                                Err(..) => {
287                                    input.expect_ident_matching("none")?;
288                                    generic::PerspectiveFunction::None
289                                }
290                            };
291                            Ok(generic::TransformOperation::Perspective(p))
292                        },
293                        _ => Err(()),
294                    };
295                    result.map_err(|()| {
296                        location.new_custom_error(StyleParseErrorKind::UnexpectedFunction(
297                            function.clone(),
298                        ))
299                    })
300                })
301            })?
302            .into(),
303        ))
304    }
305}
306
307impl Parse for Transform {
308    fn parse<'i, 't>(
309        context: &ParserContext,
310        input: &mut Parser<'i, 't>,
311    ) -> Result<Self, ParseError<'i>> {
312        Transform::parse_internal(context, input)
313    }
314}
315
316/// The specified value of a component of a CSS `<transform-origin>`.
317#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
318pub enum OriginComponent<S> {
319    /// `center`
320    Center,
321    /// `<length-percentage>`
322    Length(LengthPercentage),
323    /// `<side>`
324    Side(S),
325}
326
327impl Parse for TransformOrigin {
328    fn parse<'i, 't>(
329        context: &ParserContext,
330        input: &mut Parser<'i, 't>,
331    ) -> Result<Self, ParseError<'i>> {
332        let parse_depth = |input: &mut Parser| {
333            input
334                .try_parse(|i| Length::parse(context, i))
335                .unwrap_or(Length::zero())
336        };
337        match input.try_parse(|i| OriginComponent::parse(context, i)) {
338            Ok(x_origin @ OriginComponent::Center) => {
339                if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) {
340                    let depth = parse_depth(input);
341                    return Ok(Self::new(x_origin, y_origin, depth));
342                }
343                let y_origin = OriginComponent::Center;
344                if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) {
345                    let x_origin = OriginComponent::Side(x_keyword);
346                    let depth = parse_depth(input);
347                    return Ok(Self::new(x_origin, y_origin, depth));
348                }
349                let depth = Length::from_px(0.);
350                return Ok(Self::new(x_origin, y_origin, depth));
351            },
352            Ok(x_origin) => {
353                if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) {
354                    let depth = parse_depth(input);
355                    return Ok(Self::new(x_origin, y_origin, depth));
356                }
357                let y_origin = OriginComponent::Center;
358                let depth = Length::from_px(0.);
359                return Ok(Self::new(x_origin, y_origin, depth));
360            },
361            Err(_) => {},
362        }
363        let y_keyword = VerticalPositionKeyword::parse(input)?;
364        let y_origin = OriginComponent::Side(y_keyword);
365        if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) {
366            let x_origin = OriginComponent::Side(x_keyword);
367            let depth = parse_depth(input);
368            return Ok(Self::new(x_origin, y_origin, depth));
369        }
370        if input
371            .try_parse(|i| i.expect_ident_matching("center"))
372            .is_ok()
373        {
374            let x_origin = OriginComponent::Center;
375            let depth = parse_depth(input);
376            return Ok(Self::new(x_origin, y_origin, depth));
377        }
378        let x_origin = OriginComponent::Center;
379        let depth = Length::from_px(0.);
380        Ok(Self::new(x_origin, y_origin, depth))
381    }
382}
383
384impl<S> ToComputedValue for OriginComponent<S>
385where
386    S: Side,
387{
388    type ComputedValue = ComputedLengthPercentage;
389
390    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
391        match *self {
392            OriginComponent::Center => {
393                ComputedLengthPercentage::new_percent(ComputedPercentage(0.5))
394            },
395            OriginComponent::Length(ref length) => length.to_computed_value(context),
396            OriginComponent::Side(ref keyword) => {
397                let p = ComputedPercentage(if keyword.is_start() { 0. } else { 1. });
398                ComputedLengthPercentage::new_percent(p)
399            },
400        }
401    }
402
403    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
404        OriginComponent::Length(ToComputedValue::from_computed_value(computed))
405    }
406}
407
408impl<S> OriginComponent<S> {
409    /// `0%`
410    pub fn zero() -> Self {
411        OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage::zero()))
412    }
413}
414
415/// A specified CSS `rotate`
416pub type Rotate = generic::Rotate<Number, Angle>;
417
418impl Parse for Rotate {
419    fn parse<'i, 't>(
420        context: &ParserContext,
421        input: &mut Parser<'i, 't>,
422    ) -> Result<Self, ParseError<'i>> {
423        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
424            return Ok(generic::Rotate::None);
425        }
426
427        // Parse <angle> or [ x | y | z | <number>{3} ] && <angle>.
428        //
429        // The rotate axis and angle could be in any order, so we parse angle twice to cover
430        // two cases. i.e. `<number>{3} <angle>` or `<angle> <number>{3}`
431        let angle = input
432            .try_parse(|i| specified::Angle::parse(context, i))
433            .ok();
434        let axis = input
435            .try_parse(|i| {
436                Ok(try_match_ident_ignore_ascii_case! { i,
437                    "x" => (Number::new(1.), Number::new(0.), Number::new(0.)),
438                    "y" => (Number::new(0.), Number::new(1.), Number::new(0.)),
439                    "z" => (Number::new(0.), Number::new(0.), Number::new(1.)),
440                })
441            })
442            .or_else(|_: ParseError| -> Result<_, ParseError> {
443                input.try_parse(|i| {
444                    Ok((
445                        Number::parse(context, i)?,
446                        Number::parse(context, i)?,
447                        Number::parse(context, i)?,
448                    ))
449                })
450            })
451            .ok();
452        let angle = match angle {
453            Some(a) => a,
454            None => specified::Angle::parse(context, input)?,
455        };
456
457        Ok(match axis {
458            Some((x, y, z)) => generic::Rotate::Rotate3D(x, y, z, angle),
459            None => generic::Rotate::Rotate(angle),
460        })
461    }
462}
463
464/// A specified CSS `translate`
465pub type Translate = generic::Translate<LengthPercentage, Length>;
466
467impl Parse for Translate {
468    fn parse<'i, 't>(
469        context: &ParserContext,
470        input: &mut Parser<'i, 't>,
471    ) -> Result<Self, ParseError<'i>> {
472        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
473            return Ok(generic::Translate::None);
474        }
475
476        let tx = specified::LengthPercentage::parse(context, input)?;
477        if let Ok(ty) = input.try_parse(|i| specified::LengthPercentage::parse(context, i)) {
478            if let Ok(tz) = input.try_parse(|i| specified::Length::parse(context, i)) {
479                // 'translate: <length-percentage> <length-percentage> <length>'
480                return Ok(generic::Translate::Translate(tx, ty, tz));
481            }
482
483            // translate: <length-percentage> <length-percentage>'
484            return Ok(generic::Translate::Translate(
485                tx,
486                ty,
487                specified::Length::zero(),
488            ));
489        }
490
491        // 'translate: <length-percentage> '
492        Ok(generic::Translate::Translate(
493            tx,
494            specified::LengthPercentage::zero(),
495            specified::Length::zero(),
496        ))
497    }
498}
499
500/// A specified CSS `scale`
501pub type Scale = generic::Scale<Number>;
502
503impl Parse for Scale {
504    /// Scale accepts <number> | <percentage>, so we parse it as NumberOrPercentage,
505    /// and then convert into an Number if it's a Percentage.
506    /// https://github.com/w3c/csswg-drafts/pull/4396
507    fn parse<'i, 't>(
508        context: &ParserContext,
509        input: &mut Parser<'i, 't>,
510    ) -> Result<Self, ParseError<'i>> {
511        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
512            return Ok(generic::Scale::None);
513        }
514
515        let sx = NumberOrPercentage::parse(context, input)?.to_number();
516        if let Ok(sy) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) {
517            let sy = sy.to_number();
518            if let Ok(sz) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) {
519                // 'scale: <number> <number> <number>'
520                return Ok(generic::Scale::Scale(sx, sy, sz.to_number()));
521            }
522
523            // 'scale: <number> <number>'
524            return Ok(generic::Scale::Scale(sx, sy, Number::new(1.0)));
525        }
526
527        // 'scale: <number>'
528        Ok(generic::Scale::Scale(sx, sx, Number::new(1.0)))
529    }
530}