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<'i, 't>(
120        context: &ParserContext,
121        input: &mut Parser<'i, 't>,
122    ) -> Result<Self, ParseError<'i>> {
123        Self::parse_internal(context, input, AllowUnitlessPerspective::Yes)
124    }
125    /// Internal parse function for deciding if we wish to accept prefixed values or not
126    ///
127    /// `transform` allows unitless zero angles as an exception, see:
128    /// https://github.com/w3c/csswg-drafts/issues/1162
129    fn parse_internal<'i, 't>(
130        context: &ParserContext,
131        input: &mut Parser<'i, 't>,
132        allow_unitless_perspective: AllowUnitlessPerspective,
133    ) -> Result<Self, ParseError<'i>> {
134        use style_traits::{Separator, Space};
135
136        if input
137            .try_parse(|input| input.expect_ident_matching("none"))
138            .is_ok()
139        {
140            return Ok(generic::Transform::none());
141        }
142
143        Ok(generic::Transform(
144            Space::parse(input, |input| {
145                let function = input.expect_function()?.clone();
146                input.parse_nested_block(|input| {
147                    let location = input.current_source_location();
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(input.new_custom_error(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(input.new_custom_error(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(input.new_custom_error(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(input.new_custom_error(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(input.new_custom_error(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(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
269                            };
270                            input.expect_comma()?;
271                            let Some(sy) = NumberOrPercentage::parse(context, input)?.to_number() else {
272                                return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
273                            };
274                            input.expect_comma()?;
275                            let Some(sz) = NumberOrPercentage::parse(context, input)?.to_number() else {
276                                return Err(input.new_custom_error(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                        location.new_custom_error(StyleParseErrorKind::UnexpectedFunction(
344                            function.clone(),
345                        ))
346                    })
347                })
348            })?
349            .into(),
350        ))
351    }
352}
353
354impl Parse for Transform {
355    fn parse<'i, 't>(
356        context: &ParserContext,
357        input: &mut Parser<'i, 't>,
358    ) -> Result<Self, ParseError<'i>> {
359        Transform::parse_internal(context, input, AllowUnitlessPerspective::No)
360    }
361}
362
363/// The specified value of a component of a CSS `<transform-origin>`.
364#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
365pub enum OriginComponent<S> {
366    /// `center`
367    Center,
368    /// `<length-percentage>`
369    Length(LengthPercentage),
370    /// `<side>`
371    Side(S),
372}
373
374impl Parse for TransformOrigin {
375    fn parse<'i, 't>(
376        context: &ParserContext,
377        input: &mut Parser<'i, 't>,
378    ) -> Result<Self, ParseError<'i>> {
379        let parse_depth = |input: &mut Parser| {
380            input
381                .try_parse(|i| Length::parse(context, i))
382                .unwrap_or(Length::zero())
383        };
384        match input.try_parse(|i| OriginComponent::parse(context, i)) {
385            Ok(x_origin @ OriginComponent::Center) => {
386                if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) {
387                    let depth = parse_depth(input);
388                    return Ok(Self::new(x_origin, y_origin, depth));
389                }
390                let y_origin = OriginComponent::Center;
391                if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) {
392                    let x_origin = OriginComponent::Side(x_keyword);
393                    let depth = parse_depth(input);
394                    return Ok(Self::new(x_origin, y_origin, depth));
395                }
396                let depth = Length::from_px(0.);
397                return Ok(Self::new(x_origin, y_origin, depth));
398            },
399            Ok(x_origin) => {
400                if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) {
401                    let depth = parse_depth(input);
402                    return Ok(Self::new(x_origin, y_origin, depth));
403                }
404                let y_origin = OriginComponent::Center;
405                let depth = Length::from_px(0.);
406                return Ok(Self::new(x_origin, y_origin, depth));
407            },
408            Err(_) => {},
409        }
410        let y_keyword = VerticalPositionKeyword::parse(input)?;
411        let y_origin = OriginComponent::Side(y_keyword);
412        if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) {
413            let x_origin = OriginComponent::Side(x_keyword);
414            let depth = parse_depth(input);
415            return Ok(Self::new(x_origin, y_origin, depth));
416        }
417        if input
418            .try_parse(|i| i.expect_ident_matching("center"))
419            .is_ok()
420        {
421            let x_origin = OriginComponent::Center;
422            let depth = parse_depth(input);
423            return Ok(Self::new(x_origin, y_origin, depth));
424        }
425        let x_origin = OriginComponent::Center;
426        let depth = Length::from_px(0.);
427        Ok(Self::new(x_origin, y_origin, depth))
428    }
429}
430
431impl<S> ToComputedValue for OriginComponent<S>
432where
433    S: Side,
434{
435    type ComputedValue = ComputedLengthPercentage;
436
437    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
438        match *self {
439            OriginComponent::Center => {
440                ComputedLengthPercentage::new_percent(ComputedPercentage(0.5))
441            },
442            OriginComponent::Length(ref length) => length.to_computed_value(context),
443            OriginComponent::Side(ref keyword) => {
444                let p = ComputedPercentage(if keyword.is_start() { 0. } else { 1. });
445                ComputedLengthPercentage::new_percent(p)
446            },
447        }
448    }
449
450    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
451        OriginComponent::Length(ToComputedValue::from_computed_value(computed))
452    }
453}
454
455impl<S> OriginComponent<S> {
456    /// `0%`
457    pub fn zero() -> Self {
458        OriginComponent::Length(LengthPercentage::Percentage(NoCalcPercentage::new(0.)))
459    }
460}
461
462/// A specified CSS `rotate`
463pub type Rotate = generic::Rotate<Number, Angle>;
464
465impl Parse for Rotate {
466    fn parse<'i, 't>(
467        context: &ParserContext,
468        input: &mut Parser<'i, 't>,
469    ) -> Result<Self, ParseError<'i>> {
470        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
471            return Ok(generic::Rotate::None);
472        }
473
474        // Parse <angle> or [ x | y | z | <number>{3} ] && <angle>.
475        //
476        // The rotate axis and angle could be in any order, so we parse angle twice to cover
477        // two cases. i.e. `<number>{3} <angle>` or `<angle> <number>{3}`
478        let angle = input
479            .try_parse(|i| specified::Angle::parse(context, i))
480            .ok();
481        let axis = input
482            .try_parse(|i| {
483                Ok(try_match_ident_ignore_ascii_case! { i,
484                    "x" => (Number::new(1.), Number::new(0.), Number::new(0.)),
485                    "y" => (Number::new(0.), Number::new(1.), Number::new(0.)),
486                    "z" => (Number::new(0.), Number::new(0.), Number::new(1.)),
487                })
488            })
489            .or_else(|_: ParseError| -> Result<_, ParseError> {
490                input.try_parse(|i| {
491                    Ok((
492                        Number::parse(context, i)?,
493                        Number::parse(context, i)?,
494                        Number::parse(context, i)?,
495                    ))
496                })
497            })
498            .ok();
499        let angle = match angle {
500            Some(a) => a,
501            None => specified::Angle::parse(context, input)?,
502        };
503
504        Ok(match axis {
505            Some((x, y, z)) => generic::Rotate::Rotate3D(x, y, z, angle),
506            None => generic::Rotate::Rotate(angle),
507        })
508    }
509}
510
511/// A specified CSS `translate`
512pub type Translate = generic::Translate<LengthPercentage, Length>;
513
514impl Parse for Translate {
515    fn parse<'i, 't>(
516        context: &ParserContext,
517        input: &mut Parser<'i, 't>,
518    ) -> Result<Self, ParseError<'i>> {
519        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
520            return Ok(generic::Translate::None);
521        }
522
523        let tx = specified::LengthPercentage::parse(context, input)?;
524        if let Ok(ty) = input.try_parse(|i| specified::LengthPercentage::parse(context, i)) {
525            if let Ok(tz) = input.try_parse(|i| specified::Length::parse(context, i)) {
526                // 'translate: <length-percentage> <length-percentage> <length>'
527                return Ok(generic::Translate::Translate(tx, ty, tz));
528            }
529
530            // translate: <length-percentage> <length-percentage>'
531            return Ok(generic::Translate::Translate(
532                tx,
533                ty,
534                specified::Length::zero(),
535            ));
536        }
537
538        // 'translate: <length-percentage> '
539        Ok(generic::Translate::Translate(
540            tx,
541            specified::LengthPercentage::zero(),
542            specified::Length::zero(),
543        ))
544    }
545}
546
547/// An individual value that can appear in a CSS `scale`.
548#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped)]
549pub struct ScaleFactor(NumberOrPercentage);
550
551impl ScaleFactor {
552    fn one() -> Self {
553        ScaleFactor(NumberOrPercentage::Number(Number::new(1.0)))
554    }
555}
556
557impl Parse for ScaleFactor {
558    /// Scale accepts <number> | <percentage>, so we parse it as NumberOrPercentage,
559    /// and then convert into an Number if it's a non-calc Percentage.
560    /// https://github.com/w3c/csswg-drafts/pull/4396
561    fn parse<'i, 't>(
562        context: &ParserContext,
563        input: &mut Parser<'i, 't>,
564    ) -> Result<Self, ParseError<'i>> {
565        Ok(ScaleFactor(
566            NumberOrPercentage::parse(context, input)?.into_simplified_number(),
567        ))
568    }
569}
570
571impl ToComputedValue for ScaleFactor {
572    type ComputedValue = <Number as ToComputedValue>::ComputedValue;
573
574    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
575        self.0.to_computed_value(context).value()
576    }
577
578    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
579        ScaleFactor(NumberOrPercentage::Number(Number::from_computed_value(
580            computed,
581        )))
582    }
583}
584
585impl ToFloat for ScaleFactor {
586    fn to_f32(&self) -> Result<f32, ()> {
587        match &self.0 {
588            NumberOrPercentage::Number(n) => n.resolve().ok_or(()),
589            NumberOrPercentage::Percentage(p) => p.resolve().ok_or(()),
590        }
591    }
592
593    fn to_f64(&self) -> Result<f64, ()> {
594        self.to_f32().map(|v| v as f64)
595    }
596}
597
598/// A specified CSS `scale`
599pub type Scale = generic::Scale<ScaleFactor>;
600
601impl Parse for Scale {
602    fn parse<'i, 't>(
603        context: &ParserContext,
604        input: &mut Parser<'i, 't>,
605    ) -> Result<Self, ParseError<'i>> {
606        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
607            return Ok(generic::Scale::None);
608        }
609
610        let sx = ScaleFactor::parse(context, input)?;
611        if let Ok(sy) = input.try_parse(|i| ScaleFactor::parse(context, i)) {
612            if let Ok(sz) = input.try_parse(|i| ScaleFactor::parse(context, i)) {
613                return Ok(generic::Scale::Scale(sx, sy, sz));
614            }
615            return Ok(generic::Scale::Scale(sx, sy, ScaleFactor::one()));
616        }
617        Ok(generic::Scale::Scale(sx.clone(), sx, ScaleFactor::one()))
618    }
619}