tailwind_rs_core/utilities/
animations.rs

1//! Animation utilities for tailwind-rs
2//!
3//! This module provides utilities for CSS animations including animate-none,
4//! animate-spin, animate-ping, animate-pulse, and animate-bounce.
5
6use crate::classes::ClassBuilder;
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10/// Animation values
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum Animation {
13    /// No animation
14    None,
15    /// Spin animation
16    Spin,
17    /// Ping animation
18    Ping,
19    /// Pulse animation
20    Pulse,
21    /// Bounce animation
22    Bounce,
23    /// Fade in animation
24    FadeIn,
25    /// Fade out animation
26    FadeOut,
27    /// Slide in from left
28    SlideInLeft,
29    /// Slide in from right
30    SlideInRight,
31    /// Slide in from top
32    SlideInTop,
33    /// Slide in from bottom
34    SlideInBottom,
35    /// Zoom in animation
36    ZoomIn,
37    /// Zoom out animation
38    ZoomOut,
39    /// Wobble animation
40    Wobble,
41    /// Shake animation
42    Shake,
43    /// Flip animation
44    Flip,
45    /// Heartbeat animation
46    Heartbeat,
47}
48
49impl Animation {
50    pub fn to_class_name(&self) -> String {
51        match self {
52            Animation::None => "none".to_string(),
53            Animation::Spin => "spin".to_string(),
54            Animation::Ping => "ping".to_string(),
55            Animation::Pulse => "pulse".to_string(),
56            Animation::Bounce => "bounce".to_string(),
57            Animation::FadeIn => "fade-in".to_string(),
58            Animation::FadeOut => "fade-out".to_string(),
59            Animation::SlideInLeft => "slide-in-left".to_string(),
60            Animation::SlideInRight => "slide-in-right".to_string(),
61            Animation::SlideInTop => "slide-in-top".to_string(),
62            Animation::SlideInBottom => "slide-in-bottom".to_string(),
63            Animation::ZoomIn => "zoom-in".to_string(),
64            Animation::ZoomOut => "zoom-out".to_string(),
65            Animation::Wobble => "wobble".to_string(),
66            Animation::Shake => "shake".to_string(),
67            Animation::Flip => "flip".to_string(),
68            Animation::Heartbeat => "heartbeat".to_string(),
69        }
70    }
71
72    pub fn to_css_value(&self) -> String {
73        match self {
74            Animation::None => "none".to_string(),
75            Animation::Spin => "spin 1s linear infinite".to_string(),
76            Animation::Ping => "ping 1s cubic-bezier(0, 0, 0.2, 1) infinite".to_string(),
77            Animation::Pulse => "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite".to_string(),
78            Animation::Bounce => "bounce 1s infinite".to_string(),
79            Animation::FadeIn => "fadeIn 0.5s ease-in".to_string(),
80            Animation::FadeOut => "fadeOut 0.5s ease-out".to_string(),
81            Animation::SlideInLeft => "slideInLeft 0.5s ease-out".to_string(),
82            Animation::SlideInRight => "slideInRight 0.5s ease-out".to_string(),
83            Animation::SlideInTop => "slideInTop 0.5s ease-out".to_string(),
84            Animation::SlideInBottom => "slideInBottom 0.5s ease-out".to_string(),
85            Animation::ZoomIn => "zoomIn 0.5s ease-out".to_string(),
86            Animation::ZoomOut => "zoomOut 0.5s ease-in".to_string(),
87            Animation::Wobble => "wobble 1s ease-in-out".to_string(),
88            Animation::Shake => "shake 0.5s ease-in-out".to_string(),
89            Animation::Flip => "flip 1s ease-in-out".to_string(),
90            Animation::Heartbeat => "heartbeat 1.5s ease-in-out infinite".to_string(),
91        }
92    }
93
94    /// Get all available animation values
95    pub fn all_values() -> Vec<Animation> {
96        vec![
97            Animation::None,
98            Animation::Spin,
99            Animation::Ping,
100            Animation::Pulse,
101            Animation::Bounce,
102            Animation::FadeIn,
103            Animation::FadeOut,
104            Animation::SlideInLeft,
105            Animation::SlideInRight,
106            Animation::SlideInTop,
107            Animation::SlideInBottom,
108            Animation::ZoomIn,
109            Animation::ZoomOut,
110            Animation::Wobble,
111            Animation::Shake,
112            Animation::Flip,
113            Animation::Heartbeat,
114        ]
115    }
116
117    /// Check if animation is an infinite animation
118    pub fn is_infinite(&self) -> bool {
119        matches!(
120            self,
121            Animation::Spin
122                | Animation::Ping
123                | Animation::Pulse
124                | Animation::Bounce
125                | Animation::Heartbeat
126        )
127    }
128
129    /// Get animation duration in milliseconds
130    pub fn duration_ms(&self) -> u32 {
131        match self {
132            Animation::None => 0,
133            Animation::Spin => 1000,
134            Animation::Ping => 1000,
135            Animation::Pulse => 2000,
136            Animation::Bounce => 1000,
137            Animation::FadeIn | Animation::FadeOut => 500,
138            Animation::SlideInLeft
139            | Animation::SlideInRight
140            | Animation::SlideInTop
141            | Animation::SlideInBottom => 500,
142            Animation::ZoomIn | Animation::ZoomOut => 500,
143            Animation::Shake => 500,
144            Animation::Wobble | Animation::Flip => 1000,
145            Animation::Heartbeat => 1500,
146        }
147    }
148}
149
150impl fmt::Display for Animation {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        write!(f, "{}", self.to_class_name())
153    }
154}
155
156/// Trait for adding animation utilities to a class builder
157pub trait AnimationUtilities {
158    fn animation(self, animation: Animation) -> Self;
159
160    /// Add animation with custom duration
161    fn animation_with_duration(self, animation: Animation, duration_ms: u32) -> Self;
162
163    /// Add animation that runs only once
164    fn animation_once(self, animation: Animation) -> Self;
165
166    /// Add animation that repeats a specific number of times
167    fn animation_repeat(self, animation: Animation, count: u32) -> Self;
168
169    /// Add fade in animation
170    fn fade_in(self) -> Self;
171
172    /// Add fade out animation
173    fn fade_out(self) -> Self;
174
175    /// Add slide in from left animation
176    fn slide_in_left(self) -> Self;
177
178    /// Add slide in from right animation
179    fn slide_in_right(self) -> Self;
180
181    /// Add slide in from top animation
182    fn slide_in_top(self) -> Self;
183
184    /// Add slide in from bottom animation
185    fn slide_in_bottom(self) -> Self;
186
187    /// Add zoom in animation
188    fn zoom_in(self) -> Self;
189
190    /// Add zoom out animation
191    fn zoom_out(self) -> Self;
192
193    /// Add wobble animation
194    fn wobble(self) -> Self;
195
196    /// Add shake animation
197    fn shake(self) -> Self;
198
199    /// Add flip animation
200    fn flip(self) -> Self;
201
202    /// Add heartbeat animation
203    fn heartbeat(self) -> Self;
204
205    /// Add hover animation (animation only on hover)
206    fn hover_animation(self, animation: Animation) -> Self;
207
208    /// Add focus animation (animation only on focus)
209    fn focus_animation(self, animation: Animation) -> Self;
210
211    /// Pause animation
212    fn animation_pause(self) -> Self;
213
214    /// Resume animation
215    fn animation_resume(self) -> Self;
216}
217
218impl AnimationUtilities for ClassBuilder {
219    fn animation(self, animation: Animation) -> Self {
220        self.class(format!("animate-{}", animation.to_class_name()))
221    }
222
223    fn animation_with_duration(self, animation: Animation, duration_ms: u32) -> Self {
224        self.class(format!("animate-{}", animation.to_class_name()))
225            .class(format!("duration-{}", duration_ms))
226    }
227
228    fn animation_once(self, animation: Animation) -> Self {
229        self.class(format!("animate-{}", animation.to_class_name()))
230            .class("animation-iteration-count-1".to_string())
231    }
232
233    fn animation_repeat(self, animation: Animation, count: u32) -> Self {
234        self.class(format!("animate-{}", animation.to_class_name()))
235            .class(format!("animation-iteration-count-{}", count))
236    }
237
238    fn fade_in(self) -> Self {
239        self.animation(Animation::FadeIn)
240    }
241
242    fn fade_out(self) -> Self {
243        self.animation(Animation::FadeOut)
244    }
245
246    fn slide_in_left(self) -> Self {
247        self.animation(Animation::SlideInLeft)
248    }
249
250    fn slide_in_right(self) -> Self {
251        self.animation(Animation::SlideInRight)
252    }
253
254    fn slide_in_top(self) -> Self {
255        self.animation(Animation::SlideInTop)
256    }
257
258    fn slide_in_bottom(self) -> Self {
259        self.animation(Animation::SlideInBottom)
260    }
261
262    fn zoom_in(self) -> Self {
263        self.animation(Animation::ZoomIn)
264    }
265
266    fn zoom_out(self) -> Self {
267        self.animation(Animation::ZoomOut)
268    }
269
270    fn wobble(self) -> Self {
271        self.animation(Animation::Wobble)
272    }
273
274    fn shake(self) -> Self {
275        self.animation(Animation::Shake)
276    }
277
278    fn flip(self) -> Self {
279        self.animation(Animation::Flip)
280    }
281
282    fn heartbeat(self) -> Self {
283        self.animation(Animation::Heartbeat)
284    }
285
286    fn hover_animation(self, animation: Animation) -> Self {
287        self.class(format!("hover:animate-{}", animation.to_class_name()))
288    }
289
290    fn focus_animation(self, animation: Animation) -> Self {
291        self.class(format!("focus:animate-{}", animation.to_class_name()))
292    }
293
294    fn animation_pause(self) -> Self {
295        self.class("animation-play-state-paused".to_string())
296    }
297
298    fn animation_resume(self) -> Self {
299        self.class("animation-play-state-running".to_string())
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn test_animation_utilities() {
309        let classes = ClassBuilder::new()
310            .animation(Animation::None)
311            .animation(Animation::Spin)
312            .animation(Animation::Ping)
313            .animation(Animation::Pulse)
314            .animation(Animation::Bounce)
315            .build();
316
317        let css_classes = classes.to_css_classes();
318        assert!(css_classes.contains("animate-none"));
319        assert!(css_classes.contains("animate-spin"));
320        assert!(css_classes.contains("animate-ping"));
321        assert!(css_classes.contains("animate-pulse"));
322        assert!(css_classes.contains("animate-bounce"));
323    }
324
325    /// Test that all Week 12 animation utilities are implemented
326    #[test]
327    fn test_week12_animation_utilities() {
328        // Test all Week 12 animation utilities
329        let classes = ClassBuilder::new()
330            .animation(Animation::None)
331            .animation(Animation::Spin)
332            .animation(Animation::Ping)
333            .animation(Animation::Pulse)
334            .animation(Animation::Bounce)
335            .build();
336
337        let css_classes = classes.to_css_classes();
338
339        // Animations
340        assert!(css_classes.contains("animate-none"));
341        assert!(css_classes.contains("animate-spin"));
342        assert!(css_classes.contains("animate-ping"));
343        assert!(css_classes.contains("animate-pulse"));
344        assert!(css_classes.contains("animate-bounce"));
345    }
346
347    /// Test extended animation utilities
348    #[test]
349    fn test_extended_animation_utilities() {
350        let classes = ClassBuilder::new()
351            .animation(Animation::FadeIn)
352            .animation(Animation::FadeOut)
353            .animation(Animation::SlideInLeft)
354            .animation(Animation::SlideInRight)
355            .animation(Animation::SlideInTop)
356            .animation(Animation::SlideInBottom)
357            .animation(Animation::ZoomIn)
358            .animation(Animation::ZoomOut)
359            .animation(Animation::Wobble)
360            .animation(Animation::Shake)
361            .animation(Animation::Flip)
362            .animation(Animation::Heartbeat)
363            .build();
364
365        let css_classes = classes.to_css_classes();
366        assert!(css_classes.contains("animate-fade-in"));
367        assert!(css_classes.contains("animate-fade-out"));
368        assert!(css_classes.contains("animate-slide-in-left"));
369        assert!(css_classes.contains("animate-slide-in-right"));
370        assert!(css_classes.contains("animate-slide-in-top"));
371        assert!(css_classes.contains("animate-slide-in-bottom"));
372        assert!(css_classes.contains("animate-zoom-in"));
373        assert!(css_classes.contains("animate-zoom-out"));
374        assert!(css_classes.contains("animate-wobble"));
375        assert!(css_classes.contains("animate-shake"));
376        assert!(css_classes.contains("animate-flip"));
377        assert!(css_classes.contains("animate-heartbeat"));
378    }
379
380    /// Test convenience animation methods
381    #[test]
382    fn test_convenience_animation_methods() {
383        let classes = ClassBuilder::new()
384            .fade_in()
385            .fade_out()
386            .slide_in_left()
387            .slide_in_right()
388            .slide_in_top()
389            .slide_in_bottom()
390            .zoom_in()
391            .zoom_out()
392            .wobble()
393            .shake()
394            .flip()
395            .heartbeat()
396            .build();
397
398        let css_classes = classes.to_css_classes();
399        assert!(css_classes.contains("animate-fade-in"));
400        assert!(css_classes.contains("animate-fade-out"));
401        assert!(css_classes.contains("animate-slide-in-left"));
402        assert!(css_classes.contains("animate-slide-in-right"));
403        assert!(css_classes.contains("animate-slide-in-top"));
404        assert!(css_classes.contains("animate-slide-in-bottom"));
405        assert!(css_classes.contains("animate-zoom-in"));
406        assert!(css_classes.contains("animate-zoom-out"));
407        assert!(css_classes.contains("animate-wobble"));
408        assert!(css_classes.contains("animate-shake"));
409        assert!(css_classes.contains("animate-flip"));
410        assert!(css_classes.contains("animate-heartbeat"));
411    }
412
413    /// Test animation with duration
414    #[test]
415    fn test_animation_with_duration() {
416        let classes = ClassBuilder::new()
417            .animation_with_duration(Animation::FadeIn, 1000)
418            .build();
419
420        let css_classes = classes.to_css_classes();
421        assert!(css_classes.contains("animate-fade-in"));
422        assert!(css_classes.contains("duration-1000"));
423    }
424
425    /// Test animation repetition controls
426    #[test]
427    fn test_animation_repetition_controls() {
428        let classes = ClassBuilder::new()
429            .animation_once(Animation::Bounce)
430            .animation_repeat(Animation::Shake, 3)
431            .build();
432
433        let css_classes = classes.to_css_classes();
434        assert!(css_classes.contains("animate-bounce"));
435        assert!(css_classes.contains("animation-iteration-count-1"));
436        assert!(css_classes.contains("animate-shake"));
437        assert!(css_classes.contains("animation-iteration-count-3"));
438    }
439
440    /// Test hover and focus animations
441    #[test]
442    fn test_hover_focus_animations() {
443        let classes = ClassBuilder::new()
444            .hover_animation(Animation::Bounce)
445            .focus_animation(Animation::Pulse)
446            .build();
447
448        let css_classes = classes.to_css_classes();
449        assert!(css_classes.contains("hover:animate-bounce"));
450        assert!(css_classes.contains("focus:animate-pulse"));
451    }
452
453    /// Test animation play state controls
454    #[test]
455    fn test_animation_play_state() {
456        let classes = ClassBuilder::new()
457            .animation_pause()
458            .animation_resume()
459            .build();
460
461        let css_classes = classes.to_css_classes();
462        assert!(css_classes.contains("animation-play-state-paused"));
463        assert!(css_classes.contains("animation-play-state-running"));
464    }
465
466    /// Test animation properties
467    #[test]
468    fn test_animation_properties() {
469        // Test all animations are available
470        let all_animations = Animation::all_values();
471        assert_eq!(all_animations.len(), 17);
472        assert!(all_animations.contains(&Animation::None));
473        assert!(all_animations.contains(&Animation::FadeIn));
474        assert!(all_animations.contains(&Animation::Heartbeat));
475
476        // Test infinite animations
477        assert!(Animation::Spin.is_infinite());
478        assert!(Animation::Heartbeat.is_infinite());
479        assert!(!Animation::FadeIn.is_infinite());
480        assert!(!Animation::ZoomOut.is_infinite());
481
482        // Test durations
483        assert_eq!(Animation::None.duration_ms(), 0);
484        assert_eq!(Animation::FadeIn.duration_ms(), 500);
485        assert_eq!(Animation::Spin.duration_ms(), 1000);
486        assert_eq!(Animation::Heartbeat.duration_ms(), 1500);
487        assert_eq!(Animation::Pulse.duration_ms(), 2000);
488    }
489
490    /// Test animation CSS values
491    #[test]
492    fn test_animation_css_values() {
493        assert_eq!(Animation::None.to_css_value(), "none");
494        assert_eq!(Animation::FadeIn.to_css_value(), "fadeIn 0.5s ease-in");
495        assert_eq!(
496            Animation::SlideInLeft.to_css_value(),
497            "slideInLeft 0.5s ease-out"
498        );
499        assert_eq!(Animation::Wobble.to_css_value(), "wobble 1s ease-in-out");
500        assert_eq!(
501            Animation::Heartbeat.to_css_value(),
502            "heartbeat 1.5s ease-in-out infinite"
503        );
504    }
505
506    /// Test animation class names
507    #[test]
508    fn test_animation_class_names() {
509        assert_eq!(Animation::FadeIn.to_class_name(), "fade-in");
510        assert_eq!(Animation::SlideInLeft.to_class_name(), "slide-in-left");
511        assert_eq!(Animation::ZoomOut.to_class_name(), "zoom-out");
512        assert_eq!(Animation::Heartbeat.to_class_name(), "heartbeat");
513    }
514
515    /// Test comprehensive animation system
516    #[test]
517    fn test_comprehensive_animation_system() {
518        let classes = ClassBuilder::new()
519            // Basic animations
520            .animation(Animation::Spin)
521            .animation(Animation::Bounce)
522            // Advanced animations
523            .fade_in()
524            .slide_in_right()
525            .zoom_in()
526            // State-based animations
527            .hover_animation(Animation::Shake)
528            .focus_animation(Animation::Wobble)
529            // Animation controls
530            .animation_once(Animation::Flip)
531            .animation_with_duration(Animation::Heartbeat, 2000)
532            .animation_pause()
533            .build();
534
535        let css_classes = classes.to_css_classes();
536
537        // Basic animations
538        assert!(css_classes.contains("animate-spin"));
539        assert!(css_classes.contains("animate-bounce"));
540
541        // Advanced animations
542        assert!(css_classes.contains("animate-fade-in"));
543        assert!(css_classes.contains("animate-slide-in-right"));
544        assert!(css_classes.contains("animate-zoom-in"));
545
546        // State-based animations
547        assert!(css_classes.contains("hover:animate-shake"));
548        assert!(css_classes.contains("focus:animate-wobble"));
549
550        // Animation controls
551        assert!(css_classes.contains("animate-flip"));
552        assert!(css_classes.contains("animation-iteration-count-1"));
553        assert!(css_classes.contains("animate-heartbeat"));
554        assert!(css_classes.contains("duration-2000"));
555        assert!(css_classes.contains("animation-play-state-paused"));
556    }
557}