| //! CSS properties related to keyframe animations. |
| |
| use std::borrow::Cow; |
| |
| use crate::context::PropertyHandlerContext; |
| use crate::declaration::{DeclarationBlock, DeclarationList}; |
| use crate::error::{ParserError, PrinterError}; |
| use crate::macros::*; |
| use crate::prefixes::Feature; |
| use crate::printer::Printer; |
| use crate::properties::{Property, PropertyId, TokenOrValue, VendorPrefix}; |
| use crate::traits::{Parse, PropertyHandler, Shorthand, ToCss, Zero}; |
| use crate::values::ident::DashedIdent; |
| use crate::values::number::CSSNumber; |
| use crate::values::percentage::Percentage; |
| use crate::values::size::Size2D; |
| use crate::values::string::CSSString; |
| use crate::values::{easing::EasingFunction, ident::CustomIdent, time::Time}; |
| #[cfg(feature = "visitor")] |
| use crate::visitor::Visit; |
| use cssparser::*; |
| use itertools::izip; |
| use smallvec::SmallVec; |
| |
| use super::{LengthPercentage, LengthPercentageOrAuto}; |
| |
| /// A value for the [animation-name](https://drafts.csswg.org/css-animations/#animation-name) property. |
| #[derive(Debug, Clone, PartialEq, Parse)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| pub enum AnimationName<'i> { |
| /// The `none` keyword. |
| None, |
| /// An identifier of a `@keyframes` rule. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| Ident(CustomIdent<'i>), |
| /// A `<string>` name of a `@keyframes` rule. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| String(CSSString<'i>), |
| } |
| |
| impl<'i> ToCss for AnimationName<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| let css_module_animation_enabled = |
| dest.css_module.as_ref().map_or(false, |css_module| css_module.config.animation); |
| |
| match self { |
| AnimationName::None => dest.write_str("none"), |
| AnimationName::Ident(s) => { |
| if css_module_animation_enabled { |
| if let Some(css_module) = &mut dest.css_module { |
| css_module.reference(&s.0, dest.loc.source_index) |
| } |
| } |
| s.to_css_with_options(dest, css_module_animation_enabled) |
| } |
| AnimationName::String(s) => { |
| if css_module_animation_enabled { |
| if let Some(css_module) = &mut dest.css_module { |
| css_module.reference(&s, dest.loc.source_index) |
| } |
| } |
| |
| // CSS-wide keywords and `none` cannot remove quotes. |
| match_ignore_ascii_case! { &*s, |
| "none" | "initial" | "inherit" | "unset" | "default" | "revert" | "revert-layer" => { |
| serialize_string(&s, dest)?; |
| Ok(()) |
| }, |
| _ => { |
| dest.write_ident(s.as_ref(), css_module_animation_enabled) |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /// A list of animation names. |
| pub type AnimationNameList<'i> = SmallVec<[AnimationName<'i>; 1]>; |
| |
| /// A value for the [animation-iteration-count](https://drafts.csswg.org/css-animations/#animation-iteration-count) property. |
| #[derive(Debug, Clone, PartialEq, Parse, ToCss)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum AnimationIterationCount { |
| /// The animation will repeat the specified number of times. |
| Number(CSSNumber), |
| /// The animation will repeat forever. |
| Infinite, |
| } |
| |
| impl Default for AnimationIterationCount { |
| fn default() -> Self { |
| AnimationIterationCount::Number(1.0) |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [animation-direction](https://drafts.csswg.org/css-animations/#animation-direction) property. |
| pub enum AnimationDirection { |
| /// The animation is played as specified |
| Normal, |
| /// The animation is played in reverse. |
| Reverse, |
| /// The animation iterations alternate between forward and reverse. |
| Alternate, |
| /// The animation iterations alternate between forward and reverse, with reverse occurring first. |
| AlternateReverse, |
| } |
| } |
| |
| impl Default for AnimationDirection { |
| fn default() -> Self { |
| AnimationDirection::Normal |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [animation-play-state](https://drafts.csswg.org/css-animations/#animation-play-state) property. |
| pub enum AnimationPlayState { |
| /// The animation is playing. |
| Running, |
| /// The animation is paused. |
| Paused, |
| } |
| } |
| |
| impl Default for AnimationPlayState { |
| fn default() -> Self { |
| AnimationPlayState::Running |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [animation-fill-mode](https://drafts.csswg.org/css-animations/#animation-fill-mode) property. |
| pub enum AnimationFillMode { |
| /// The animation has no effect while not playing. |
| None, |
| /// After the animation, the ending values are applied. |
| Forwards, |
| /// Before the animation, the starting values are applied. |
| Backwards, |
| /// Both forwards and backwards apply. |
| Both, |
| } |
| } |
| |
| impl Default for AnimationFillMode { |
| fn default() -> Self { |
| AnimationFillMode::None |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [animation-composition](https://drafts.csswg.org/css-animations-2/#animation-composition) property. |
| pub enum AnimationComposition { |
| /// The result of compositing the effect value with the underlying value is simply the effect value. |
| Replace, |
| /// The effect value is added to the underlying value. |
| Add, |
| /// The effect value is accumulated onto the underlying value. |
| Accumulate, |
| } |
| } |
| |
| /// A value for the [animation-timeline](https://drafts.csswg.org/css-animations-2/#animation-timeline) property. |
| #[derive(Debug, Clone, PartialEq, Parse, ToCss)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum AnimationTimeline<'i> { |
| /// The animation’s timeline is a DocumentTimeline, more specifically the default document timeline. |
| Auto, |
| /// The animation is not associated with a timeline. |
| None, |
| /// A timeline referenced by name. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| DashedIdent(DashedIdent<'i>), |
| /// The scroll() function. |
| Scroll(ScrollTimeline), |
| /// The view() function. |
| View(ViewTimeline), |
| } |
| |
| impl<'i> Default for AnimationTimeline<'i> { |
| fn default() -> Self { |
| AnimationTimeline::Auto |
| } |
| } |
| |
| /// The [scroll()](https://drafts.csswg.org/scroll-animations-1/#scroll-notation) function. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct ScrollTimeline { |
| /// Specifies which element to use as the scroll container. |
| pub scroller: Scroller, |
| /// Specifies which axis of the scroll container to use as the progress for the timeline. |
| pub axis: ScrollAxis, |
| } |
| |
| impl<'i> Parse<'i> for ScrollTimeline { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| input.expect_function_matching("scroll")?; |
| input.parse_nested_block(|input| { |
| let mut scroller = None; |
| let mut axis = None; |
| loop { |
| if scroller.is_none() { |
| scroller = input.try_parse(Scroller::parse).ok(); |
| } |
| |
| if axis.is_none() { |
| axis = input.try_parse(ScrollAxis::parse).ok(); |
| if axis.is_some() { |
| continue; |
| } |
| } |
| break; |
| } |
| |
| Ok(ScrollTimeline { |
| scroller: scroller.unwrap_or_default(), |
| axis: axis.unwrap_or_default(), |
| }) |
| }) |
| } |
| } |
| |
| impl ToCss for ScrollTimeline { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| dest.write_str("scroll(")?; |
| |
| let mut needs_space = false; |
| if self.scroller != Scroller::default() { |
| self.scroller.to_css(dest)?; |
| needs_space = true; |
| } |
| |
| if self.axis != ScrollAxis::default() { |
| if needs_space { |
| dest.write_char(' ')?; |
| } |
| self.axis.to_css(dest)?; |
| } |
| |
| dest.write_char(')') |
| } |
| } |
| |
| enum_property! { |
| /// A scroller, used in the `scroll()` function. |
| pub enum Scroller { |
| /// Specifies to use the document viewport as the scroll container. |
| "root": Root, |
| /// Specifies to use the nearest ancestor scroll container. |
| "nearest": Nearest, |
| /// Specifies to use the element’s own principal box as the scroll container. |
| "self": SelfElement, |
| } |
| } |
| |
| impl Default for Scroller { |
| fn default() -> Self { |
| Scroller::Nearest |
| } |
| } |
| |
| enum_property! { |
| /// A scroll axis, used in the `scroll()` function. |
| pub enum ScrollAxis { |
| /// Specifies to use the measure of progress along the block axis of the scroll container. |
| Block, |
| /// Specifies to use the measure of progress along the inline axis of the scroll container. |
| Inline, |
| /// Specifies to use the measure of progress along the horizontal axis of the scroll container. |
| X, |
| /// Specifies to use the measure of progress along the vertical axis of the scroll container. |
| Y, |
| } |
| } |
| |
| impl Default for ScrollAxis { |
| fn default() -> Self { |
| ScrollAxis::Block |
| } |
| } |
| |
| /// The [view()](https://drafts.csswg.org/scroll-animations-1/#view-notation) function. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct ViewTimeline { |
| /// Specifies which axis of the scroll container to use as the progress for the timeline. |
| pub axis: ScrollAxis, |
| /// Provides an adjustment of the view progress visibility range. |
| pub inset: Size2D<LengthPercentageOrAuto>, |
| } |
| |
| impl<'i> Parse<'i> for ViewTimeline { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| input.expect_function_matching("view")?; |
| input.parse_nested_block(|input| { |
| let mut axis = None; |
| let mut inset = None; |
| loop { |
| if axis.is_none() { |
| axis = input.try_parse(ScrollAxis::parse).ok(); |
| } |
| |
| if inset.is_none() { |
| inset = input.try_parse(Size2D::parse).ok(); |
| if inset.is_some() { |
| continue; |
| } |
| } |
| break; |
| } |
| |
| Ok(ViewTimeline { |
| axis: axis.unwrap_or_default(), |
| inset: inset.unwrap_or(Size2D(LengthPercentageOrAuto::Auto, LengthPercentageOrAuto::Auto)), |
| }) |
| }) |
| } |
| } |
| |
| impl ToCss for ViewTimeline { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| dest.write_str("view(")?; |
| let mut needs_space = false; |
| if self.axis != ScrollAxis::default() { |
| self.axis.to_css(dest)?; |
| needs_space = true; |
| } |
| |
| if self.inset.0 != LengthPercentageOrAuto::Auto || self.inset.1 != LengthPercentageOrAuto::Auto { |
| if needs_space { |
| dest.write_char(' ')?; |
| } |
| self.inset.to_css(dest)?; |
| } |
| |
| dest.write_char(')') |
| } |
| } |
| |
| /// A [view progress timeline range](https://drafts.csswg.org/scroll-animations/#view-timelines-ranges) |
| #[derive(Debug, Clone, PartialEq, Parse, ToCss)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum TimelineRangeName { |
| /// Represents the full range of the view progress timeline. |
| Cover, |
| /// Represents the range during which the principal box is either fully contained by, |
| /// or fully covers, its view progress visibility range within the scrollport. |
| Contain, |
| /// Represents the range during which the principal box is entering the view progress visibility range. |
| Entry, |
| /// Represents the range during which the principal box is exiting the view progress visibility range. |
| Exit, |
| /// Represents the range during which the principal box crosses the end border edge. |
| EntryCrossing, |
| /// Represents the range during which the principal box crosses the start border edge. |
| ExitCrossing, |
| } |
| |
| /// A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) |
| /// or [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(rename_all = "lowercase") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum AnimationAttachmentRange { |
| /// The start of the animation’s attachment range is the start of its associated timeline. |
| Normal, |
| /// The animation attachment range starts at the specified point on the timeline measuring from the start of the timeline. |
| #[cfg_attr(feature = "serde", serde(untagged))] |
| LengthPercentage(LengthPercentage), |
| /// The animation attachment range starts at the specified point on the timeline measuring from the start of the specified named timeline range. |
| #[cfg_attr(feature = "serde", serde(untagged))] |
| TimelineRange { |
| /// The name of the timeline range. |
| name: TimelineRangeName, |
| /// The offset from the start of the named timeline range. |
| offset: LengthPercentage, |
| }, |
| } |
| |
| impl<'i> AnimationAttachmentRange { |
| fn parse<'t>(input: &mut Parser<'i, 't>, default: f32) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { |
| return Ok(AnimationAttachmentRange::Normal); |
| } |
| |
| if let Ok(val) = input.try_parse(LengthPercentage::parse) { |
| return Ok(AnimationAttachmentRange::LengthPercentage(val)); |
| } |
| |
| let name = TimelineRangeName::parse(input)?; |
| let offset = input |
| .try_parse(LengthPercentage::parse) |
| .unwrap_or(LengthPercentage::Percentage(Percentage(default))); |
| Ok(AnimationAttachmentRange::TimelineRange { name, offset }) |
| } |
| |
| fn to_css<W>(&self, dest: &mut Printer<W>, default: f32) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| match self { |
| Self::Normal => dest.write_str("normal"), |
| Self::LengthPercentage(val) => val.to_css(dest), |
| Self::TimelineRange { name, offset } => { |
| name.to_css(dest)?; |
| if *offset != LengthPercentage::Percentage(Percentage(default)) { |
| dest.write_char(' ')?; |
| offset.to_css(dest)?; |
| } |
| Ok(()) |
| } |
| } |
| } |
| } |
| |
| impl Default for AnimationAttachmentRange { |
| fn default() -> Self { |
| AnimationAttachmentRange::Normal |
| } |
| } |
| |
| /// A value for the [animation-range-start](https://drafts.csswg.org/scroll-animations/#animation-range-start) property. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct AnimationRangeStart(pub AnimationAttachmentRange); |
| |
| impl<'i> Parse<'i> for AnimationRangeStart { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let range = AnimationAttachmentRange::parse(input, 0.0)?; |
| Ok(Self(range)) |
| } |
| } |
| |
| impl ToCss for AnimationRangeStart { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| self.0.to_css(dest, 0.0) |
| } |
| } |
| |
| /// A value for the [animation-range-end](https://drafts.csswg.org/scroll-animations/#animation-range-end) property. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct AnimationRangeEnd(pub AnimationAttachmentRange); |
| |
| impl<'i> Parse<'i> for AnimationRangeEnd { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let range = AnimationAttachmentRange::parse(input, 1.0)?; |
| Ok(Self(range)) |
| } |
| } |
| |
| impl ToCss for AnimationRangeEnd { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| self.0.to_css(dest, 1.0) |
| } |
| } |
| |
| /// A value for the [animation-range](https://drafts.csswg.org/scroll-animations/#animation-range) shorthand property. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct AnimationRange { |
| /// The start of the animation's attachment range. |
| pub start: AnimationRangeStart, |
| /// The end of the animation's attachment range. |
| pub end: AnimationRangeEnd, |
| } |
| |
| impl<'i> Parse<'i> for AnimationRange { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let start = AnimationRangeStart::parse(input)?; |
| let end = input |
| .try_parse(AnimationRangeStart::parse) |
| .map(|r| AnimationRangeEnd(r.0)) |
| .unwrap_or_else(|_| { |
| // If <'animation-range-end'> is omitted and <'animation-range-start'> includes a <timeline-range-name> component, then |
| // animation-range-end is set to that same <timeline-range-name> and 100%. Otherwise, any omitted longhand is set to its initial value. |
| match &start.0 { |
| AnimationAttachmentRange::TimelineRange { name, .. } => { |
| AnimationRangeEnd(AnimationAttachmentRange::TimelineRange { |
| name: name.clone(), |
| offset: LengthPercentage::Percentage(Percentage(1.0)), |
| }) |
| } |
| _ => AnimationRangeEnd(AnimationAttachmentRange::default()), |
| } |
| }); |
| Ok(AnimationRange { start, end }) |
| } |
| } |
| |
| impl ToCss for AnimationRange { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| self.start.to_css(dest)?; |
| |
| let omit_end = match (&self.start.0, &self.end.0) { |
| ( |
| AnimationAttachmentRange::TimelineRange { name: start_name, .. }, |
| AnimationAttachmentRange::TimelineRange { |
| name: end_name, |
| offset: end_offset, |
| }, |
| ) => start_name == end_name && *end_offset == LengthPercentage::Percentage(Percentage(1.0)), |
| (_, end) => *end == AnimationAttachmentRange::default(), |
| }; |
| |
| if !omit_end { |
| dest.write_char(' ')?; |
| self.end.to_css(dest)?; |
| } |
| Ok(()) |
| } |
| } |
| |
| define_list_shorthand! { |
| /// A value for the [animation](https://drafts.csswg.org/css-animations/#animation) shorthand property. |
| pub struct Animation<'i>(VendorPrefix) { |
| /// The animation name. |
| #[cfg_attr(feature = "serde", serde(borrow))] |
| name: AnimationName(AnimationName<'i>, VendorPrefix), |
| /// The animation duration. |
| duration: AnimationDuration(Time, VendorPrefix), |
| /// The easing function for the animation. |
| timing_function: AnimationTimingFunction(EasingFunction, VendorPrefix), |
| /// The number of times the animation will run. |
| iteration_count: AnimationIterationCount(AnimationIterationCount, VendorPrefix), |
| /// The direction of the animation. |
| direction: AnimationDirection(AnimationDirection, VendorPrefix), |
| /// The current play state of the animation. |
| play_state: AnimationPlayState(AnimationPlayState, VendorPrefix), |
| /// The animation delay. |
| delay: AnimationDelay(Time, VendorPrefix), |
| /// The animation fill mode. |
| fill_mode: AnimationFillMode(AnimationFillMode, VendorPrefix), |
| /// The animation timeline. |
| timeline: AnimationTimeline(AnimationTimeline<'i>), |
| } |
| } |
| |
| impl<'i> Parse<'i> for Animation<'i> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let mut name = None; |
| let mut duration = None; |
| let mut timing_function = None; |
| let mut iteration_count = None; |
| let mut direction = None; |
| let mut play_state = None; |
| let mut delay = None; |
| let mut fill_mode = None; |
| let mut timeline = None; |
| |
| macro_rules! parse_prop { |
| ($var: ident, $type: ident) => { |
| if $var.is_none() { |
| if let Ok(value) = input.try_parse($type::parse) { |
| $var = Some(value); |
| continue; |
| } |
| } |
| }; |
| } |
| |
| loop { |
| parse_prop!(duration, Time); |
| parse_prop!(timing_function, EasingFunction); |
| parse_prop!(delay, Time); |
| parse_prop!(iteration_count, AnimationIterationCount); |
| parse_prop!(direction, AnimationDirection); |
| parse_prop!(fill_mode, AnimationFillMode); |
| parse_prop!(play_state, AnimationPlayState); |
| parse_prop!(name, AnimationName); |
| parse_prop!(timeline, AnimationTimeline); |
| break; |
| } |
| |
| Ok(Animation { |
| name: name.unwrap_or(AnimationName::None), |
| duration: duration.unwrap_or(Time::Seconds(0.0)), |
| timing_function: timing_function.unwrap_or(EasingFunction::Ease), |
| iteration_count: iteration_count.unwrap_or(AnimationIterationCount::Number(1.0)), |
| direction: direction.unwrap_or(AnimationDirection::Normal), |
| play_state: play_state.unwrap_or(AnimationPlayState::Running), |
| delay: delay.unwrap_or(Time::Seconds(0.0)), |
| fill_mode: fill_mode.unwrap_or(AnimationFillMode::None), |
| timeline: timeline.unwrap_or(AnimationTimeline::Auto), |
| }) |
| } |
| } |
| |
| impl<'i> ToCss for Animation<'i> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| match &self.name { |
| AnimationName::None => {} |
| AnimationName::Ident(CustomIdent(name)) | AnimationName::String(CSSString(name)) => { |
| if !self.duration.is_zero() || !self.delay.is_zero() { |
| self.duration.to_css(dest)?; |
| dest.write_char(' ')?; |
| } |
| |
| if !self.timing_function.is_ease() || EasingFunction::is_ident(&name) { |
| self.timing_function.to_css(dest)?; |
| dest.write_char(' ')?; |
| } |
| |
| if !self.delay.is_zero() { |
| self.delay.to_css(dest)?; |
| dest.write_char(' ')?; |
| } |
| |
| if self.iteration_count != AnimationIterationCount::default() || name.as_ref() == "infinite" { |
| self.iteration_count.to_css(dest)?; |
| dest.write_char(' ')?; |
| } |
| |
| if self.direction != AnimationDirection::default() || AnimationDirection::parse_string(&name).is_ok() { |
| self.direction.to_css(dest)?; |
| dest.write_char(' ')?; |
| } |
| |
| if self.fill_mode != AnimationFillMode::default() |
| || (!name.eq_ignore_ascii_case("none") && AnimationFillMode::parse_string(&name).is_ok()) |
| { |
| self.fill_mode.to_css(dest)?; |
| dest.write_char(' ')?; |
| } |
| |
| if self.play_state != AnimationPlayState::default() || AnimationPlayState::parse_string(&name).is_ok() { |
| self.play_state.to_css(dest)?; |
| dest.write_char(' ')?; |
| } |
| } |
| } |
| |
| // Eventually we could output a string here to avoid duplicating some properties above. |
| // Chrome does not yet support strings, however. |
| self.name.to_css(dest)?; |
| |
| if self.name != AnimationName::None && self.timeline != AnimationTimeline::default() { |
| dest.write_char(' ')?; |
| self.timeline.to_css(dest)?; |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| /// A list of animations. |
| pub type AnimationList<'i> = SmallVec<[Animation<'i>; 1]>; |
| |
| #[derive(Default)] |
| pub(crate) struct AnimationHandler<'i> { |
| names: Option<(SmallVec<[AnimationName<'i>; 1]>, VendorPrefix)>, |
| durations: Option<(SmallVec<[Time; 1]>, VendorPrefix)>, |
| timing_functions: Option<(SmallVec<[EasingFunction; 1]>, VendorPrefix)>, |
| iteration_counts: Option<(SmallVec<[AnimationIterationCount; 1]>, VendorPrefix)>, |
| directions: Option<(SmallVec<[AnimationDirection; 1]>, VendorPrefix)>, |
| play_states: Option<(SmallVec<[AnimationPlayState; 1]>, VendorPrefix)>, |
| delays: Option<(SmallVec<[Time; 1]>, VendorPrefix)>, |
| fill_modes: Option<(SmallVec<[AnimationFillMode; 1]>, VendorPrefix)>, |
| timelines: Option<SmallVec<[AnimationTimeline<'i>; 1]>>, |
| range_starts: Option<SmallVec<[AnimationRangeStart; 1]>>, |
| range_ends: Option<SmallVec<[AnimationRangeEnd; 1]>>, |
| has_any: bool, |
| } |
| |
| impl<'i> PropertyHandler<'i> for AnimationHandler<'i> { |
| fn handle_property( |
| &mut self, |
| property: &Property<'i>, |
| dest: &mut DeclarationList<'i>, |
| context: &mut PropertyHandlerContext<'i, '_>, |
| ) -> bool { |
| macro_rules! maybe_flush { |
| ($prop: ident, $val: expr, $vp: ident) => {{ |
| // If two vendor prefixes for the same property have different |
| // values, we need to flush what we have immediately to preserve order. |
| if let Some((val, prefixes)) = &self.$prop { |
| if val != $val && !prefixes.contains(*$vp) { |
| self.flush(dest, context); |
| } |
| } |
| }}; |
| } |
| |
| macro_rules! property { |
| ($prop: ident, $val: expr, $vp: ident) => {{ |
| maybe_flush!($prop, $val, $vp); |
| |
| // Otherwise, update the value and add the prefix. |
| if let Some((val, prefixes)) = &mut self.$prop { |
| *val = $val.clone(); |
| *prefixes |= *$vp; |
| } else { |
| self.$prop = Some(($val.clone(), *$vp)); |
| self.has_any = true; |
| } |
| }}; |
| } |
| |
| match property { |
| Property::AnimationName(val, vp) => property!(names, val, vp), |
| Property::AnimationDuration(val, vp) => property!(durations, val, vp), |
| Property::AnimationTimingFunction(val, vp) => property!(timing_functions, val, vp), |
| Property::AnimationIterationCount(val, vp) => property!(iteration_counts, val, vp), |
| Property::AnimationDirection(val, vp) => property!(directions, val, vp), |
| Property::AnimationPlayState(val, vp) => property!(play_states, val, vp), |
| Property::AnimationDelay(val, vp) => property!(delays, val, vp), |
| Property::AnimationFillMode(val, vp) => property!(fill_modes, val, vp), |
| Property::AnimationTimeline(val) => { |
| self.timelines = Some(val.clone()); |
| self.has_any = true; |
| } |
| Property::AnimationRangeStart(val) => { |
| self.range_starts = Some(val.clone()); |
| self.has_any = true; |
| } |
| Property::AnimationRangeEnd(val) => { |
| self.range_ends = Some(val.clone()); |
| self.has_any = true; |
| } |
| Property::AnimationRange(val) => { |
| self.range_starts = Some(val.iter().map(|v| v.start.clone()).collect()); |
| self.range_ends = Some(val.iter().map(|v| v.end.clone()).collect()); |
| self.has_any = true; |
| } |
| Property::Animation(val, vp) => { |
| let names = val.iter().map(|b| b.name.clone()).collect(); |
| maybe_flush!(names, &names, vp); |
| |
| let durations = val.iter().map(|b| b.duration.clone()).collect(); |
| maybe_flush!(durations, &durations, vp); |
| |
| let timing_functions = val.iter().map(|b| b.timing_function.clone()).collect(); |
| maybe_flush!(timing_functions, &timing_functions, vp); |
| |
| let iteration_counts = val.iter().map(|b| b.iteration_count.clone()).collect(); |
| maybe_flush!(iteration_counts, &iteration_counts, vp); |
| |
| let directions = val.iter().map(|b| b.direction.clone()).collect(); |
| maybe_flush!(directions, &directions, vp); |
| |
| let play_states = val.iter().map(|b| b.play_state.clone()).collect(); |
| maybe_flush!(play_states, &play_states, vp); |
| |
| let delays = val.iter().map(|b| b.delay.clone()).collect(); |
| maybe_flush!(delays, &delays, vp); |
| |
| let fill_modes = val.iter().map(|b| b.fill_mode.clone()).collect(); |
| maybe_flush!(fill_modes, &fill_modes, vp); |
| |
| self.timelines = Some(val.iter().map(|b| b.timeline.clone()).collect()); |
| |
| property!(names, &names, vp); |
| property!(durations, &durations, vp); |
| property!(timing_functions, &timing_functions, vp); |
| property!(iteration_counts, &iteration_counts, vp); |
| property!(directions, &directions, vp); |
| property!(play_states, &play_states, vp); |
| property!(delays, &delays, vp); |
| property!(fill_modes, &fill_modes, vp); |
| |
| // The animation shorthand resets animation-range |
| // https://drafts.csswg.org/scroll-animations/#named-range-animation-declaration |
| self.range_starts = None; |
| self.range_ends = None; |
| } |
| Property::Unparsed(val) if is_animation_property(&val.property_id) => { |
| let mut val = Cow::Borrowed(val); |
| if matches!(val.property_id, PropertyId::Animation(_)) { |
| use crate::properties::custom::Token; |
| |
| // Find an identifier that isn't a keyword and replace it with an |
| // AnimationName token so it is scoped in CSS modules. |
| for token in &mut val.to_mut().value.0 { |
| match token { |
| TokenOrValue::Token(Token::Ident(id)) => { |
| if AnimationDirection::parse_string(&id).is_err() |
| && AnimationPlayState::parse_string(&id).is_err() |
| && AnimationFillMode::parse_string(&id).is_err() |
| && !EasingFunction::is_ident(&id) |
| && id.as_ref() != "infinite" |
| && id.as_ref() != "auto" |
| { |
| *token = TokenOrValue::AnimationName(AnimationName::Ident(CustomIdent(id.clone()))); |
| } |
| } |
| TokenOrValue::Token(Token::String(s)) => { |
| *token = TokenOrValue::AnimationName(AnimationName::String(CSSString(s.clone()))); |
| } |
| _ => {} |
| } |
| } |
| |
| self.range_starts = None; |
| self.range_ends = None; |
| } |
| |
| self.flush(dest, context); |
| dest.push(Property::Unparsed( |
| val.get_prefixed(context.targets, Feature::Animation), |
| )); |
| } |
| _ => return false, |
| } |
| |
| true |
| } |
| |
| fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) { |
| self.flush(dest, context); |
| } |
| } |
| |
| impl<'i> AnimationHandler<'i> { |
| fn flush(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) { |
| if !self.has_any { |
| return; |
| } |
| |
| self.has_any = false; |
| |
| let mut names = std::mem::take(&mut self.names); |
| let mut durations = std::mem::take(&mut self.durations); |
| let mut timing_functions = std::mem::take(&mut self.timing_functions); |
| let mut iteration_counts = std::mem::take(&mut self.iteration_counts); |
| let mut directions = std::mem::take(&mut self.directions); |
| let mut play_states = std::mem::take(&mut self.play_states); |
| let mut delays = std::mem::take(&mut self.delays); |
| let mut fill_modes = std::mem::take(&mut self.fill_modes); |
| let mut timelines_value = std::mem::take(&mut self.timelines); |
| let range_starts = std::mem::take(&mut self.range_starts); |
| let range_ends = std::mem::take(&mut self.range_ends); |
| |
| if let ( |
| Some((names, names_vp)), |
| Some((durations, durations_vp)), |
| Some((timing_functions, timing_functions_vp)), |
| Some((iteration_counts, iteration_counts_vp)), |
| Some((directions, directions_vp)), |
| Some((play_states, play_states_vp)), |
| Some((delays, delays_vp)), |
| Some((fill_modes, fill_modes_vp)), |
| ) = ( |
| &mut names, |
| &mut durations, |
| &mut timing_functions, |
| &mut iteration_counts, |
| &mut directions, |
| &mut play_states, |
| &mut delays, |
| &mut fill_modes, |
| ) { |
| // Only use shorthand syntax if the number of animations matches on all properties. |
| let len = names.len(); |
| let intersection = *names_vp |
| & *durations_vp |
| & *timing_functions_vp |
| & *iteration_counts_vp |
| & *directions_vp |
| & *play_states_vp |
| & *delays_vp |
| & *fill_modes_vp; |
| let mut timelines = if let Some(timelines) = &mut timelines_value { |
| Cow::Borrowed(timelines) |
| } else if !intersection.contains(VendorPrefix::None) { |
| // Prefixed animation shorthand does not support animation-timeline |
| Cow::Owned(std::iter::repeat(AnimationTimeline::Auto).take(len).collect()) |
| } else { |
| Cow::Owned(SmallVec::new()) |
| }; |
| |
| if !intersection.is_empty() |
| && durations.len() == len |
| && timing_functions.len() == len |
| && iteration_counts.len() == len |
| && directions.len() == len |
| && play_states.len() == len |
| && delays.len() == len |
| && fill_modes.len() == len |
| && timelines.len() == len |
| { |
| let timeline_property = if timelines.iter().any(|t| *t != AnimationTimeline::Auto) |
| && (intersection != VendorPrefix::None |
| || !context |
| .targets |
| .is_compatible(crate::compat::Feature::AnimationTimelineShorthand)) |
| { |
| Some(Property::AnimationTimeline(timelines.clone().into_owned())) |
| } else { |
| None |
| }; |
| |
| let animations = izip!( |
| names.drain(..), |
| durations.drain(..), |
| timing_functions.drain(..), |
| iteration_counts.drain(..), |
| directions.drain(..), |
| play_states.drain(..), |
| delays.drain(..), |
| fill_modes.drain(..), |
| timelines.to_mut().drain(..) |
| ) |
| .map( |
| |( |
| name, |
| duration, |
| timing_function, |
| iteration_count, |
| direction, |
| play_state, |
| delay, |
| fill_mode, |
| timeline, |
| )| { |
| Animation { |
| name, |
| duration, |
| timing_function, |
| iteration_count, |
| direction, |
| play_state, |
| delay, |
| fill_mode, |
| timeline: if timeline_property.is_some() { |
| AnimationTimeline::Auto |
| } else { |
| timeline |
| }, |
| } |
| }, |
| ) |
| .collect(); |
| let prefix = context.targets.prefixes(intersection, Feature::Animation); |
| dest.push(Property::Animation(animations, prefix)); |
| names_vp.remove(intersection); |
| durations_vp.remove(intersection); |
| timing_functions_vp.remove(intersection); |
| iteration_counts_vp.remove(intersection); |
| directions_vp.remove(intersection); |
| play_states_vp.remove(intersection); |
| delays_vp.remove(intersection); |
| fill_modes_vp.remove(intersection); |
| |
| if let Some(p) = timeline_property { |
| dest.push(p); |
| } |
| timelines_value = None; |
| } |
| } |
| |
| macro_rules! prop { |
| ($var: ident, $property: ident) => { |
| if let Some((val, vp)) = $var { |
| if !vp.is_empty() { |
| let prefix = context.targets.prefixes(vp, Feature::$property); |
| dest.push(Property::$property(val, prefix)) |
| } |
| } |
| }; |
| } |
| |
| prop!(names, AnimationName); |
| prop!(durations, AnimationDuration); |
| prop!(timing_functions, AnimationTimingFunction); |
| prop!(iteration_counts, AnimationIterationCount); |
| prop!(directions, AnimationDirection); |
| prop!(play_states, AnimationPlayState); |
| prop!(delays, AnimationDelay); |
| prop!(fill_modes, AnimationFillMode); |
| |
| if let Some(val) = timelines_value { |
| dest.push(Property::AnimationTimeline(val)); |
| } |
| |
| match (range_starts, range_ends) { |
| (Some(range_starts), Some(range_ends)) => { |
| if range_starts.len() == range_ends.len() { |
| dest.push(Property::AnimationRange( |
| range_starts |
| .into_iter() |
| .zip(range_ends.into_iter()) |
| .map(|(start, end)| AnimationRange { start, end }) |
| .collect(), |
| )); |
| } else { |
| dest.push(Property::AnimationRangeStart(range_starts)); |
| dest.push(Property::AnimationRangeEnd(range_ends)); |
| } |
| } |
| (range_starts, range_ends) => { |
| if let Some(range_starts) = range_starts { |
| dest.push(Property::AnimationRangeStart(range_starts)); |
| } |
| |
| if let Some(range_ends) = range_ends { |
| dest.push(Property::AnimationRangeEnd(range_ends)); |
| } |
| } |
| } |
| } |
| } |
| |
| #[inline] |
| fn is_animation_property(property_id: &PropertyId) -> bool { |
| match property_id { |
| PropertyId::AnimationName(_) |
| | PropertyId::AnimationDuration(_) |
| | PropertyId::AnimationTimingFunction(_) |
| | PropertyId::AnimationIterationCount(_) |
| | PropertyId::AnimationDirection(_) |
| | PropertyId::AnimationPlayState(_) |
| | PropertyId::AnimationDelay(_) |
| | PropertyId::AnimationFillMode(_) |
| | PropertyId::AnimationComposition |
| | PropertyId::AnimationTimeline |
| | PropertyId::AnimationRange |
| | PropertyId::AnimationRangeStart |
| | PropertyId::AnimationRangeEnd |
| | PropertyId::Animation(_) => true, |
| _ => false, |
| } |
| } |