blob: 51bd1ea48c5eae18a88139c4a097ef5870c116e9 [file] [log] [blame] [edit]
//! 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,
}
}