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