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