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, ToTyped,
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    ToTyped,
210)]
211#[value_info(other_values = "none")]
212#[repr(C)]
213pub struct AnimationName(pub KeyframesName);
214
215impl AnimationName {
216    /// Get the name of the animation as an `Atom`.
217    pub fn as_atom(&self) -> Option<&Atom> {
218        if self.is_none() {
219            return None;
220        }
221        Some(self.0.as_atom())
222    }
223
224    /// Returns the `none` value.
225    pub fn none() -> Self {
226        AnimationName(KeyframesName::none())
227    }
228
229    /// Returns whether this is the none value.
230    pub fn is_none(&self) -> bool {
231        self.0.is_none()
232    }
233}
234
235impl Parse for AnimationName {
236    fn parse<'i, 't>(
237        context: &ParserContext,
238        input: &mut Parser<'i, 't>,
239    ) -> Result<Self, ParseError<'i>> {
240        if let Ok(name) = input.try_parse(|input| KeyframesName::parse(context, input)) {
241            return Ok(AnimationName(name));
242        }
243
244        input.expect_ident_matching("none")?;
245        Ok(AnimationName(KeyframesName::none()))
246    }
247}
248
249/// https://drafts.csswg.org/css-animations/#propdef-animation-direction
250#[derive(
251    Copy,
252    Clone,
253    Debug,
254    MallocSizeOf,
255    Parse,
256    PartialEq,
257    SpecifiedValueInfo,
258    ToComputedValue,
259    ToCss,
260    ToResolvedValue,
261    ToShmem,
262    ToTyped,
263)]
264#[repr(u8)]
265#[allow(missing_docs)]
266pub enum AnimationDirection {
267    Normal,
268    Reverse,
269    Alternate,
270    AlternateReverse,
271}
272
273impl AnimationDirection {
274    /// Returns true if the name matches any animation-direction keyword.
275    #[inline]
276    pub fn match_keywords(name: &AnimationName) -> bool {
277        if let Some(name) = name.as_atom() {
278            #[cfg(feature = "gecko")]
279            return name.with_str(|n| Self::from_ident(n).is_ok());
280            #[cfg(feature = "servo")]
281            return Self::from_ident(name).is_ok();
282        }
283        false
284    }
285}
286
287/// https://drafts.csswg.org/css-animations/#animation-play-state
288#[derive(
289    Copy,
290    Clone,
291    Debug,
292    MallocSizeOf,
293    Parse,
294    PartialEq,
295    SpecifiedValueInfo,
296    ToComputedValue,
297    ToCss,
298    ToResolvedValue,
299    ToShmem,
300    ToTyped,
301)]
302#[repr(u8)]
303#[allow(missing_docs)]
304pub enum AnimationPlayState {
305    Running,
306    Paused,
307}
308
309impl AnimationPlayState {
310    /// Returns true if the name matches any animation-play-state keyword.
311    #[inline]
312    pub fn match_keywords(name: &AnimationName) -> bool {
313        if let Some(name) = name.as_atom() {
314            #[cfg(feature = "gecko")]
315            return name.with_str(|n| Self::from_ident(n).is_ok());
316            #[cfg(feature = "servo")]
317            return Self::from_ident(name).is_ok();
318        }
319        false
320    }
321}
322
323/// https://drafts.csswg.org/css-animations/#propdef-animation-fill-mode
324#[derive(
325    Copy,
326    Clone,
327    Debug,
328    MallocSizeOf,
329    Parse,
330    PartialEq,
331    SpecifiedValueInfo,
332    ToComputedValue,
333    ToCss,
334    ToResolvedValue,
335    ToShmem,
336    ToTyped,
337)]
338#[repr(u8)]
339#[allow(missing_docs)]
340pub enum AnimationFillMode {
341    None,
342    Forwards,
343    Backwards,
344    Both,
345}
346
347impl AnimationFillMode {
348    /// Returns true if the name matches any animation-fill-mode keyword.
349    /// Note: animation-name:none is its initial value, so we don't have to match none here.
350    #[inline]
351    pub fn match_keywords(name: &AnimationName) -> bool {
352        if let Some(name) = name.as_atom() {
353            #[cfg(feature = "gecko")]
354            return name.with_str(|n| Self::from_ident(n).is_ok());
355            #[cfg(feature = "servo")]
356            return Self::from_ident(name).is_ok();
357        }
358        false
359    }
360}
361
362/// https://drafts.csswg.org/css-animations-2/#animation-composition
363#[derive(
364    Copy,
365    Clone,
366    Debug,
367    MallocSizeOf,
368    Parse,
369    PartialEq,
370    SpecifiedValueInfo,
371    ToComputedValue,
372    ToCss,
373    ToResolvedValue,
374    ToShmem,
375    ToTyped,
376)]
377#[repr(u8)]
378#[allow(missing_docs)]
379pub enum AnimationComposition {
380    Replace,
381    Add,
382    Accumulate,
383}
384
385/// A value for the <Scroller> used in scroll().
386///
387/// https://drafts.csswg.org/scroll-animations-1/rewrite#typedef-scroller
388#[derive(
389    Copy,
390    Clone,
391    Debug,
392    Eq,
393    Hash,
394    MallocSizeOf,
395    Parse,
396    PartialEq,
397    SpecifiedValueInfo,
398    ToComputedValue,
399    ToCss,
400    ToResolvedValue,
401    ToShmem,
402)]
403#[repr(u8)]
404pub enum Scroller {
405    /// The nearest ancestor scroll container. (Default.)
406    Nearest,
407    /// The document viewport as the scroll container.
408    Root,
409    /// Specifies to use the element’s own principal box as the scroll container.
410    #[css(keyword = "self")]
411    SelfElement,
412}
413
414impl Scroller {
415    /// Returns true if it is default.
416    #[inline]
417    fn is_default(&self) -> bool {
418        matches!(*self, Self::Nearest)
419    }
420}
421
422impl Default for Scroller {
423    fn default() -> Self {
424        Self::Nearest
425    }
426}
427
428/// A value for the <Axis> used in scroll(), or a value for {scroll|view}-timeline-axis.
429///
430/// https://drafts.csswg.org/scroll-animations-1/#typedef-axis
431/// https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis
432/// https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis
433#[derive(
434    Copy,
435    Clone,
436    Debug,
437    Eq,
438    Hash,
439    MallocSizeOf,
440    Parse,
441    PartialEq,
442    SpecifiedValueInfo,
443    ToComputedValue,
444    ToCss,
445    ToResolvedValue,
446    ToShmem,
447)]
448#[repr(u8)]
449pub enum ScrollAxis {
450    /// The block axis of the scroll container. (Default.)
451    Block = 0,
452    /// The inline axis of the scroll container.
453    Inline = 1,
454    /// The horizontal axis of the scroll container.
455    X = 2,
456    /// The vertical axis of the scroll container.
457    Y = 3,
458}
459
460impl ScrollAxis {
461    /// Returns true if it is default.
462    #[inline]
463    pub fn is_default(&self) -> bool {
464        matches!(*self, Self::Block)
465    }
466}
467
468impl Default for ScrollAxis {
469    fn default() -> Self {
470        Self::Block
471    }
472}
473
474/// The scroll() notation.
475/// https://drafts.csswg.org/scroll-animations-1/#scroll-notation
476#[derive(
477    Copy,
478    Clone,
479    Debug,
480    MallocSizeOf,
481    PartialEq,
482    SpecifiedValueInfo,
483    ToComputedValue,
484    ToCss,
485    ToResolvedValue,
486    ToShmem,
487)]
488#[css(function = "scroll")]
489#[repr(C)]
490pub struct ScrollFunction {
491    /// The scroll container element whose scroll position drives the progress of the timeline.
492    #[css(skip_if = "Scroller::is_default")]
493    pub scroller: Scroller,
494    /// The axis of scrolling that drives the progress of the timeline.
495    #[css(skip_if = "ScrollAxis::is_default")]
496    pub axis: ScrollAxis,
497}
498
499impl ScrollFunction {
500    /// Parse the inner function arguments of `scroll()`.
501    fn parse_arguments<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
502        // <scroll()> = scroll( [ <scroller> || <axis> ]? )
503        // https://drafts.csswg.org/scroll-animations-1/#funcdef-scroll
504        let mut scroller = None;
505        let mut axis = None;
506        loop {
507            if scroller.is_none() {
508                scroller = input.try_parse(Scroller::parse).ok();
509            }
510
511            if axis.is_none() {
512                axis = input.try_parse(ScrollAxis::parse).ok();
513                if axis.is_some() {
514                    continue;
515                }
516            }
517            break;
518        }
519
520        Ok(Self {
521            scroller: scroller.unwrap_or_default(),
522            axis: axis.unwrap_or_default(),
523        })
524    }
525}
526
527impl generics::ViewFunction<LengthPercentage> {
528    /// Parse the inner function arguments of `view()`.
529    fn parse_arguments<'i, 't>(
530        context: &ParserContext,
531        input: &mut Parser<'i, 't>,
532    ) -> Result<Self, ParseError<'i>> {
533        // <view()> = view( [ <axis> || <'view-timeline-inset'> ]? )
534        // https://drafts.csswg.org/scroll-animations-1/#funcdef-view
535        let mut axis = None;
536        let mut inset = None;
537        loop {
538            if axis.is_none() {
539                axis = input.try_parse(ScrollAxis::parse).ok();
540            }
541
542            if inset.is_none() {
543                inset = input
544                    .try_parse(|i| ViewTimelineInset::parse(context, i))
545                    .ok();
546                if inset.is_some() {
547                    continue;
548                }
549            }
550            break;
551        }
552
553        Ok(Self {
554            inset: inset.unwrap_or_default(),
555            axis: axis.unwrap_or_default(),
556        })
557    }
558}
559
560/// The typedef of scroll-timeline-name or view-timeline-name.
561///
562/// https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-name
563/// https://drafts.csswg.org/scroll-animations-1/#view-timeline-name
564#[derive(
565    Clone,
566    Debug,
567    Eq,
568    Hash,
569    MallocSizeOf,
570    PartialEq,
571    SpecifiedValueInfo,
572    ToComputedValue,
573    ToResolvedValue,
574    ToShmem,
575)]
576#[repr(C)]
577pub struct TimelineName(DashedIdent);
578
579impl TimelineName {
580    /// Returns the `none` value.
581    pub fn none() -> Self {
582        Self(DashedIdent::empty())
583    }
584
585    /// Check if this is `none` value.
586    pub fn is_none(&self) -> bool {
587        self.0.is_empty()
588    }
589}
590
591impl Parse for TimelineName {
592    fn parse<'i, 't>(
593        context: &ParserContext,
594        input: &mut Parser<'i, 't>,
595    ) -> Result<Self, ParseError<'i>> {
596        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
597            return Ok(Self::none());
598        }
599
600        DashedIdent::parse(context, input).map(TimelineName)
601    }
602}
603
604impl ToCss for TimelineName {
605    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
606    where
607        W: Write,
608    {
609        if self.is_none() {
610            return dest.write_str("none");
611        }
612
613        self.0.to_css(dest)
614    }
615}
616
617/// A specified value for the `animation-timeline` property.
618pub type AnimationTimeline = generics::GenericAnimationTimeline<LengthPercentage>;
619
620impl Parse for AnimationTimeline {
621    fn parse<'i, 't>(
622        context: &ParserContext,
623        input: &mut Parser<'i, 't>,
624    ) -> Result<Self, ParseError<'i>> {
625        use crate::values::generics::animation::ViewFunction;
626
627        // <single-animation-timeline> = auto | none | <dashed-ident> | <scroll()> | <view()>
628        // https://drafts.csswg.org/css-animations-2/#typedef-single-animation-timeline
629
630        if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() {
631            return Ok(Self::Auto);
632        }
633
634        // This parses none or <dashed-indent>.
635        if let Ok(name) = input.try_parse(|i| TimelineName::parse(context, i)) {
636            return Ok(AnimationTimeline::Timeline(name));
637        }
638
639        // Parse <scroll()> or <view()>.
640        let location = input.current_source_location();
641        let function = input.expect_function()?.clone();
642        input.parse_nested_block(move |i| {
643            match_ignore_ascii_case! { &function,
644                "scroll" => ScrollFunction::parse_arguments(i).map(Self::Scroll),
645                "view" => ViewFunction::parse_arguments(context, i).map(Self::View),
646                _ => {
647                    Err(location.new_custom_error(
648                        StyleParseErrorKind::UnexpectedFunction(function.clone())
649                    ))
650                },
651            }
652        })
653    }
654}
655
656/// A specified value for the `view-timeline-inset` property.
657pub type ViewTimelineInset = generics::GenericViewTimelineInset<LengthPercentage>;
658
659impl Parse for ViewTimelineInset {
660    fn parse<'i, 't>(
661        context: &ParserContext,
662        input: &mut Parser<'i, 't>,
663    ) -> Result<Self, ParseError<'i>> {
664        use crate::values::specified::LengthPercentageOrAuto;
665
666        let start = LengthPercentageOrAuto::parse(context, input)?;
667        let end = match input.try_parse(|input| LengthPercentageOrAuto::parse(context, input)) {
668            Ok(end) => end,
669            Err(_) => start.clone(),
670        };
671
672        Ok(Self { start, end })
673    }
674}
675
676/// The view-transition-name: `none | <custom-ident> | match-element`.
677///
678/// https://drafts.csswg.org/css-view-transitions-1/#view-transition-name-prop
679/// https://drafts.csswg.org/css-view-transitions-2/#auto-vt-name
680// TODO: auto keyword.
681#[derive(
682    Clone,
683    Debug,
684    Eq,
685    Hash,
686    PartialEq,
687    MallocSizeOf,
688    SpecifiedValueInfo,
689    ToComputedValue,
690    ToResolvedValue,
691    ToShmem,
692    ToTyped,
693)]
694#[repr(C, u8)]
695pub enum ViewTransitionName {
696    /// None keyword.
697    None,
698    /// match-element keyword.
699    /// https://drafts.csswg.org/css-view-transitions-2/#auto-vt-name
700    MatchElement,
701    /// A `<custom-ident>`.
702    Ident(Atom),
703}
704
705impl ViewTransitionName {
706    /// Returns the `none` value.
707    pub fn none() -> Self {
708        Self::None
709    }
710}
711
712impl Parse for ViewTransitionName {
713    fn parse<'i, 't>(
714        _: &ParserContext,
715        input: &mut Parser<'i, 't>,
716    ) -> Result<Self, ParseError<'i>> {
717        let location = input.current_source_location();
718        let ident = input.expect_ident()?;
719        if ident.eq_ignore_ascii_case("none") {
720            return Ok(Self::none());
721        }
722
723        if ident.eq_ignore_ascii_case("match-element") {
724            return Ok(Self::MatchElement);
725        }
726
727        // We check none already, so don't need to exclude none here.
728        // Note: "auto" is not supported yet so we exclude it.
729        CustomIdent::from_ident(location, ident, &["auto"]).map(|i| Self::Ident(i.0))
730    }
731}
732
733impl ToCss for ViewTransitionName {
734    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
735    where
736        W: Write,
737    {
738        use crate::values::serialize_atom_identifier;
739        match *self {
740            Self::None => dest.write_str("none"),
741            Self::MatchElement => dest.write_str("match-element"),
742            Self::Ident(ref ident) => serialize_atom_identifier(ident, dest),
743        }
744    }
745}
746
747/// The view-transition-class: `none | <custom-ident>+`.
748///
749/// https://drafts.csswg.org/css-view-transitions-2/#view-transition-class-prop
750///
751/// Empty slice represents `none`.
752#[derive(
753    Clone,
754    Debug,
755    Eq,
756    Hash,
757    PartialEq,
758    MallocSizeOf,
759    SpecifiedValueInfo,
760    ToComputedValue,
761    ToCss,
762    ToResolvedValue,
763    ToShmem,
764    ToTyped,
765)]
766#[repr(C)]
767#[value_info(other_values = "none")]
768pub struct ViewTransitionClass(
769    #[css(iterable, if_empty = "none")]
770    #[ignore_malloc_size_of = "Arc"]
771    crate::ArcSlice<CustomIdent>,
772);
773
774impl ViewTransitionClass {
775    /// Returns the default value, `none`. We use the default slice (i.e. empty) to represent it.
776    pub fn none() -> Self {
777        Self(Default::default())
778    }
779
780    /// Returns whether this is the `none` value.
781    pub fn is_none(&self) -> bool {
782        self.0.is_empty()
783    }
784}
785
786impl Parse for ViewTransitionClass {
787    fn parse<'i, 't>(
788        _: &ParserContext,
789        input: &mut Parser<'i, 't>,
790    ) -> Result<Self, ParseError<'i>> {
791        use style_traits::{Separator, Space};
792
793        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
794            return Ok(Self::none());
795        }
796
797        Ok(Self(crate::ArcSlice::from_iter(
798            Space::parse(input, |i| CustomIdent::parse(i, &["none"]))?.into_iter(),
799        )))
800    }
801}