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