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