style/values/specified/
animation.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 properties related to animations and transitions.
6
7use crate::parser::{Parse, ParserContext};
8use crate::properties::{NonCustomPropertyId, PropertyId, ShorthandId};
9use crate::values::generics::animation as generics;
10use crate::values::specified::{LengthPercentage, NonNegativeNumber, Time};
11use crate::values::{CustomIdent, DashedIdent, KeyframesName};
12use crate::Atom;
13use cssparser::Parser;
14use std::fmt::{self, Write};
15use style_traits::{
16    CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss,
17};
18
19/// A given transition property, that is either `All`, a longhand or shorthand
20/// property, or an unsupported or custom property.
21#[derive(
22    Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
23)]
24#[repr(u8)]
25pub enum TransitionProperty {
26    /// A non-custom property.
27    NonCustom(NonCustomPropertyId),
28    /// A custom property.
29    Custom(Atom),
30    /// Unrecognized property which could be any non-transitionable, custom property, or
31    /// unknown property.
32    Unsupported(CustomIdent),
33}
34
35impl ToCss for TransitionProperty {
36    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
37    where
38        W: Write,
39    {
40        match *self {
41            TransitionProperty::NonCustom(ref id) => id.to_css(dest),
42            TransitionProperty::Custom(ref name) => {
43                dest.write_str("--")?;
44                crate::values::serialize_atom_name(name, dest)
45            },
46            TransitionProperty::Unsupported(ref i) => i.to_css(dest),
47        }
48    }
49}
50
51impl Parse for TransitionProperty {
52    fn parse<'i, 't>(
53        context: &ParserContext,
54        input: &mut Parser<'i, 't>,
55    ) -> Result<Self, ParseError<'i>> {
56        let location = input.current_source_location();
57        let ident = input.expect_ident()?;
58
59        let id = match PropertyId::parse_ignoring_rule_type(&ident, context) {
60            Ok(id) => id,
61            Err(..) => {
62                // None is not acceptable as a single transition-property.
63                return Ok(TransitionProperty::Unsupported(CustomIdent::from_ident(
64                    location,
65                    ident,
66                    &["none"],
67                )?));
68            },
69        };
70
71        Ok(match id {
72            PropertyId::NonCustom(id) => TransitionProperty::NonCustom(id.unaliased()),
73            PropertyId::Custom(name) => TransitionProperty::Custom(name),
74        })
75    }
76}
77
78impl SpecifiedValueInfo for TransitionProperty {
79    fn collect_completion_keywords(f: KeywordsCollectFn) {
80        // `transition-property` can actually accept all properties and
81        // arbitrary identifiers, but `all` is a special one we'd like
82        // to list.
83        f(&["all"]);
84    }
85}
86
87impl TransitionProperty {
88    /// Returns the `none` value.
89    #[inline]
90    pub fn none() -> Self {
91        TransitionProperty::Unsupported(CustomIdent(atom!("none")))
92    }
93
94    /// Returns whether we're the `none` value.
95    #[inline]
96    pub fn is_none(&self) -> bool {
97        matches!(*self, TransitionProperty::Unsupported(ref ident) if ident.0 == atom!("none"))
98    }
99
100    /// Returns `all`.
101    #[inline]
102    pub fn all() -> Self {
103        TransitionProperty::NonCustom(NonCustomPropertyId::from_shorthand(ShorthandId::All))
104    }
105
106    /// Returns true if it is `all`.
107    #[inline]
108    pub fn is_all(&self) -> bool {
109        self == &TransitionProperty::NonCustom(NonCustomPropertyId::from_shorthand(
110            ShorthandId::All,
111        ))
112    }
113}
114
115/// A specified value for <transition-behavior-value>.
116///
117/// https://drafts.csswg.org/css-transitions-2/#transition-behavior-property
118#[derive(
119    Clone,
120    Copy,
121    Debug,
122    MallocSizeOf,
123    Parse,
124    PartialEq,
125    SpecifiedValueInfo,
126    ToComputedValue,
127    ToCss,
128    ToResolvedValue,
129    ToShmem,
130)]
131#[repr(u8)]
132pub enum TransitionBehavior {
133    /// Transitions will not be started for discrete properties, only for interpolable properties.
134    Normal,
135    /// Transitions will be started for discrete properties as well as interpolable properties.
136    AllowDiscrete,
137}
138
139impl TransitionBehavior {
140    /// Return normal, the initial value.
141    #[inline]
142    pub fn normal() -> Self {
143        Self::Normal
144    }
145
146    /// Return true if it is normal.
147    #[inline]
148    pub fn is_normal(&self) -> bool {
149        matches!(*self, Self::Normal)
150    }
151}
152
153/// A specified value for the `animation-duration` property.
154pub type AnimationDuration = generics::GenericAnimationDuration<Time>;
155
156impl Parse for AnimationDuration {
157    fn parse<'i, 't>(
158        context: &ParserContext,
159        input: &mut Parser<'i, 't>,
160    ) -> Result<Self, ParseError<'i>> {
161        if static_prefs::pref!("layout.css.scroll-driven-animations.enabled")
162            && input.try_parse(|i| i.expect_ident_matching("auto")).is_ok()
163        {
164            return Ok(Self::auto());
165        }
166
167        Time::parse_non_negative(context, input).map(AnimationDuration::Time)
168    }
169}
170
171/// https://drafts.csswg.org/css-animations/#animation-iteration-count
172#[derive(
173    Copy, Clone, Debug, MallocSizeOf, PartialEq, Parse, SpecifiedValueInfo, ToCss, ToShmem,
174)]
175pub enum AnimationIterationCount {
176    /// A `<number>` value.
177    Number(NonNegativeNumber),
178    /// The `infinite` keyword.
179    Infinite,
180}
181
182impl AnimationIterationCount {
183    /// Returns the value `1.0`.
184    #[inline]
185    pub fn one() -> Self {
186        Self::Number(NonNegativeNumber::new(1.0))
187    }
188
189    /// Returns true if it's `1.0`.
190    #[inline]
191    pub fn is_one(&self) -> bool {
192        *self == Self::one()
193    }
194}
195
196/// A value for the `animation-name` property.
197#[derive(
198    Clone,
199    Debug,
200    Eq,
201    Hash,
202    MallocSizeOf,
203    PartialEq,
204    SpecifiedValueInfo,
205    ToComputedValue,
206    ToCss,
207    ToResolvedValue,
208    ToShmem,
209)]
210#[value_info(other_values = "none")]
211#[repr(C)]
212pub struct AnimationName(pub KeyframesName);
213
214impl AnimationName {
215    /// Get the name of the animation as an `Atom`.
216    pub fn as_atom(&self) -> Option<&Atom> {
217        if self.is_none() {
218            return None;
219        }
220        Some(self.0.as_atom())
221    }
222
223    /// Returns the `none` value.
224    pub fn none() -> Self {
225        AnimationName(KeyframesName::none())
226    }
227
228    /// Returns whether this is the none value.
229    pub fn is_none(&self) -> bool {
230        self.0.is_none()
231    }
232}
233
234impl Parse for AnimationName {
235    fn parse<'i, 't>(
236        context: &ParserContext,
237        input: &mut Parser<'i, 't>,
238    ) -> Result<Self, ParseError<'i>> {
239        if let Ok(name) = input.try_parse(|input| KeyframesName::parse(context, input)) {
240            return Ok(AnimationName(name));
241        }
242
243        input.expect_ident_matching("none")?;
244        Ok(AnimationName(KeyframesName::none()))
245    }
246}
247
248/// https://drafts.csswg.org/css-animations/#propdef-animation-direction
249#[derive(
250    Copy,
251    Clone,
252    Debug,
253    MallocSizeOf,
254    Parse,
255    PartialEq,
256    SpecifiedValueInfo,
257    ToComputedValue,
258    ToCss,
259    ToResolvedValue,
260    ToShmem,
261)]
262#[repr(u8)]
263#[allow(missing_docs)]
264pub enum AnimationDirection {
265    Normal,
266    Reverse,
267    Alternate,
268    AlternateReverse,
269}
270
271impl AnimationDirection {
272    /// Returns true if the name matches any animation-direction keyword.
273    #[inline]
274    pub fn match_keywords(name: &AnimationName) -> bool {
275        if let Some(name) = name.as_atom() {
276            #[cfg(feature = "gecko")]
277            return name.with_str(|n| Self::from_ident(n).is_ok());
278            #[cfg(feature = "servo")]
279            return Self::from_ident(name).is_ok();
280        }
281        false
282    }
283}
284
285/// https://drafts.csswg.org/css-animations/#animation-play-state
286#[derive(
287    Copy,
288    Clone,
289    Debug,
290    MallocSizeOf,
291    Parse,
292    PartialEq,
293    SpecifiedValueInfo,
294    ToComputedValue,
295    ToCss,
296    ToResolvedValue,
297    ToShmem,
298)]
299#[repr(u8)]
300#[allow(missing_docs)]
301pub enum AnimationPlayState {
302    Running,
303    Paused,
304}
305
306impl AnimationPlayState {
307    /// Returns true if the name matches any animation-play-state keyword.
308    #[inline]
309    pub fn match_keywords(name: &AnimationName) -> bool {
310        if let Some(name) = name.as_atom() {
311            #[cfg(feature = "gecko")]
312            return name.with_str(|n| Self::from_ident(n).is_ok());
313            #[cfg(feature = "servo")]
314            return Self::from_ident(name).is_ok();
315        }
316        false
317    }
318}
319
320/// https://drafts.csswg.org/css-animations/#propdef-animation-fill-mode
321#[derive(
322    Copy,
323    Clone,
324    Debug,
325    MallocSizeOf,
326    Parse,
327    PartialEq,
328    SpecifiedValueInfo,
329    ToComputedValue,
330    ToCss,
331    ToResolvedValue,
332    ToShmem,
333)]
334#[repr(u8)]
335#[allow(missing_docs)]
336pub enum AnimationFillMode {
337    None,
338    Forwards,
339    Backwards,
340    Both,
341}
342
343impl AnimationFillMode {
344    /// Returns true if the name matches any animation-fill-mode keyword.
345    /// Note: animation-name:none is its initial value, so we don't have to match none here.
346    #[inline]
347    pub fn match_keywords(name: &AnimationName) -> bool {
348        if let Some(name) = name.as_atom() {
349            #[cfg(feature = "gecko")]
350            return name.with_str(|n| Self::from_ident(n).is_ok());
351            #[cfg(feature = "servo")]
352            return Self::from_ident(name).is_ok();
353        }
354        false
355    }
356}
357
358/// https://drafts.csswg.org/css-animations-2/#animation-composition
359#[derive(
360    Copy,
361    Clone,
362    Debug,
363    MallocSizeOf,
364    Parse,
365    PartialEq,
366    SpecifiedValueInfo,
367    ToComputedValue,
368    ToCss,
369    ToResolvedValue,
370    ToShmem,
371)]
372#[repr(u8)]
373#[allow(missing_docs)]
374pub enum AnimationComposition {
375    Replace,
376    Add,
377    Accumulate,
378}
379
380/// A value for the <Scroller> used in scroll().
381///
382/// https://drafts.csswg.org/scroll-animations-1/rewrite#typedef-scroller
383#[derive(
384    Copy,
385    Clone,
386    Debug,
387    Eq,
388    Hash,
389    MallocSizeOf,
390    Parse,
391    PartialEq,
392    SpecifiedValueInfo,
393    ToComputedValue,
394    ToCss,
395    ToResolvedValue,
396    ToShmem,
397)]
398#[repr(u8)]
399pub enum Scroller {
400    /// The nearest ancestor scroll container. (Default.)
401    Nearest,
402    /// The document viewport as the scroll container.
403    Root,
404    /// Specifies to use the element’s own principal box as the scroll container.
405    #[css(keyword = "self")]
406    SelfElement,
407}
408
409impl Scroller {
410    /// Returns true if it is default.
411    #[inline]
412    fn is_default(&self) -> bool {
413        matches!(*self, Self::Nearest)
414    }
415}
416
417impl Default for Scroller {
418    fn default() -> Self {
419        Self::Nearest
420    }
421}
422
423/// A value for the <Axis> used in scroll(), or a value for {scroll|view}-timeline-axis.
424///
425/// https://drafts.csswg.org/scroll-animations-1/#typedef-axis
426/// https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis
427/// https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis
428#[derive(
429    Copy,
430    Clone,
431    Debug,
432    Eq,
433    Hash,
434    MallocSizeOf,
435    Parse,
436    PartialEq,
437    SpecifiedValueInfo,
438    ToComputedValue,
439    ToCss,
440    ToResolvedValue,
441    ToShmem,
442)]
443#[repr(u8)]
444pub enum ScrollAxis {
445    /// The block axis of the scroll container. (Default.)
446    Block = 0,
447    /// The inline axis of the scroll container.
448    Inline = 1,
449    /// The horizontal axis of the scroll container.
450    X = 2,
451    /// The vertical axis of the scroll container.
452    Y = 3,
453}
454
455impl ScrollAxis {
456    /// Returns true if it is default.
457    #[inline]
458    pub fn is_default(&self) -> bool {
459        matches!(*self, Self::Block)
460    }
461}
462
463impl Default for ScrollAxis {
464    fn default() -> Self {
465        Self::Block
466    }
467}
468
469/// The scroll() notation.
470/// https://drafts.csswg.org/scroll-animations-1/#scroll-notation
471#[derive(
472    Copy,
473    Clone,
474    Debug,
475    MallocSizeOf,
476    PartialEq,
477    SpecifiedValueInfo,
478    ToComputedValue,
479    ToCss,
480    ToResolvedValue,
481    ToShmem,
482)]
483#[css(function = "scroll")]
484#[repr(C)]
485pub struct ScrollFunction {
486    /// The scroll container element whose scroll position drives the progress of the timeline.
487    #[css(skip_if = "Scroller::is_default")]
488    pub scroller: Scroller,
489    /// The axis of scrolling that drives the progress of the timeline.
490    #[css(skip_if = "ScrollAxis::is_default")]
491    pub axis: ScrollAxis,
492}
493
494impl ScrollFunction {
495    /// Parse the inner function arguments of `scroll()`.
496    fn parse_arguments<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
497        // <scroll()> = scroll( [ <scroller> || <axis> ]? )
498        // https://drafts.csswg.org/scroll-animations-1/#funcdef-scroll
499        let mut scroller = None;
500        let mut axis = None;
501        loop {
502            if scroller.is_none() {
503                scroller = input.try_parse(Scroller::parse).ok();
504            }
505
506            if axis.is_none() {
507                axis = input.try_parse(ScrollAxis::parse).ok();
508                if axis.is_some() {
509                    continue;
510                }
511            }
512            break;
513        }
514
515        Ok(Self {
516            scroller: scroller.unwrap_or_default(),
517            axis: axis.unwrap_or_default(),
518        })
519    }
520}
521
522impl generics::ViewFunction<LengthPercentage> {
523    /// Parse the inner function arguments of `view()`.
524    fn parse_arguments<'i, 't>(
525        context: &ParserContext,
526        input: &mut Parser<'i, 't>,
527    ) -> Result<Self, ParseError<'i>> {
528        // <view()> = view( [ <axis> || <'view-timeline-inset'> ]? )
529        // https://drafts.csswg.org/scroll-animations-1/#funcdef-view
530        let mut axis = None;
531        let mut inset = None;
532        loop {
533            if axis.is_none() {
534                axis = input.try_parse(ScrollAxis::parse).ok();
535            }
536
537            if inset.is_none() {
538                inset = input
539                    .try_parse(|i| ViewTimelineInset::parse(context, i))
540                    .ok();
541                if inset.is_some() {
542                    continue;
543                }
544            }
545            break;
546        }
547
548        Ok(Self {
549            inset: inset.unwrap_or_default(),
550            axis: axis.unwrap_or_default(),
551        })
552    }
553}
554
555/// The typedef of scroll-timeline-name or view-timeline-name.
556///
557/// https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-name
558/// https://drafts.csswg.org/scroll-animations-1/#view-timeline-name
559#[derive(
560    Clone,
561    Debug,
562    Eq,
563    Hash,
564    MallocSizeOf,
565    PartialEq,
566    SpecifiedValueInfo,
567    ToComputedValue,
568    ToResolvedValue,
569    ToShmem,
570)]
571#[repr(C)]
572pub struct TimelineName(DashedIdent);
573
574impl TimelineName {
575    /// Returns the `none` value.
576    pub fn none() -> Self {
577        Self(DashedIdent::empty())
578    }
579
580    /// Check if this is `none` value.
581    pub fn is_none(&self) -> bool {
582        self.0.is_empty()
583    }
584}
585
586impl Parse for TimelineName {
587    fn parse<'i, 't>(
588        context: &ParserContext,
589        input: &mut Parser<'i, 't>,
590    ) -> Result<Self, ParseError<'i>> {
591        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
592            return Ok(Self::none());
593        }
594
595        DashedIdent::parse(context, input).map(TimelineName)
596    }
597}
598
599impl ToCss for TimelineName {
600    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
601    where
602        W: Write,
603    {
604        if self.is_none() {
605            return dest.write_str("none");
606        }
607
608        self.0.to_css(dest)
609    }
610}
611
612/// A specified value for the `animation-timeline` property.
613pub type AnimationTimeline = generics::GenericAnimationTimeline<LengthPercentage>;
614
615impl Parse for AnimationTimeline {
616    fn parse<'i, 't>(
617        context: &ParserContext,
618        input: &mut Parser<'i, 't>,
619    ) -> Result<Self, ParseError<'i>> {
620        use crate::values::generics::animation::ViewFunction;
621
622        // <single-animation-timeline> = auto | none | <dashed-ident> | <scroll()> | <view()>
623        // https://drafts.csswg.org/css-animations-2/#typedef-single-animation-timeline
624
625        if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() {
626            return Ok(Self::Auto);
627        }
628
629        // This parses none or <dashed-indent>.
630        if let Ok(name) = input.try_parse(|i| TimelineName::parse(context, i)) {
631            return Ok(AnimationTimeline::Timeline(name));
632        }
633
634        // Parse <scroll()> or <view()>.
635        let location = input.current_source_location();
636        let function = input.expect_function()?.clone();
637        input.parse_nested_block(move |i| {
638            match_ignore_ascii_case! { &function,
639                "scroll" => ScrollFunction::parse_arguments(i).map(Self::Scroll),
640                "view" => ViewFunction::parse_arguments(context, i).map(Self::View),
641                _ => {
642                    Err(location.new_custom_error(
643                        StyleParseErrorKind::UnexpectedFunction(function.clone())
644                    ))
645                },
646            }
647        })
648    }
649}
650
651/// A specified value for the `view-timeline-inset` property.
652pub type ViewTimelineInset = generics::GenericViewTimelineInset<LengthPercentage>;
653
654impl Parse for ViewTimelineInset {
655    fn parse<'i, 't>(
656        context: &ParserContext,
657        input: &mut Parser<'i, 't>,
658    ) -> Result<Self, ParseError<'i>> {
659        use crate::values::specified::LengthPercentageOrAuto;
660
661        let start = LengthPercentageOrAuto::parse(context, input)?;
662        let end = match input.try_parse(|input| LengthPercentageOrAuto::parse(context, input)) {
663            Ok(end) => end,
664            Err(_) => start.clone(),
665        };
666
667        Ok(Self { start, end })
668    }
669}
670
671/// The view-transition-name: `none | <custom-ident> | match-element`.
672///
673/// https://drafts.csswg.org/css-view-transitions-1/#view-transition-name-prop
674/// https://drafts.csswg.org/css-view-transitions-2/#auto-vt-name
675// TODO: auto keyword.
676#[derive(
677    Clone,
678    Debug,
679    Eq,
680    Hash,
681    PartialEq,
682    MallocSizeOf,
683    SpecifiedValueInfo,
684    ToComputedValue,
685    ToResolvedValue,
686    ToShmem,
687)]
688#[repr(C, u8)]
689pub enum ViewTransitionName {
690    /// None keyword.
691    None,
692    /// match-element keyword.
693    /// https://drafts.csswg.org/css-view-transitions-2/#auto-vt-name
694    MatchElement,
695    /// A `<custom-ident>`.
696    Ident(Atom),
697}
698
699impl ViewTransitionName {
700    /// Returns the `none` value.
701    pub fn none() -> Self {
702        Self::None
703    }
704}
705
706impl Parse for ViewTransitionName {
707    fn parse<'i, 't>(
708        _: &ParserContext,
709        input: &mut Parser<'i, 't>,
710    ) -> Result<Self, ParseError<'i>> {
711        let location = input.current_source_location();
712        let ident = input.expect_ident()?;
713        if ident.eq_ignore_ascii_case("none") {
714            return Ok(Self::none());
715        }
716
717        if ident.eq_ignore_ascii_case("match-element") {
718            return Ok(Self::MatchElement);
719        }
720
721        // We check none already, so don't need to exclude none here.
722        // Note: "auto" is not supported yet so we exclude it.
723        CustomIdent::from_ident(location, ident, &["auto"]).map(|i| Self::Ident(i.0))
724    }
725}
726
727impl ToCss for ViewTransitionName {
728    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
729    where
730        W: Write,
731    {
732        use crate::values::serialize_atom_identifier;
733        match *self {
734            Self::None => dest.write_str("none"),
735            Self::MatchElement => dest.write_str("match-element"),
736            Self::Ident(ref ident) => serialize_atom_identifier(ident, dest),
737        }
738    }
739}
740
741/// The view-transition-class: `none | <custom-ident>+`.
742///
743/// https://drafts.csswg.org/css-view-transitions-2/#view-transition-class-prop
744///
745/// Empty slice represents `none`.
746#[derive(
747    Clone,
748    Debug,
749    Eq,
750    Hash,
751    PartialEq,
752    MallocSizeOf,
753    SpecifiedValueInfo,
754    ToComputedValue,
755    ToCss,
756    ToResolvedValue,
757    ToShmem,
758)]
759#[repr(C)]
760#[value_info(other_values = "none")]
761pub struct ViewTransitionClass(
762    #[css(iterable, if_empty = "none")]
763    #[ignore_malloc_size_of = "Arc"]
764    crate::ArcSlice<CustomIdent>,
765);
766
767impl ViewTransitionClass {
768    /// Returns the default value, `none`. We use the default slice (i.e. empty) to represent it.
769    pub fn none() -> Self {
770        Self(Default::default())
771    }
772
773    /// Returns whether this is the `none` value.
774    pub fn is_none(&self) -> bool {
775        self.0.is_empty()
776    }
777}
778
779impl Parse for ViewTransitionClass {
780    fn parse<'i, 't>(
781        _: &ParserContext,
782        input: &mut Parser<'i, 't>,
783    ) -> Result<Self, ParseError<'i>> {
784        use style_traits::{Separator, Space};
785
786        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
787            return Ok(Self::none());
788        }
789
790        Ok(Self(crate::ArcSlice::from_iter(
791            Space::parse(input, |i| CustomIdent::parse(i, &["none"]))?.into_iter(),
792        )))
793    }
794}