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