Skip to main content

lightningcss/properties/
animation.rs

1//! CSS properties related to keyframe animations.
2
3use std::borrow::Cow;
4
5use crate::context::PropertyHandlerContext;
6use crate::declaration::{DeclarationBlock, DeclarationList};
7use crate::error::{ParserError, PrinterError};
8use crate::macros::*;
9use crate::prefixes::Feature;
10use crate::printer::Printer;
11use crate::properties::{Property, PropertyId, TokenOrValue, VendorPrefix};
12use crate::traits::{Parse, PropertyHandler, Shorthand, ToCss, Zero};
13use crate::values::ident::DashedIdent;
14use crate::values::number::CSSNumber;
15use crate::values::percentage::Percentage;
16use crate::values::size::Size2D;
17use crate::values::string::CSSString;
18use crate::values::{easing::EasingFunction, ident::CustomIdent, time::Time};
19#[cfg(feature = "visitor")]
20use crate::visitor::Visit;
21use cssparser::*;
22use itertools::izip;
23use smallvec::SmallVec;
24
25use super::{LengthPercentage, LengthPercentageOrAuto};
26
27/// A value for the [animation-name](https://drafts.csswg.org/css-animations/#animation-name) property.
28#[derive(Debug, Clone, PartialEq, Parse)]
29#[cfg_attr(feature = "visitor", derive(Visit))]
30#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
31#[cfg_attr(
32  feature = "serde",
33  derive(serde::Serialize, serde::Deserialize),
34  serde(tag = "type", content = "value", rename_all = "kebab-case")
35)]
36#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
37pub enum AnimationName<'i> {
38  /// The `none` keyword.
39  None,
40  /// An identifier of a `@keyframes` rule.
41  #[cfg_attr(feature = "serde", serde(borrow))]
42  Ident(CustomIdent<'i>),
43  /// A `<string>` name of a `@keyframes` rule.
44  #[cfg_attr(feature = "serde", serde(borrow))]
45  String(CSSString<'i>),
46}
47
48impl<'i> ToCss for AnimationName<'i> {
49  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
50  where
51    W: std::fmt::Write,
52  {
53    let css_module_animation_enabled =
54      dest.css_module.as_ref().map_or(false, |css_module| css_module.config.animation);
55
56    match self {
57      AnimationName::None => dest.write_str("none"),
58      AnimationName::Ident(s) => {
59        if css_module_animation_enabled {
60          if let Some(css_module) = &mut dest.css_module {
61            css_module.reference(&s.0, dest.loc.source_index)
62          }
63        }
64        s.to_css_with_options(dest, css_module_animation_enabled)
65      }
66      AnimationName::String(s) => {
67        if css_module_animation_enabled {
68          if let Some(css_module) = &mut dest.css_module {
69            css_module.reference(&s, dest.loc.source_index)
70          }
71        }
72
73        // CSS-wide keywords and `none` cannot remove quotes.
74        match_ignore_ascii_case! { &*s,
75          "none" | "initial" | "inherit" | "unset" | "default" | "revert" | "revert-layer" => {
76            serialize_string(&s, dest)?;
77            Ok(())
78          },
79          _ => {
80            dest.write_ident(s.as_ref(), css_module_animation_enabled)
81          }
82        }
83      }
84    }
85  }
86}
87
88/// A list of animation names.
89pub type AnimationNameList<'i> = SmallVec<[AnimationName<'i>; 1]>;
90
91/// A value for the [animation-iteration-count](https://drafts.csswg.org/css-animations/#animation-iteration-count) property.
92#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
93#[cfg_attr(feature = "visitor", derive(Visit))]
94#[cfg_attr(
95  feature = "serde",
96  derive(serde::Serialize, serde::Deserialize),
97  serde(tag = "type", content = "value", rename_all = "kebab-case")
98)]
99#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
100#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
101pub enum AnimationIterationCount {
102  /// The animation will repeat the specified number of times.
103  Number(CSSNumber),
104  /// The animation will repeat forever.
105  Infinite,
106}
107
108impl Default for AnimationIterationCount {
109  fn default() -> Self {
110    AnimationIterationCount::Number(1.0)
111  }
112}
113
114enum_property! {
115  /// A value for the [animation-direction](https://drafts.csswg.org/css-animations/#animation-direction) property.
116  pub enum AnimationDirection {
117    /// The animation is played as specified
118    Normal,
119    /// The animation is played in reverse.
120    Reverse,
121    /// The animation iterations alternate between forward and reverse.
122    Alternate,
123    /// The animation iterations alternate between forward and reverse, with reverse occurring first.
124    AlternateReverse,
125  }
126}
127
128impl Default for AnimationDirection {
129  fn default() -> Self {
130    AnimationDirection::Normal
131  }
132}
133
134enum_property! {
135  /// A value for the [animation-play-state](https://drafts.csswg.org/css-animations/#animation-play-state) property.
136  pub enum AnimationPlayState {
137    /// The animation is playing.
138    Running,
139    /// The animation is paused.
140    Paused,
141  }
142}
143
144impl Default for AnimationPlayState {
145  fn default() -> Self {
146    AnimationPlayState::Running
147  }
148}
149
150enum_property! {
151  /// A value for the [animation-fill-mode](https://drafts.csswg.org/css-animations/#animation-fill-mode) property.
152  pub enum AnimationFillMode {
153    /// The animation has no effect while not playing.
154    None,
155    /// After the animation, the ending values are applied.
156    Forwards,
157    /// Before the animation, the starting values are applied.
158    Backwards,
159    /// Both forwards and backwards apply.
160    Both,
161  }
162}
163
164impl Default for AnimationFillMode {
165  fn default() -> Self {
166    AnimationFillMode::None
167  }
168}
169
170enum_property! {
171  /// A value for the [animation-composition](https://drafts.csswg.org/css-animations-2/#animation-composition) property.
172  pub enum AnimationComposition {
173    /// The result of compositing the effect value with the underlying value is simply the effect value.
174    Replace,
175    /// The effect value is added to the underlying value.
176    Add,
177    /// The effect value is accumulated onto the underlying value.
178    Accumulate,
179  }
180}
181
182/// A value for the [animation-timeline](https://drafts.csswg.org/css-animations-2/#animation-timeline) property.
183#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
184#[cfg_attr(feature = "visitor", derive(Visit))]
185#[cfg_attr(
186  feature = "serde",
187  derive(serde::Serialize, serde::Deserialize),
188  serde(tag = "type", content = "value", rename_all = "kebab-case")
189)]
190#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
191#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
192pub enum AnimationTimeline<'i> {
193  /// The animation’s timeline is a DocumentTimeline, more specifically the default document timeline.
194  Auto,
195  /// The animation is not associated with a timeline.
196  None,
197  /// A timeline referenced by name.
198  #[cfg_attr(feature = "serde", serde(borrow))]
199  DashedIdent(DashedIdent<'i>),
200  /// The scroll() function.
201  Scroll(ScrollTimeline),
202  /// The view() function.
203  View(ViewTimeline),
204}
205
206impl<'i> Default for AnimationTimeline<'i> {
207  fn default() -> Self {
208    AnimationTimeline::Auto
209  }
210}
211
212/// The [scroll()](https://drafts.csswg.org/scroll-animations-1/#scroll-notation) function.
213#[derive(Debug, Clone, PartialEq)]
214#[cfg_attr(feature = "visitor", derive(Visit))]
215#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
216#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
217#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
218pub struct ScrollTimeline {
219  /// Specifies which element to use as the scroll container.
220  pub scroller: Scroller,
221  /// Specifies which axis of the scroll container to use as the progress for the timeline.
222  pub axis: ScrollAxis,
223}
224
225impl<'i> Parse<'i> for ScrollTimeline {
226  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
227    input.expect_function_matching("scroll")?;
228    input.parse_nested_block(|input| {
229      let mut scroller = None;
230      let mut axis = None;
231      loop {
232        if scroller.is_none() {
233          scroller = input.try_parse(Scroller::parse).ok();
234        }
235
236        if axis.is_none() {
237          axis = input.try_parse(ScrollAxis::parse).ok();
238          if axis.is_some() {
239            continue;
240          }
241        }
242        break;
243      }
244
245      Ok(ScrollTimeline {
246        scroller: scroller.unwrap_or_default(),
247        axis: axis.unwrap_or_default(),
248      })
249    })
250  }
251}
252
253impl ToCss for ScrollTimeline {
254  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
255  where
256    W: std::fmt::Write,
257  {
258    dest.write_str("scroll(")?;
259
260    let mut needs_space = false;
261    if self.scroller != Scroller::default() {
262      self.scroller.to_css(dest)?;
263      needs_space = true;
264    }
265
266    if self.axis != ScrollAxis::default() {
267      if needs_space {
268        dest.write_char(' ')?;
269      }
270      self.axis.to_css(dest)?;
271    }
272
273    dest.write_char(')')
274  }
275}
276
277enum_property! {
278  /// A scroller, used in the `scroll()` function.
279  pub enum Scroller {
280    /// Specifies to use the document viewport as the scroll container.
281    "root": Root,
282    /// Specifies to use the nearest ancestor scroll container.
283    "nearest": Nearest,
284    /// Specifies to use the element’s own principal box as the scroll container.
285    "self": SelfElement,
286  }
287}
288
289impl Default for Scroller {
290  fn default() -> Self {
291    Scroller::Nearest
292  }
293}
294
295enum_property! {
296  /// A scroll axis, used in the `scroll()` function.
297  pub enum ScrollAxis {
298    /// Specifies to use the measure of progress along the block axis of the scroll container.
299    Block,
300    /// Specifies to use the measure of progress along the inline axis of the scroll container.
301    Inline,
302    /// Specifies to use the measure of progress along the horizontal axis of the scroll container.
303    X,
304    /// Specifies to use the measure of progress along the vertical axis of the scroll container.
305    Y,
306  }
307}
308
309impl Default for ScrollAxis {
310  fn default() -> Self {
311    ScrollAxis::Block
312  }
313}
314
315/// The [view()](https://drafts.csswg.org/scroll-animations-1/#view-notation) function.
316#[derive(Debug, Clone, PartialEq)]
317#[cfg_attr(feature = "visitor", derive(Visit))]
318#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
319#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
320#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
321pub struct ViewTimeline {
322  /// Specifies which axis of the scroll container to use as the progress for the timeline.
323  pub axis: ScrollAxis,
324  /// Provides an adjustment of the view progress visibility range.
325  pub inset: Size2D<LengthPercentageOrAuto>,
326}
327
328impl<'i> Parse<'i> for ViewTimeline {
329  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
330    input.expect_function_matching("view")?;
331    input.parse_nested_block(|input| {
332      let mut axis = None;
333      let mut inset = None;
334      loop {
335        if axis.is_none() {
336          axis = input.try_parse(ScrollAxis::parse).ok();
337        }
338
339        if inset.is_none() {
340          inset = input.try_parse(Size2D::parse).ok();
341          if inset.is_some() {
342            continue;
343          }
344        }
345        break;
346      }
347
348      Ok(ViewTimeline {
349        axis: axis.unwrap_or_default(),
350        inset: inset.unwrap_or(Size2D(LengthPercentageOrAuto::Auto, LengthPercentageOrAuto::Auto)),
351      })
352    })
353  }
354}
355
356impl ToCss for ViewTimeline {
357  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
358  where
359    W: std::fmt::Write,
360  {
361    dest.write_str("view(")?;
362    let mut needs_space = false;
363    if self.axis != ScrollAxis::default() {
364      self.axis.to_css(dest)?;
365      needs_space = true;
366    }
367
368    if self.inset.0 != LengthPercentageOrAuto::Auto || self.inset.1 != LengthPercentageOrAuto::Auto {
369      if needs_space {
370        dest.write_char(' ')?;
371      }
372      self.inset.to_css(dest)?;
373    }
374
375    dest.write_char(')')
376  }
377}
378
379/// A [view progress timeline range](https://drafts.csswg.org/scroll-animations/#view-timelines-ranges)
380#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
381#[cfg_attr(feature = "visitor", derive(Visit))]
382#[cfg_attr(
383  feature = "serde",
384  derive(serde::Serialize, serde::Deserialize),
385  serde(rename_all = "kebab-case")
386)]
387#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
388#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
389pub enum TimelineRangeName {
390  /// Represents the full range of the view progress timeline.
391  Cover,
392  /// Represents the range during which the principal box is either fully contained by,
393  /// or fully covers, its view progress visibility range within the scrollport.
394  Contain,
395  /// Represents the range during which the principal box is entering the view progress visibility range.
396  Entry,
397  /// Represents the range during which the principal box is exiting the view progress visibility range.
398  Exit,
399  /// Represents the range during which the principal box crosses the end border edge.
400  EntryCrossing,
401  /// Represents the range during which the principal box crosses the start border edge.
402  ExitCrossing,
403}
404
405/// A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start)
406/// or [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property.
407#[derive(Debug, Clone, PartialEq)]
408#[cfg_attr(feature = "visitor", derive(Visit))]
409#[cfg_attr(
410  feature = "serde",
411  derive(serde::Serialize, serde::Deserialize),
412  serde(rename_all = "lowercase")
413)]
414#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
415#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
416pub enum AnimationAttachmentRange {
417  /// The start of the animation’s attachment range is the start of its associated timeline.
418  Normal,
419  /// The animation attachment range starts at the specified point on the timeline measuring from the start of the timeline.
420  #[cfg_attr(feature = "serde", serde(untagged))]
421  LengthPercentage(LengthPercentage),
422  /// The animation attachment range starts at the specified point on the timeline measuring from the start of the specified named timeline range.
423  #[cfg_attr(feature = "serde", serde(untagged))]
424  TimelineRange {
425    /// The name of the timeline range.
426    name: TimelineRangeName,
427    /// The offset from the start of the named timeline range.
428    offset: LengthPercentage,
429  },
430}
431
432impl<'i> AnimationAttachmentRange {
433  fn parse<'t>(input: &mut Parser<'i, 't>, default: f32) -> Result<Self, ParseError<'i, ParserError<'i>>> {
434    if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() {
435      return Ok(AnimationAttachmentRange::Normal);
436    }
437
438    if let Ok(val) = input.try_parse(LengthPercentage::parse) {
439      return Ok(AnimationAttachmentRange::LengthPercentage(val));
440    }
441
442    let name = TimelineRangeName::parse(input)?;
443    let offset = input
444      .try_parse(LengthPercentage::parse)
445      .unwrap_or(LengthPercentage::Percentage(Percentage(default)));
446    Ok(AnimationAttachmentRange::TimelineRange { name, offset })
447  }
448
449  fn to_css<W>(&self, dest: &mut Printer<W>, default: f32) -> Result<(), PrinterError>
450  where
451    W: std::fmt::Write,
452  {
453    match self {
454      Self::Normal => dest.write_str("normal"),
455      Self::LengthPercentage(val) => val.to_css(dest),
456      Self::TimelineRange { name, offset } => {
457        name.to_css(dest)?;
458        if *offset != LengthPercentage::Percentage(Percentage(default)) {
459          dest.write_char(' ')?;
460          offset.to_css(dest)?;
461        }
462        Ok(())
463      }
464    }
465  }
466}
467
468impl Default for AnimationAttachmentRange {
469  fn default() -> Self {
470    AnimationAttachmentRange::Normal
471  }
472}
473
474/// A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) property.
475#[derive(Debug, Clone, PartialEq)]
476#[cfg_attr(feature = "visitor", derive(Visit))]
477#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
478#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
479#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
480pub struct AnimationRangeStart(pub AnimationAttachmentRange);
481
482impl<'i> Parse<'i> for AnimationRangeStart {
483  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
484    let range = AnimationAttachmentRange::parse(input, 0.0)?;
485    Ok(Self(range))
486  }
487}
488
489impl ToCss for AnimationRangeStart {
490  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
491  where
492    W: std::fmt::Write,
493  {
494    self.0.to_css(dest, 0.0)
495  }
496}
497
498/// A value for the [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property.
499#[derive(Debug, Clone, PartialEq)]
500#[cfg_attr(feature = "visitor", derive(Visit))]
501#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
502#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
503#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
504pub struct AnimationRangeEnd(pub AnimationAttachmentRange);
505
506impl<'i> Parse<'i> for AnimationRangeEnd {
507  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
508    let range = AnimationAttachmentRange::parse(input, 1.0)?;
509    Ok(Self(range))
510  }
511}
512
513impl ToCss for AnimationRangeEnd {
514  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
515  where
516    W: std::fmt::Write,
517  {
518    self.0.to_css(dest, 1.0)
519  }
520}
521
522/// A value for the [animation-range](https://drafts.csswg.org/scroll-animations/#animation-range) shorthand property.
523#[derive(Debug, Clone, PartialEq)]
524#[cfg_attr(feature = "visitor", derive(Visit))]
525#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
526#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
527#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
528pub struct AnimationRange {
529  /// The start of the animation's attachment range.
530  pub start: AnimationRangeStart,
531  /// The end of the animation's attachment range.
532  pub end: AnimationRangeEnd,
533}
534
535impl<'i> Parse<'i> for AnimationRange {
536  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
537    let start = AnimationRangeStart::parse(input)?;
538    let end = input
539      .try_parse(AnimationRangeStart::parse)
540      .map(|r| AnimationRangeEnd(r.0))
541      .unwrap_or_else(|_| {
542        // If <'animation-range-end'> is omitted and <'animation-range-start'> includes a <timeline-range-name> component, then
543        // animation-range-end is set to that same <timeline-range-name> and 100%. Otherwise, any omitted longhand is set to its initial value.
544        match &start.0 {
545          AnimationAttachmentRange::TimelineRange { name, .. } => {
546            AnimationRangeEnd(AnimationAttachmentRange::TimelineRange {
547              name: name.clone(),
548              offset: LengthPercentage::Percentage(Percentage(1.0)),
549            })
550          }
551          _ => AnimationRangeEnd(AnimationAttachmentRange::default()),
552        }
553      });
554    Ok(AnimationRange { start, end })
555  }
556}
557
558impl ToCss for AnimationRange {
559  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
560  where
561    W: std::fmt::Write,
562  {
563    self.start.to_css(dest)?;
564
565    let omit_end = match (&self.start.0, &self.end.0) {
566      (
567        AnimationAttachmentRange::TimelineRange { name: start_name, .. },
568        AnimationAttachmentRange::TimelineRange {
569          name: end_name,
570          offset: end_offset,
571        },
572      ) => start_name == end_name && *end_offset == LengthPercentage::Percentage(Percentage(1.0)),
573      (_, end) => *end == AnimationAttachmentRange::default(),
574    };
575
576    if !omit_end {
577      dest.write_char(' ')?;
578      self.end.to_css(dest)?;
579    }
580    Ok(())
581  }
582}
583
584define_list_shorthand! {
585  /// A value for the [animation](https://drafts.csswg.org/css-animations/#animation) shorthand property.
586  pub struct Animation<'i>(VendorPrefix) {
587    /// The animation name.
588    #[cfg_attr(feature = "serde", serde(borrow))]
589    name: AnimationName(AnimationName<'i>, VendorPrefix),
590    /// The animation duration.
591    duration: AnimationDuration(Time, VendorPrefix),
592    /// The easing function for the animation.
593    timing_function: AnimationTimingFunction(EasingFunction, VendorPrefix),
594    /// The number of times the animation will run.
595    iteration_count: AnimationIterationCount(AnimationIterationCount, VendorPrefix),
596    /// The direction of the animation.
597    direction: AnimationDirection(AnimationDirection, VendorPrefix),
598    /// The current play state of the animation.
599    play_state: AnimationPlayState(AnimationPlayState, VendorPrefix),
600    /// The animation delay.
601    delay: AnimationDelay(Time, VendorPrefix),
602    /// The animation fill mode.
603    fill_mode: AnimationFillMode(AnimationFillMode, VendorPrefix),
604    /// The animation timeline.
605    timeline: AnimationTimeline(AnimationTimeline<'i>),
606  }
607}
608
609impl<'i> Parse<'i> for Animation<'i> {
610  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
611    let mut name = None;
612    let mut duration = None;
613    let mut timing_function = None;
614    let mut iteration_count = None;
615    let mut direction = None;
616    let mut play_state = None;
617    let mut delay = None;
618    let mut fill_mode = None;
619    let mut timeline = None;
620
621    macro_rules! parse_prop {
622      ($var: ident, $type: ident) => {
623        if $var.is_none() {
624          if let Ok(value) = input.try_parse($type::parse) {
625            $var = Some(value);
626            continue;
627          }
628        }
629      };
630    }
631
632    loop {
633      parse_prop!(duration, Time);
634      parse_prop!(timing_function, EasingFunction);
635      parse_prop!(delay, Time);
636      parse_prop!(iteration_count, AnimationIterationCount);
637      parse_prop!(direction, AnimationDirection);
638      parse_prop!(fill_mode, AnimationFillMode);
639      parse_prop!(play_state, AnimationPlayState);
640      parse_prop!(name, AnimationName);
641      parse_prop!(timeline, AnimationTimeline);
642      break;
643    }
644
645    Ok(Animation {
646      name: name.unwrap_or(AnimationName::None),
647      duration: duration.unwrap_or(Time::Seconds(0.0)),
648      timing_function: timing_function.unwrap_or(EasingFunction::Ease),
649      iteration_count: iteration_count.unwrap_or(AnimationIterationCount::Number(1.0)),
650      direction: direction.unwrap_or(AnimationDirection::Normal),
651      play_state: play_state.unwrap_or(AnimationPlayState::Running),
652      delay: delay.unwrap_or(Time::Seconds(0.0)),
653      fill_mode: fill_mode.unwrap_or(AnimationFillMode::None),
654      timeline: timeline.unwrap_or(AnimationTimeline::Auto),
655    })
656  }
657}
658
659impl<'i> ToCss for Animation<'i> {
660  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
661  where
662    W: std::fmt::Write,
663  {
664    match &self.name {
665      AnimationName::None => {}
666      AnimationName::Ident(CustomIdent(name)) | AnimationName::String(CSSString(name)) => {
667        if !self.duration.is_zero() || !self.delay.is_zero() {
668          self.duration.to_css(dest)?;
669          dest.write_char(' ')?;
670        }
671
672        if !self.timing_function.is_ease() || EasingFunction::is_ident(&name) {
673          self.timing_function.to_css(dest)?;
674          dest.write_char(' ')?;
675        }
676
677        if !self.delay.is_zero() {
678          self.delay.to_css(dest)?;
679          dest.write_char(' ')?;
680        }
681
682        if self.iteration_count != AnimationIterationCount::default() || name.as_ref() == "infinite" {
683          self.iteration_count.to_css(dest)?;
684          dest.write_char(' ')?;
685        }
686
687        if self.direction != AnimationDirection::default() || AnimationDirection::parse_string(&name).is_ok() {
688          self.direction.to_css(dest)?;
689          dest.write_char(' ')?;
690        }
691
692        if self.fill_mode != AnimationFillMode::default()
693          || (!name.eq_ignore_ascii_case("none") && AnimationFillMode::parse_string(&name).is_ok())
694        {
695          self.fill_mode.to_css(dest)?;
696          dest.write_char(' ')?;
697        }
698
699        if self.play_state != AnimationPlayState::default() || AnimationPlayState::parse_string(&name).is_ok() {
700          self.play_state.to_css(dest)?;
701          dest.write_char(' ')?;
702        }
703      }
704    }
705
706    // Eventually we could output a string here to avoid duplicating some properties above.
707    // Chrome does not yet support strings, however.
708    self.name.to_css(dest)?;
709
710    if self.name != AnimationName::None && self.timeline != AnimationTimeline::default() {
711      dest.write_char(' ')?;
712      self.timeline.to_css(dest)?;
713    }
714
715    Ok(())
716  }
717}
718
719/// A list of animations.
720pub type AnimationList<'i> = SmallVec<[Animation<'i>; 1]>;
721
722#[derive(Default)]
723pub(crate) struct AnimationHandler<'i> {
724  names: Option<(SmallVec<[AnimationName<'i>; 1]>, VendorPrefix)>,
725  durations: Option<(SmallVec<[Time; 1]>, VendorPrefix)>,
726  timing_functions: Option<(SmallVec<[EasingFunction; 1]>, VendorPrefix)>,
727  iteration_counts: Option<(SmallVec<[AnimationIterationCount; 1]>, VendorPrefix)>,
728  directions: Option<(SmallVec<[AnimationDirection; 1]>, VendorPrefix)>,
729  play_states: Option<(SmallVec<[AnimationPlayState; 1]>, VendorPrefix)>,
730  delays: Option<(SmallVec<[Time; 1]>, VendorPrefix)>,
731  fill_modes: Option<(SmallVec<[AnimationFillMode; 1]>, VendorPrefix)>,
732  timelines: Option<SmallVec<[AnimationTimeline<'i>; 1]>>,
733  range_starts: Option<SmallVec<[AnimationRangeStart; 1]>>,
734  range_ends: Option<SmallVec<[AnimationRangeEnd; 1]>>,
735  has_any: bool,
736}
737
738impl<'i> PropertyHandler<'i> for AnimationHandler<'i> {
739  fn handle_property(
740    &mut self,
741    property: &Property<'i>,
742    dest: &mut DeclarationList<'i>,
743    context: &mut PropertyHandlerContext<'i, '_>,
744  ) -> bool {
745    macro_rules! maybe_flush {
746      ($prop: ident, $val: expr, $vp: ident) => {{
747        // If two vendor prefixes for the same property have different
748        // values, we need to flush what we have immediately to preserve order.
749        if let Some((val, prefixes)) = &self.$prop {
750          if val != $val && !prefixes.contains(*$vp) {
751            self.flush(dest, context);
752          }
753        }
754      }};
755    }
756
757    macro_rules! property {
758      ($prop: ident, $val: expr, $vp: ident) => {{
759        maybe_flush!($prop, $val, $vp);
760
761        // Otherwise, update the value and add the prefix.
762        if let Some((val, prefixes)) = &mut self.$prop {
763          *val = $val.clone();
764          *prefixes |= *$vp;
765        } else {
766          self.$prop = Some(($val.clone(), *$vp));
767          self.has_any = true;
768        }
769      }};
770    }
771
772    match property {
773      Property::AnimationName(val, vp) => property!(names, val, vp),
774      Property::AnimationDuration(val, vp) => property!(durations, val, vp),
775      Property::AnimationTimingFunction(val, vp) => property!(timing_functions, val, vp),
776      Property::AnimationIterationCount(val, vp) => property!(iteration_counts, val, vp),
777      Property::AnimationDirection(val, vp) => property!(directions, val, vp),
778      Property::AnimationPlayState(val, vp) => property!(play_states, val, vp),
779      Property::AnimationDelay(val, vp) => property!(delays, val, vp),
780      Property::AnimationFillMode(val, vp) => property!(fill_modes, val, vp),
781      Property::AnimationTimeline(val) => {
782        self.timelines = Some(val.clone());
783        self.has_any = true;
784      }
785      Property::AnimationRangeStart(val) => {
786        self.range_starts = Some(val.clone());
787        self.has_any = true;
788      }
789      Property::AnimationRangeEnd(val) => {
790        self.range_ends = Some(val.clone());
791        self.has_any = true;
792      }
793      Property::AnimationRange(val) => {
794        self.range_starts = Some(val.iter().map(|v| v.start.clone()).collect());
795        self.range_ends = Some(val.iter().map(|v| v.end.clone()).collect());
796        self.has_any = true;
797      }
798      Property::Animation(val, vp) => {
799        let names = val.iter().map(|b| b.name.clone()).collect();
800        maybe_flush!(names, &names, vp);
801
802        let durations = val.iter().map(|b| b.duration.clone()).collect();
803        maybe_flush!(durations, &durations, vp);
804
805        let timing_functions = val.iter().map(|b| b.timing_function.clone()).collect();
806        maybe_flush!(timing_functions, &timing_functions, vp);
807
808        let iteration_counts = val.iter().map(|b| b.iteration_count.clone()).collect();
809        maybe_flush!(iteration_counts, &iteration_counts, vp);
810
811        let directions = val.iter().map(|b| b.direction.clone()).collect();
812        maybe_flush!(directions, &directions, vp);
813
814        let play_states = val.iter().map(|b| b.play_state.clone()).collect();
815        maybe_flush!(play_states, &play_states, vp);
816
817        let delays = val.iter().map(|b| b.delay.clone()).collect();
818        maybe_flush!(delays, &delays, vp);
819
820        let fill_modes = val.iter().map(|b| b.fill_mode.clone()).collect();
821        maybe_flush!(fill_modes, &fill_modes, vp);
822
823        self.timelines = Some(val.iter().map(|b| b.timeline.clone()).collect());
824
825        property!(names, &names, vp);
826        property!(durations, &durations, vp);
827        property!(timing_functions, &timing_functions, vp);
828        property!(iteration_counts, &iteration_counts, vp);
829        property!(directions, &directions, vp);
830        property!(play_states, &play_states, vp);
831        property!(delays, &delays, vp);
832        property!(fill_modes, &fill_modes, vp);
833
834        // The animation shorthand resets animation-range
835        // https://drafts.csswg.org/scroll-animations/#named-range-animation-declaration
836        self.range_starts = None;
837        self.range_ends = None;
838      }
839      Property::Unparsed(val) if is_animation_property(&val.property_id) => {
840        let mut val = Cow::Borrowed(val);
841        if matches!(val.property_id, PropertyId::Animation(_)) {
842          use crate::properties::custom::Token;
843
844          // Find an identifier that isn't a keyword and replace it with an
845          // AnimationName token so it is scoped in CSS modules.
846          for token in &mut val.to_mut().value.0 {
847            match token {
848              TokenOrValue::Token(Token::Ident(id)) => {
849                if AnimationDirection::parse_string(&id).is_err()
850                  && AnimationPlayState::parse_string(&id).is_err()
851                  && AnimationFillMode::parse_string(&id).is_err()
852                  && !EasingFunction::is_ident(&id)
853                  && id.as_ref() != "infinite"
854                  && id.as_ref() != "auto"
855                {
856                  *token = TokenOrValue::AnimationName(AnimationName::Ident(CustomIdent(id.clone())));
857                }
858              }
859              TokenOrValue::Token(Token::String(s)) => {
860                *token = TokenOrValue::AnimationName(AnimationName::String(CSSString(s.clone())));
861              }
862              _ => {}
863            }
864          }
865
866          self.range_starts = None;
867          self.range_ends = None;
868        }
869
870        self.flush(dest, context);
871        dest.push(Property::Unparsed(
872          val.get_prefixed(context.targets, Feature::Animation),
873        ));
874      }
875      _ => return false,
876    }
877
878    true
879  }
880
881  fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
882    self.flush(dest, context);
883  }
884}
885
886impl<'i> AnimationHandler<'i> {
887  fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) {
888    if !self.has_any {
889      return;
890    }
891
892    self.has_any = false;
893
894    let mut names = std::mem::take(&mut self.names);
895    let mut durations = std::mem::take(&mut self.durations);
896    let mut timing_functions = std::mem::take(&mut self.timing_functions);
897    let mut iteration_counts = std::mem::take(&mut self.iteration_counts);
898    let mut directions = std::mem::take(&mut self.directions);
899    let mut play_states = std::mem::take(&mut self.play_states);
900    let mut delays = std::mem::take(&mut self.delays);
901    let mut fill_modes = std::mem::take(&mut self.fill_modes);
902    let mut timelines_value = std::mem::take(&mut self.timelines);
903    let range_starts = std::mem::take(&mut self.range_starts);
904    let range_ends = std::mem::take(&mut self.range_ends);
905
906    if let (
907      Some((names, names_vp)),
908      Some((durations, durations_vp)),
909      Some((timing_functions, timing_functions_vp)),
910      Some((iteration_counts, iteration_counts_vp)),
911      Some((directions, directions_vp)),
912      Some((play_states, play_states_vp)),
913      Some((delays, delays_vp)),
914      Some((fill_modes, fill_modes_vp)),
915    ) = (
916      &mut names,
917      &mut durations,
918      &mut timing_functions,
919      &mut iteration_counts,
920      &mut directions,
921      &mut play_states,
922      &mut delays,
923      &mut fill_modes,
924    ) {
925      // Only use shorthand syntax if the number of animations matches on all properties.
926      let len = names.len();
927      let intersection = *names_vp
928        & *durations_vp
929        & *timing_functions_vp
930        & *iteration_counts_vp
931        & *directions_vp
932        & *play_states_vp
933        & *delays_vp
934        & *fill_modes_vp;
935      let mut timelines = if let Some(timelines) = &mut timelines_value {
936        Cow::Borrowed(timelines)
937      } else if !intersection.contains(VendorPrefix::None) {
938        // Prefixed animation shorthand does not support animation-timeline
939        Cow::Owned(std::iter::repeat(AnimationTimeline::Auto).take(len).collect())
940      } else {
941        Cow::Owned(SmallVec::new())
942      };
943
944      if !intersection.is_empty()
945        && durations.len() == len
946        && timing_functions.len() == len
947        && iteration_counts.len() == len
948        && directions.len() == len
949        && play_states.len() == len
950        && delays.len() == len
951        && fill_modes.len() == len
952        && timelines.len() == len
953      {
954        let timeline_property = if timelines.iter().any(|t| *t != AnimationTimeline::Auto)
955          && (intersection != VendorPrefix::None
956            || !context
957              .targets
958              .is_compatible(crate::compat::Feature::AnimationTimelineShorthand))
959        {
960          Some(Property::AnimationTimeline(timelines.clone().into_owned()))
961        } else {
962          None
963        };
964
965        let animations = izip!(
966          names.drain(..),
967          durations.drain(..),
968          timing_functions.drain(..),
969          iteration_counts.drain(..),
970          directions.drain(..),
971          play_states.drain(..),
972          delays.drain(..),
973          fill_modes.drain(..),
974          timelines.to_mut().drain(..)
975        )
976        .map(
977          |(
978            name,
979            duration,
980            timing_function,
981            iteration_count,
982            direction,
983            play_state,
984            delay,
985            fill_mode,
986            timeline,
987          )| {
988            Animation {
989              name,
990              duration,
991              timing_function,
992              iteration_count,
993              direction,
994              play_state,
995              delay,
996              fill_mode,
997              timeline: if timeline_property.is_some() {
998                AnimationTimeline::Auto
999              } else {
1000                timeline
1001              },
1002            }
1003          },
1004        )
1005        .collect();
1006        let prefix = context.targets.prefixes(intersection, Feature::Animation);
1007        dest.push(Property::Animation(animations, prefix));
1008        names_vp.remove(intersection);
1009        durations_vp.remove(intersection);
1010        timing_functions_vp.remove(intersection);
1011        iteration_counts_vp.remove(intersection);
1012        directions_vp.remove(intersection);
1013        play_states_vp.remove(intersection);
1014        delays_vp.remove(intersection);
1015        fill_modes_vp.remove(intersection);
1016
1017        if let Some(p) = timeline_property {
1018          dest.push(p);
1019        }
1020        timelines_value = None;
1021      }
1022    }
1023
1024    macro_rules! prop {
1025      ($var: ident, $property: ident) => {
1026        if let Some((val, vp)) = $var {
1027          if !vp.is_empty() {
1028            let prefix = context.targets.prefixes(vp, Feature::$property);
1029            dest.push(Property::$property(val, prefix))
1030          }
1031        }
1032      };
1033    }
1034
1035    prop!(names, AnimationName);
1036    prop!(durations, AnimationDuration);
1037    prop!(timing_functions, AnimationTimingFunction);
1038    prop!(iteration_counts, AnimationIterationCount);
1039    prop!(directions, AnimationDirection);
1040    prop!(play_states, AnimationPlayState);
1041    prop!(delays, AnimationDelay);
1042    prop!(fill_modes, AnimationFillMode);
1043
1044    if let Some(val) = timelines_value {
1045      dest.push(Property::AnimationTimeline(val));
1046    }
1047
1048    match (range_starts, range_ends) {
1049      (Some(range_starts), Some(range_ends)) => {
1050        if range_starts.len() == range_ends.len() {
1051          dest.push(Property::AnimationRange(
1052            range_starts
1053              .into_iter()
1054              .zip(range_ends.into_iter())
1055              .map(|(start, end)| AnimationRange { start, end })
1056              .collect(),
1057          ));
1058        } else {
1059          dest.push(Property::AnimationRangeStart(range_starts));
1060          dest.push(Property::AnimationRangeEnd(range_ends));
1061        }
1062      }
1063      (range_starts, range_ends) => {
1064        if let Some(range_starts) = range_starts {
1065          dest.push(Property::AnimationRangeStart(range_starts));
1066        }
1067
1068        if let Some(range_ends) = range_ends {
1069          dest.push(Property::AnimationRangeEnd(range_ends));
1070        }
1071      }
1072    }
1073  }
1074}
1075
1076#[inline]
1077fn is_animation_property(property_id: &PropertyId) -> bool {
1078  match property_id {
1079    PropertyId::AnimationName(_)
1080    | PropertyId::AnimationDuration(_)
1081    | PropertyId::AnimationTimingFunction(_)
1082    | PropertyId::AnimationIterationCount(_)
1083    | PropertyId::AnimationDirection(_)
1084    | PropertyId::AnimationPlayState(_)
1085    | PropertyId::AnimationDelay(_)
1086    | PropertyId::AnimationFillMode(_)
1087    | PropertyId::AnimationComposition
1088    | PropertyId::AnimationTimeline
1089    | PropertyId::AnimationRange
1090    | PropertyId::AnimationRangeStart
1091    | PropertyId::AnimationRangeEnd
1092    | PropertyId::Animation(_) => true,
1093    _ => false,
1094  }
1095}