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