1use std::{borrow::Cow, fmt};
2
3use cssparser::{
4 BasicParseErrorKind, ParseError as CssParseError, Parser, Token, match_ignore_ascii_case,
5 serialize_string,
6};
7use typed_builder::TypedBuilder;
8
9use crate::style::{
10 FromCss, FromCssStr, MakeComputed, ParseResult, ToCss, impl_comma_list_from_css, impl_css_enum,
11 next_is_comma, tw::TailwindPropertyParser, unexpected_token,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Default)]
16pub struct AnimationTime {
17 pub milliseconds: f32,
19}
20
21impl AnimationTime {
22 pub const fn from_milliseconds(milliseconds: f32) -> Self {
24 Self { milliseconds }
25 }
26}
27
28impl MakeComputed for AnimationTime {}
29
30impl<'i> FromCss<'i> for AnimationTime {
31 fn from_css(input: &mut Parser<'i>) -> ParseResult<'i, Self> {
32 let token = input.next()?;
33
34 match token {
35 Token::Dimension { value, unit, .. } => match_ignore_ascii_case! {unit.as_ref(),
36 "ms" => Ok(Self::from_milliseconds(*value)),
37 "s" => Ok(Self::from_milliseconds(*value * 1000.0)),
38 _ => Err(unexpected_token!(token)),
39 },
40 Token::Number { value, .. } if *value == 0.0 => Ok(Self::from_milliseconds(0.0)),
41 _ => Err(unexpected_token!(token)),
42 }
43 }
44
45 const VALID_TOKENS: &'static [&'static str] = &["<time>"];
46}
47
48pub(crate) type AnimationName = Option<String>;
50
51pub(crate) type AnimationNames = Box<[AnimationName]>;
53
54impl MakeComputed for AnimationNames {}
55
56impl_comma_list_from_css!(
57 AnimationNames,
58 AnimationName,
59 &["none", "<custom-ident>", "<string>",]
60);
61
62pub(crate) type AnimationDurations = Box<[AnimationTime]>;
64
65impl_comma_list_from_css!(AnimationDurations, AnimationTime);
66
67#[derive(Debug, Clone, Copy, PartialEq, Default)]
69#[non_exhaustive]
70pub enum AnimationTimingFunction {
71 Linear,
73 #[default]
75 Ease,
76 EaseIn,
78 EaseOut,
80 EaseInOut,
82 StepStart,
84 StepEnd,
86 Steps(u32, StepPosition),
88 CubicBezier(f32, f32, f32, f32),
90}
91
92impl MakeComputed for AnimationTimingFunction {}
93
94#[derive(Debug, Clone, Copy, PartialEq)]
96#[non_exhaustive]
97pub enum StepPosition {
98 Start,
100 End,
102 JumpBoth,
104 JumpNone,
106}
107
108impl<'i> FromCss<'i> for AnimationTimingFunction {
109 fn from_css(input: &mut Parser<'i>) -> ParseResult<'i, Self> {
110 if let Ok(function) = input.try_parse(parse_timing_keyword) {
111 return Ok(function);
112 }
113
114 if let Ok(function) = input.try_parse(parse_steps_function) {
115 return Ok(function);
116 }
117
118 input.expect_function_matching("cubic-bezier")?;
119 input.parse_nested_block(|input| {
120 let x1 = expect_number(input)?;
121 input.expect_comma()?;
122 let y1 = expect_number(input)?;
123 input.expect_comma()?;
124 let x2 = expect_number(input)?;
125 input.expect_comma()?;
126 let y2 = expect_number(input)?;
127
128 if !(0.0..=1.0).contains(&x1) || !(0.0..=1.0).contains(&x2) {
129 return Err(CssParseError::from_basic_kind(
130 BasicParseErrorKind::QualifiedRuleInvalid,
131 ));
132 }
133
134 Ok(Self::CubicBezier(x1, y1, x2, y2))
135 })
136 }
137
138 const VALID_TOKENS: &'static [&'static str] = &[
139 "linear",
140 "ease",
141 "ease-in",
142 "ease-out",
143 "ease-in-out",
144 "step-start",
145 "step-end",
146 "<steps()>",
147 "<cubic-bezier()>",
148 ];
149}
150
151pub(crate) type AnimationTimingFunctions = Box<[AnimationTimingFunction]>;
153
154impl_comma_list_from_css!(AnimationTimingFunctions, AnimationTimingFunction);
155
156#[derive(Debug, Clone, Copy, PartialEq)]
158#[non_exhaustive]
159pub enum AnimationIterationCount {
160 Number(f32),
162 Infinite,
164}
165
166impl Default for AnimationIterationCount {
167 fn default() -> Self {
168 Self::Number(1.0)
169 }
170}
171
172impl MakeComputed for AnimationIterationCount {}
173
174impl<'i> FromCss<'i> for AnimationIterationCount {
175 fn from_css(input: &mut Parser<'i>) -> ParseResult<'i, Self> {
176 if input
177 .try_parse(|parser| parser.expect_ident_matching("infinite"))
178 .is_ok()
179 {
180 return Ok(Self::Infinite);
181 }
182
183 let value = expect_number(input)?;
184 if value < 0.0 {
185 return Err(CssParseError::from_basic_kind(
186 BasicParseErrorKind::QualifiedRuleInvalid,
187 ));
188 }
189
190 Ok(Self::Number(value))
191 }
192
193 const VALID_TOKENS: &'static [&'static str] = &["<number>", "infinite"];
194}
195
196pub(crate) type AnimationIterationCounts = Box<[AnimationIterationCount]>;
198
199impl_comma_list_from_css!(AnimationIterationCounts, AnimationIterationCount);
200
201#[derive(Debug, Clone, Copy, PartialEq, Default)]
203#[non_exhaustive]
204pub enum AnimationDirection {
205 #[default]
206 Normal,
208 Reverse,
210 Alternate,
212 AlternateReverse,
214}
215
216impl_css_enum!(
217 AnimationDirection,
218 "normal" => AnimationDirection::Normal,
219 "reverse" => AnimationDirection::Reverse,
220 "alternate" => AnimationDirection::Alternate,
221 "alternate-reverse" => AnimationDirection::AlternateReverse,
222);
223
224pub(crate) type AnimationDirections = Box<[AnimationDirection]>;
226
227impl_comma_list_from_css!(AnimationDirections, AnimationDirection);
228
229#[derive(Debug, Clone, Copy, PartialEq, Default)]
231#[non_exhaustive]
232pub enum AnimationFillMode {
233 #[default]
234 None,
236 Forwards,
238 Backwards,
240 Both,
242}
243
244impl_css_enum!(
245 AnimationFillMode,
246 "none" => AnimationFillMode::None,
247 "forwards" => AnimationFillMode::Forwards,
248 "backwards" => AnimationFillMode::Backwards,
249 "both" => AnimationFillMode::Both,
250);
251
252pub(crate) type AnimationFillModes = Box<[AnimationFillMode]>;
254
255impl_comma_list_from_css!(AnimationFillModes, AnimationFillMode);
256
257#[derive(Debug, Clone, Copy, PartialEq, Default)]
259#[non_exhaustive]
260pub enum AnimationPlayState {
261 #[default]
262 Running,
264 Paused,
266}
267
268impl_css_enum!(
269 AnimationPlayState,
270 "running" => AnimationPlayState::Running,
271 "paused" => AnimationPlayState::Paused,
272);
273
274pub(crate) type AnimationPlayStates = Box<[AnimationPlayState]>;
276
277impl_comma_list_from_css!(AnimationPlayStates, AnimationPlayState);
278
279#[derive(Debug, Clone, PartialEq, Default, TypedBuilder)]
281#[builder(field_defaults(default))]
282#[non_exhaustive]
283pub struct Animation {
284 pub duration: AnimationTime,
286 pub delay: AnimationTime,
288 pub timing_function: AnimationTimingFunction,
290 pub iteration_count: AnimationIterationCount,
292 pub direction: AnimationDirection,
294 pub fill_mode: AnimationFillMode,
296 pub play_state: AnimationPlayState,
298 #[builder(setter(strip_option))]
300 pub name: Option<String>,
301}
302
303impl MakeComputed for Animation {}
304
305impl<'i> FromCss<'i> for Animation {
306 fn from_css(input: &mut Parser<'i>) -> ParseResult<'i, Self> {
307 let mut animation = Self::default();
308 let mut time_count = 0;
309
310 while !input.is_exhausted() && !next_is_comma(input) {
311 if let Ok(value) = input.try_parse(AnimationTime::from_css) {
312 match time_count {
313 0 => animation.duration = value,
314 1 => animation.delay = value,
315 _ => return Err(input.new_error_for_next_token()),
316 }
317 time_count += 1;
318 continue;
319 }
320
321 if let Ok(value) = input.try_parse(AnimationTimingFunction::from_css) {
322 animation.timing_function = value;
323 continue;
324 }
325
326 if let Ok(value) = input.try_parse(AnimationIterationCount::from_css) {
327 animation.iteration_count = value;
328 continue;
329 }
330
331 if let Ok(value) = input.try_parse(AnimationDirection::from_css) {
332 animation.direction = value;
333 continue;
334 }
335
336 if let Ok(value) = input.try_parse(AnimationFillMode::from_css) {
337 animation.fill_mode = value;
338 continue;
339 }
340
341 if let Ok(value) = input.try_parse(AnimationPlayState::from_css) {
342 animation.play_state = value;
343 continue;
344 }
345
346 if let Ok(value) = input.try_parse(AnimationName::from_css) {
347 animation.name = value;
348 continue;
349 }
350
351 return Err(input.new_error_for_next_token());
352 }
353
354 Ok(animation)
355 }
356
357 const VALID_TOKENS: &'static [&'static str] = Animations::VALID_TOKENS;
358}
359
360pub(crate) type Animations = Box<[Animation]>;
362
363impl_comma_list_from_css!(
364 Animations,
365 Animation,
366 &[
367 "<time>",
368 "<easing-function>",
369 "<number>",
370 "infinite",
371 "normal",
372 "reverse",
373 "alternate",
374 "alternate-reverse",
375 "none",
376 "forwards",
377 "backwards",
378 "both",
379 "running",
380 "paused",
381 "<custom-ident>",
382 "<string>",
383 ]
384);
385
386impl TailwindPropertyParser for Animations {
387 fn parse_tw(token: &str) -> Option<Self> {
388 match_ignore_ascii_case! {token,
389 "none" => Some(Box::from([Animation::default()])),
390 "spin" => Some(Box::from([Animation {
391 duration: AnimationTime::from_milliseconds(1000.0),
392 timing_function: AnimationTimingFunction::Linear,
393 iteration_count: AnimationIterationCount::Infinite,
394 name: Some("spin".to_string()),
395 ..Animation::default()
396 }])),
397 "ping" => Some(Box::from([Animation {
398 duration: AnimationTime::from_milliseconds(1000.0),
399 timing_function: AnimationTimingFunction::CubicBezier(0.0, 0.0, 0.2, 1.0),
400 iteration_count: AnimationIterationCount::Infinite,
401 name: Some("ping".to_string()),
402 ..Animation::default()
403 }])),
404 "pulse" => Some(Box::from([Animation {
405 duration: AnimationTime::from_milliseconds(2000.0),
406 timing_function: AnimationTimingFunction::CubicBezier(0.4, 0.0, 0.6, 1.0),
407 iteration_count: AnimationIterationCount::Infinite,
408 name: Some("pulse".to_string()),
409 ..Animation::default()
410 }])),
411 "bounce" => Some(Box::from([Animation {
412 duration: AnimationTime::from_milliseconds(1000.0),
413 iteration_count: AnimationIterationCount::Infinite,
414 name: Some("bounce".to_string()),
415 ..Animation::default()
416 }])),
417 _ => None,
418 }
419 }
420
421 fn parse_tw_with_arbitrary(token: &str) -> Option<Self> {
422 if let Some(value) = token
423 .strip_prefix('[')
424 .and_then(|value| value.strip_suffix(']'))
425 {
426 let value = if value.contains('_') {
427 Cow::Owned(value.replace('_', " "))
428 } else {
429 Cow::Borrowed(value)
430 };
431
432 return Self::from_css_str(&value).ok();
433 }
434
435 Self::parse_tw(token)
436 }
437}
438
439fn parse_timing_keyword<'i>(input: &mut Parser<'i>) -> ParseResult<'i, AnimationTimingFunction> {
440 let token = input.next()?;
441 let Token::Ident(ident) = token else {
442 return Err(unexpected_token!(AnimationTimingFunction, token));
443 };
444
445 match_ignore_ascii_case! {ident,
446 "linear" => Ok(AnimationTimingFunction::Linear),
447 "ease" => Ok(AnimationTimingFunction::Ease),
448 "ease-in" => Ok(AnimationTimingFunction::EaseIn),
449 "ease-out" => Ok(AnimationTimingFunction::EaseOut),
450 "ease-in-out" => Ok(AnimationTimingFunction::EaseInOut),
451 "step-start" => Ok(AnimationTimingFunction::StepStart),
452 "step-end" => Ok(AnimationTimingFunction::StepEnd),
453 _ => Err(unexpected_token!(AnimationTimingFunction, token)),
454 }
455}
456
457fn parse_step_position<'i>(input: &mut Parser<'i>) -> ParseResult<'i, StepPosition> {
458 let token = input.next()?;
459 let Token::Ident(ident) = token else {
460 return Err(unexpected_token!(AnimationTimingFunction, token));
461 };
462
463 match_ignore_ascii_case! {ident,
464 "start" | "jump-start" => Ok(StepPosition::Start),
465 "end" | "jump-end" => Ok(StepPosition::End),
466 "jump-both" => Ok(StepPosition::JumpBoth),
467 "jump-none" => Ok(StepPosition::JumpNone),
468 _ => Err(unexpected_token!(AnimationTimingFunction, token)),
469 }
470}
471
472fn parse_steps_function<'i>(input: &mut Parser<'i>) -> ParseResult<'i, AnimationTimingFunction> {
473 input.expect_function_matching("steps")?;
474 input.parse_nested_block(|input| {
475 let count = input.expect_integer()?;
476 if count <= 0 {
477 return Err(CssParseError::from_basic_kind(
478 BasicParseErrorKind::QualifiedRuleInvalid,
479 ));
480 }
481
482 let position = if input.try_parse(Parser::expect_comma).is_ok() {
483 parse_step_position(input)?
484 } else {
485 StepPosition::End
486 };
487
488 if position == StepPosition::JumpNone && count < 2 {
489 return Err(CssParseError::from_basic_kind(
490 BasicParseErrorKind::QualifiedRuleInvalid,
491 ));
492 }
493
494 Ok(AnimationTimingFunction::Steps(count as u32, position))
495 })
496}
497
498fn expect_number<'i>(input: &mut Parser<'i>) -> ParseResult<'i, f32> {
499 let token = input.next()?;
500 let Token::Number { value, .. } = token else {
501 return Err(unexpected_token!(AnimationTime, token));
502 };
503 Ok(*value)
504}
505
506pub(crate) fn repeated_list_value<T: Clone + Default>(values: &[T], index: usize) -> T {
508 if values.is_empty() {
509 return T::default();
510 }
511
512 values[index % values.len()].clone()
513}
514
515fn cubic_bezier_sample(x1: f32, y1: f32, x2: f32, y2: f32, progress: f32) -> f32 {
516 fn sample_curve(a: f32, b: f32, c: f32, t: f32) -> f32 {
517 ((a * t + b) * t + c) * t
518 }
519
520 fn sample_derivative(a: f32, b: f32, c: f32, t: f32) -> f32 {
521 (3.0 * a * t + 2.0 * b) * t + c
522 }
523
524 let cx = 3.0 * x1;
525 let bx = 3.0 * (x2 - x1) - cx;
526 let ax = 1.0 - cx - bx;
527 let cy = 3.0 * y1;
528 let by = 3.0 * (y2 - y1) - cy;
529 let ay = 1.0 - cy - by;
530
531 let mut t = progress.clamp(0.0, 1.0);
532 for _ in 0..6 {
533 let x = sample_curve(ax, bx, cx, t) - progress;
534 let derivative = sample_derivative(ax, bx, cx, t);
535 if derivative.abs() < f32::EPSILON {
536 break;
537 }
538 t = (t - x / derivative).clamp(0.0, 1.0);
539 }
540
541 sample_curve(ay, by, cy, t)
542}
543
544fn steps_sample(step_count: u32, position: StepPosition, progress: f32, before: bool) -> f32 {
545 let steps = step_count as f32;
546 let progress = progress.clamp(0.0, 1.0);
547
548 let (jumps, start_offset) = match position {
551 StepPosition::Start => (steps, 1.0),
552 StepPosition::End => (steps, 0.0),
553 StepPosition::JumpBoth => (steps + 1.0, 1.0),
554 StepPosition::JumpNone => ((steps - 1.0).max(1.0), 0.0),
555 };
556
557 let scaled = progress * steps;
558 let mut current_step = (scaled + start_offset).floor();
559 if before && scaled.fract() == 0.0 {
560 current_step -= 1.0;
561 }
562
563 current_step.clamp(0.0, jumps) / jumps
564}
565
566impl AnimationTimingFunction {
567 pub(crate) fn apply(self, progress: f32, before: bool) -> f32 {
572 match self {
573 Self::Linear => progress,
574 Self::Ease => cubic_bezier_sample(0.25, 0.1, 0.25, 1.0, progress),
575 Self::EaseIn => cubic_bezier_sample(0.42, 0.0, 1.0, 1.0, progress),
576 Self::EaseOut => cubic_bezier_sample(0.0, 0.0, 0.58, 1.0, progress),
577 Self::EaseInOut => cubic_bezier_sample(0.42, 0.0, 0.58, 1.0, progress),
578 Self::StepStart => steps_sample(1, StepPosition::Start, progress, before),
579 Self::StepEnd => steps_sample(1, StepPosition::End, progress, before),
580 Self::Steps(count, position) => steps_sample(count, position, progress, before),
581 Self::CubicBezier(x1, y1, x2, y2) => cubic_bezier_sample(x1, y1, x2, y2, progress),
582 }
583 }
584}
585
586impl ToCss for AnimationTime {
587 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
588 write!(dest, "{milliseconds}ms", milliseconds = self.milliseconds)
589 }
590}
591
592impl ToCss for StepPosition {
593 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
594 match self {
595 Self::Start => dest.write_str("start"),
596 Self::End => dest.write_str("end"),
597 Self::JumpBoth => dest.write_str("jump-both"),
598 Self::JumpNone => dest.write_str("jump-none"),
599 }
600 }
601}
602
603impl ToCss for AnimationTimingFunction {
604 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
605 match self {
606 Self::Linear => dest.write_str("linear"),
607 Self::Ease => dest.write_str("ease"),
608 Self::EaseIn => dest.write_str("ease-in"),
609 Self::EaseOut => dest.write_str("ease-out"),
610 Self::EaseInOut => dest.write_str("ease-in-out"),
611 Self::StepStart => dest.write_str("step-start"),
612 Self::StepEnd => dest.write_str("step-end"),
613 Self::Steps(count, position) => {
614 write!(dest, "steps({count}")?;
615 if *position != StepPosition::End {
616 dest.write_str(", ")?;
617 position.to_css(dest)?;
618 }
619 dest.write_char(')')
620 }
621 Self::CubicBezier(x1, y1, x2, y2) => write!(dest, "cubic-bezier({x1}, {y1}, {x2}, {y2})"),
622 }
623 }
624}
625
626impl ToCss for AnimationIterationCount {
627 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
628 match self {
629 Self::Number(n) => write!(dest, "{n}"),
630 Self::Infinite => dest.write_str("infinite"),
631 }
632 }
633}
634
635impl ToCss for Animation {
636 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
637 if let Some(ref name) = self.name {
638 let needs_quoting = name
639 .chars()
640 .any(|c| !matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-'));
641 if needs_quoting {
642 serialize_string(name, dest)?;
643 } else {
644 dest.write_str(name)?;
645 }
646 } else {
647 dest.write_str("none")?;
648 }
649 dest.write_char(' ')?;
650 self.duration.to_css(dest)?;
651 dest.write_char(' ')?;
652 self.timing_function.to_css(dest)?;
653 dest.write_char(' ')?;
654 self.delay.to_css(dest)?;
655 dest.write_char(' ')?;
656 self.iteration_count.to_css(dest)?;
657 dest.write_char(' ')?;
658 self.direction.to_css(dest)?;
659 dest.write_char(' ')?;
660 self.fill_mode.to_css(dest)?;
661 dest.write_char(' ')?;
662 self.play_state.to_css(dest)
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use std::assert_matches;
669
670 use super::*;
671
672 #[test]
673 fn parse_animation_time() {
674 assert_eq!(
675 AnimationTime::from_css_str("150ms"),
676 Ok(AnimationTime::from_milliseconds(150.0))
677 );
678 assert_eq!(
679 AnimationTime::from_css_str("2s"),
680 Ok(AnimationTime::from_milliseconds(2000.0))
681 );
682 }
683
684 #[test]
685 fn parse_animation_names() {
686 assert_matches!(
687 AnimationNames::from_css_str("fade, slide"),
688 Ok(names) if names.as_ref() == [Some("fade".to_string()), Some("slide".to_string())]
689 );
690 }
691
692 #[test]
693 fn parse_quoted_animation_names() {
694 assert_matches!(
695 AnimationNames::from_css_str("\"fade\", slide"),
696 Ok(names) if names.as_ref() == [Some("fade".to_string()), Some("slide".to_string())]
697 );
698 }
699
700 #[test]
701 fn parse_animation_names_with_none_entry() {
702 assert_matches!(
703 AnimationNames::from_css_str("none, slide"),
704 Ok(names) if names.as_ref() == [None, Some("slide".to_string())]
705 );
706 }
707
708 #[test]
709 fn parse_steps_timing_functions() {
710 assert_eq!(
711 AnimationTimingFunction::from_css_str("step-start"),
712 Ok(AnimationTimingFunction::StepStart)
713 );
714 assert_eq!(
715 AnimationTimingFunction::from_css_str("step-end"),
716 Ok(AnimationTimingFunction::StepEnd)
717 );
718 assert_eq!(
719 AnimationTimingFunction::from_css_str("steps(4, end)"),
720 Ok(AnimationTimingFunction::Steps(4, StepPosition::End))
721 );
722 assert_eq!(
723 AnimationTimingFunction::from_css_str("steps(4)"),
724 Ok(AnimationTimingFunction::Steps(4, StepPosition::End))
725 );
726 assert_eq!(
727 AnimationTimingFunction::from_css_str("steps(4, jump-start)"),
728 Ok(AnimationTimingFunction::Steps(4, StepPosition::Start))
729 );
730 assert_eq!(
731 AnimationTimingFunction::from_css_str("steps(4, jump-none)"),
732 Ok(AnimationTimingFunction::Steps(4, StepPosition::JumpNone))
733 );
734 assert_eq!(
735 AnimationTimingFunction::from_css_str("steps(4, jump-both)"),
736 Ok(AnimationTimingFunction::Steps(4, StepPosition::JumpBoth))
737 );
738 }
739
740 #[test]
741 fn reject_single_step_jump_none() {
742 assert!(AnimationTimingFunction::from_css_str("steps(1, jump-none)").is_err());
743 }
744
745 #[test]
746 fn steps_timing_functions_sample_as_staircase() {
747 let sample = |css: &str, progress: f32| {
748 AnimationTimingFunction::from_css_str(css)
749 .unwrap()
750 .apply(progress, false)
751 };
752
753 for (progress, expected) in [(0.0, 0.0), (0.3, 0.25), (0.6, 0.5), (1.0, 1.0)] {
754 assert_eq!(sample("steps(4, jump-end)", progress), expected);
755 }
756
757 for (progress, expected) in [(0.0, 0.0), (0.3, 1.0 / 3.0), (0.6, 2.0 / 3.0), (1.0, 1.0)] {
758 assert_eq!(sample("steps(4, jump-none)", progress), expected);
759 }
760
761 for (progress, expected) in [(0.0, 0.2), (0.3, 0.4), (0.6, 0.6), (1.0, 1.0)] {
762 assert_eq!(sample("steps(4, jump-both)", progress), expected);
763 }
764
765 for (progress, expected) in [(0.0, 0.25), (0.3, 0.5), (0.6, 0.75), (1.0, 1.0)] {
766 assert_eq!(sample("steps(4, jump-start)", progress), expected);
767 }
768 }
769
770 #[test]
771 fn steps_timing_functions_hold_the_lower_step_before_the_active_phase() {
772 let sample = |css: &str, progress: f32| {
773 AnimationTimingFunction::from_css_str(css)
774 .unwrap()
775 .apply(progress, true)
776 };
777
778 assert_eq!(sample("steps(4, jump-start)", 0.0), 0.0);
781 assert_eq!(sample("steps(4, jump-both)", 0.0), 0.0);
782 assert_eq!(sample("steps(4, jump-end)", 0.0), 0.0);
783 assert_eq!(sample("step-start", 0.0), 0.0);
784
785 assert_eq!(sample("steps(4, jump-end)", 1.0), 0.75);
787 assert_eq!(sample("steps(4, jump-start)", 1.0), 1.0);
788
789 assert_eq!(sample("steps(4, jump-end)", 0.3), 0.25);
791 }
792
793 #[test]
794 fn steps_timing_functions_serialize_without_a_default_position() {
795 let round_trip = |css: &str| {
796 AnimationTimingFunction::from_css_str(css)
797 .unwrap()
798 .to_css_string()
799 };
800
801 assert_eq!(round_trip("steps(4)"), "steps(4)");
802 assert_eq!(round_trip("steps(4, end)"), "steps(4)");
803 assert_eq!(round_trip("steps(4, jump-end)"), "steps(4)");
804 assert_eq!(round_trip("steps(4, jump-start)"), "steps(4, start)");
805 assert_eq!(round_trip("steps(4, jump-none)"), "steps(4, jump-none)");
806 assert_eq!(round_trip("steps(4, jump-both)"), "steps(4, jump-both)");
807 }
808
809 #[test]
810 fn parse_animation_shorthand_with_steps() {
811 assert_eq!(
812 Animations::from_css_str("fade 1s steps(4, jump-none)"),
813 Ok(Box::from([Animation {
814 duration: AnimationTime::from_milliseconds(1000.0),
815 timing_function: AnimationTimingFunction::Steps(4, StepPosition::JumpNone),
816 name: Some("fade".to_string()),
817 ..Animation::default()
818 }]))
819 );
820 }
821
822 #[test]
823 fn reject_invalid_cubic_bezier_x_coordinates() {
824 assert!(AnimationTimingFunction::from_css_str("cubic-bezier(-0.1, 0, 0.2, 1)").is_err());
825 assert!(AnimationTimingFunction::from_css_str("cubic-bezier(0.1, 0, 1.2, 1)").is_err());
826 }
827
828 #[test]
829 fn reject_negative_animation_iteration_count() {
830 assert!(AnimationIterationCount::from_css_str("-1").is_err());
831 }
832
833 #[test]
834 fn cubic_bezier_preserves_overshoot() {
835 let function =
836 AnimationTimingFunction::from_css_str("cubic-bezier(0.68, -0.6, 0.32, 1.6)").unwrap();
837
838 let early = function.apply(0.2, false);
839 let late = function.apply(0.8, false);
840
841 assert!(early < 0.0, "expected negative overshoot, got {early}");
842 assert!(late > 1.0, "expected positive overshoot, got {late}");
843 }
844
845 #[test]
846 fn repeated_list_value_wraps() {
847 let values: AnimationDirections =
848 [AnimationDirection::Normal, AnimationDirection::Reverse].into();
849 assert_eq!(repeated_list_value(&values, 2), AnimationDirection::Normal);
850 }
851
852 #[test]
853 fn parse_animation_shorthand() {
854 assert_eq!(
855 Animations::from_css_str("fade 1s ease-in 200ms 2 alternate both paused"),
856 Ok(Box::from([Animation {
857 duration: AnimationTime::from_milliseconds(1000.0),
858 delay: AnimationTime::from_milliseconds(200.0),
859 timing_function: AnimationTimingFunction::EaseIn,
860 iteration_count: AnimationIterationCount::Number(2.0),
861 direction: AnimationDirection::Alternate,
862 fill_mode: AnimationFillMode::Both,
863 play_state: AnimationPlayState::Paused,
864 name: Some("fade".to_string()),
865 }]))
866 );
867 }
868
869 #[test]
870 fn parse_animation_shorthand_with_quoted_name() {
871 assert_eq!(
872 Animations::from_css_str("\"fade\" 1s linear"),
873 Ok(Box::from([Animation {
874 duration: AnimationTime::from_milliseconds(1000.0),
875 timing_function: AnimationTimingFunction::Linear,
876 name: Some("fade".to_string()),
877 ..Animation::default()
878 }]))
879 );
880 }
881
882 #[test]
883 fn parse_multiple_animation_shorthand_values() {
884 assert_eq!(
885 Animations::from_css_str("fade 1s linear, 2s slide"),
886 Ok(Box::from([
887 Animation {
888 duration: AnimationTime::from_milliseconds(1000.0),
889 timing_function: AnimationTimingFunction::Linear,
890 name: Some("fade".to_string()),
891 ..Animation::default()
892 },
893 Animation {
894 duration: AnimationTime::from_milliseconds(2000.0),
895 name: Some("slide".to_string()),
896 ..Animation::default()
897 },
898 ]))
899 );
900 }
901}