Skip to main content

style/values/specified/
easing.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 Easing functions.
6use crate::parser::{Parse, ParserContext};
7use crate::piecewise_linear::{PiecewiseLinearFunction, PiecewiseLinearFunctionBuilder};
8use crate::values::computed::easing::TimingFunction as ComputedTimingFunction;
9use crate::values::computed::{Context, ToComputedValue};
10use crate::values::generics::easing::TimingFunction as GenericTimingFunction;
11use crate::values::generics::easing::{StepPosition, TimingKeyword};
12use crate::values::specified::percentage::ToPercentage;
13use crate::values::specified::{AnimationName, Integer, Number, Percentage};
14use cssparser::{match_ignore_ascii_case, Delimiter, Parser, Token};
15use selectors::parser::SelectorParseErrorKind;
16use style_traits::{ParseError, StyleParseErrorKind};
17
18/// A specified timing function.
19pub type TimingFunction = GenericTimingFunction<Integer, Number, PiecewiseLinearFunction>;
20
21impl Parse for TimingFunction {
22    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
23        if let Ok(keyword) = input.try_parse(TimingKeyword::parse) {
24            return Ok(GenericTimingFunction::Keyword(keyword));
25        }
26        if let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) {
27            let position = match_ignore_ascii_case! { &ident,
28                "step-start" => StepPosition::Start,
29                "step-end" => StepPosition::End,
30                _ => {
31                    return Err(ParseError::custom(
32                        SelectorParseErrorKind::UnexpectedIdent
33                    ));
34                },
35            };
36            return Ok(GenericTimingFunction::Steps(Integer::new(1), position));
37        }
38        let function = input.expect_function()?.clone();
39        input.parse_nested_block(move |i| {
40            match_ignore_ascii_case! { &function,
41                "cubic-bezier" => Self::parse_cubic_bezier(context, i),
42                "steps" => Self::parse_steps(context, i),
43                "linear" => Self::parse_linear_function(context, i),
44                _ => Err(ParseError::custom(StyleParseErrorKind::UnexpectedFunction)),
45            }
46        })
47    }
48}
49
50impl TimingFunction {
51    fn parse_cubic_bezier(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
52        let x1 = Number::parse(context, input)?;
53        input.expect_comma()?;
54        let y1 = Number::parse(context, input)?;
55        input.expect_comma()?;
56        let x2 = Number::parse(context, input)?;
57        input.expect_comma()?;
58        let y2 = Number::parse(context, input)?;
59
60        // TODO(Bug 2037743) - Enable calc()-expressions that can only be resolved at
61        // computed value time (due to relative lengths, sibling-index(), etc.).
62        if let (Some(x1), Some(_), Some(x2), Some(_)) =
63            (x1.resolve(), y1.resolve(), x2.resolve(), y2.resolve())
64        {
65            if !(0.0..=1.0).contains(&x1) || !(0.0..=1.0).contains(&x2) {
66                return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
67            }
68        } else {
69            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
70        }
71
72        Ok(GenericTimingFunction::CubicBezier { x1, y1, x2, y2 })
73    }
74
75    fn parse_steps(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
76        let steps = Integer::parse_positive(context, input)?;
77        let position = input
78            .try_parse(|i| {
79                i.expect_comma()?;
80                StepPosition::parse(i)
81            })
82            .unwrap_or(StepPosition::End);
83
84        // TODO(Bug 2037743) - Enable calc()-expressions that can only be resolved at
85        // computed value time (due to relative lengths, sibling-index(), etc.).
86        let num_steps = match steps.resolve() {
87            Some(v) => v,
88            None => return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError)),
89        };
90
91        // jump-none accepts a positive integer greater than 1.
92        // FIXME(emilio): The spec asks us to avoid rejecting it at parse
93        // time except until computed value time.
94        //
95        // It's not totally clear it's worth it though, and no other browser
96        // does this.
97        if position == StepPosition::JumpNone && num_steps <= 1 {
98            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
99        }
100        Ok(GenericTimingFunction::Steps(steps, position))
101    }
102
103    fn parse_linear_function(
104        context: &ParserContext,
105        input: &mut Parser,
106    ) -> Result<Self, ParseError> {
107        let mut builder = PiecewiseLinearFunctionBuilder::default();
108        let mut num_specified_stops = 0;
109        // Closely follows `parse_comma_separated`, but can generate multiple entries for one comma-separated entry.
110        loop {
111            input.parse_until_before(Delimiter::Comma, |i| {
112                let builder = &mut builder;
113                let mut input_start = i.try_parse(|i| Percentage::parse(context, i)).ok();
114                let mut input_end = i.try_parse(|i| Percentage::parse(context, i)).ok();
115
116                let output = Number::parse(context, i)?;
117                if input_start.is_none() {
118                    debug_assert!(input_end.is_none(), "Input end parsed without input start?");
119                    input_start = i.try_parse(|i| Percentage::parse(context, i)).ok();
120                    input_end = i.try_parse(|i| Percentage::parse(context, i)).ok();
121                }
122
123                // TODO(Bug 2037743) - Enable calc()-expressions that can only be resolved at
124                // computed value time (due to relative lengths, sibling-index(), etc.).
125                let output = match output.resolve() {
126                    Some(v) => v,
127                    None => return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError)),
128                };
129                if matches!(input_start.as_ref().or(input_end.as_ref()), Some(p) if p.resolve().is_none()) {
130                    return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
131                }
132
133                let has_input_start = input_start.is_some();
134                builder.push(
135                    output,
136                    input_start.map(|v| v.to_percentage().unwrap()),
137                );
138                num_specified_stops += 1;
139                if input_end.is_some() {
140                    debug_assert!(has_input_start, "Input end valid but not input start?");
141                    builder.push(output, input_end.map(|v| v.to_percentage().unwrap()));
142                }
143
144                Ok(())
145            })?;
146
147            match input.next() {
148                Err(_) => break,
149                Ok(&Token::Comma) => continue,
150                Ok(_) => unreachable!(),
151            }
152        }
153        // By spec, specifying only a single stop makes the function invalid, even if that single entry may generate
154        // two entries.
155        if num_specified_stops < 2 {
156            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
157        }
158
159        Ok(GenericTimingFunction::LinearFunction(builder.build()))
160    }
161
162    /// Returns true if the name matches any keyword.
163    #[inline]
164    pub fn match_keywords(name: &AnimationName) -> bool {
165        if let Some(name) = name.as_atom() {
166            #[cfg(feature = "gecko")]
167            return name.with_str(|n| TimingKeyword::from_ident(n).is_ok());
168            #[cfg(feature = "servo")]
169            return TimingKeyword::from_ident(name).is_ok();
170        }
171        false
172    }
173}
174
175// We need this for converting the specified TimingFunction into computed TimingFunction without
176// Context (for some FFIs in glue.rs). In fact, we don't really need Context to get the computed
177// value of TimingFunction.
178impl TimingFunction {
179    /// Generate the ComputedTimingFunction without Context.
180    pub fn to_computed_value_without_context(&self) -> ComputedTimingFunction {
181        match &self {
182            GenericTimingFunction::Steps(steps, pos) => {
183                // Resolvable value was enforced at parse time
184                GenericTimingFunction::Steps(steps.resolve().unwrap(), *pos)
185            },
186            GenericTimingFunction::CubicBezier { x1, y1, x2, y2 } => {
187                // Resolvable value was enforced at parse time
188                GenericTimingFunction::CubicBezier {
189                    x1: x1.resolve().unwrap(),
190                    y1: y1.resolve().unwrap(),
191                    x2: x2.resolve().unwrap(),
192                    y2: y2.resolve().unwrap(),
193                }
194            },
195            GenericTimingFunction::Keyword(keyword) => GenericTimingFunction::Keyword(*keyword),
196            GenericTimingFunction::LinearFunction(function) => {
197                // Resolvable value was enforced at parse time
198                GenericTimingFunction::LinearFunction(function.clone())
199            },
200        }
201    }
202}
203
204impl ToComputedValue for TimingFunction {
205    type ComputedValue = ComputedTimingFunction;
206    fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
207        self.to_computed_value_without_context()
208    }
209
210    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
211        match &computed {
212            ComputedTimingFunction::Steps(steps, pos) => Self::Steps(Integer::new(*steps), *pos),
213            ComputedTimingFunction::CubicBezier { x1, y1, x2, y2 } => Self::CubicBezier {
214                x1: Number::new(*x1),
215                y1: Number::new(*y1),
216                x2: Number::new(*x2),
217                y2: Number::new(*y2),
218            },
219            ComputedTimingFunction::Keyword(keyword) => GenericTimingFunction::Keyword(*keyword),
220            ComputedTimingFunction::LinearFunction(function) => {
221                GenericTimingFunction::LinearFunction(function.clone())
222            },
223        }
224    }
225}