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