Skip to main content

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