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!(self, Animation::Spin | Animation::Ping | Animation::Pulse | Animation::Bounce | Animation::Heartbeat)
120    }
121
122    /// Get animation duration in milliseconds
123    pub fn duration_ms(&self) -> u32 {
124        match self {
125            Animation::None => 0,
126            Animation::Spin => 1000,
127            Animation::Ping => 1000,
128            Animation::Pulse => 2000,
129            Animation::Bounce => 1000,
130            Animation::FadeIn | Animation::FadeOut => 500,
131            Animation::SlideInLeft | Animation::SlideInRight |
132            Animation::SlideInTop | Animation::SlideInBottom => 500,
133            Animation::ZoomIn | Animation::ZoomOut => 500,
134            Animation::Shake => 500,
135            Animation::Wobble | Animation::Flip => 1000,
136            Animation::Heartbeat => 1500,
137        }
138    }
139}
140
141impl fmt::Display for Animation {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        write!(f, "{}", self.to_class_name())
144    }
145}
146
147/// Trait for adding animation utilities to a class builder
148pub trait AnimationUtilities {
149    fn animation(self, animation: Animation) -> Self;
150
151    /// Add animation with custom duration
152    fn animation_with_duration(self, animation: Animation, duration_ms: u32) -> Self;
153
154    /// Add animation that runs only once
155    fn animation_once(self, animation: Animation) -> Self;
156
157    /// Add animation that repeats a specific number of times
158    fn animation_repeat(self, animation: Animation, count: u32) -> Self;
159
160    /// Add fade in animation
161    fn fade_in(self) -> Self;
162
163    /// Add fade out animation
164    fn fade_out(self) -> Self;
165
166    /// Add slide in from left animation
167    fn slide_in_left(self) -> Self;
168
169    /// Add slide in from right animation
170    fn slide_in_right(self) -> Self;
171
172    /// Add slide in from top animation
173    fn slide_in_top(self) -> Self;
174
175    /// Add slide in from bottom animation
176    fn slide_in_bottom(self) -> Self;
177
178    /// Add zoom in animation
179    fn zoom_in(self) -> Self;
180
181    /// Add zoom out animation
182    fn zoom_out(self) -> Self;
183
184    /// Add wobble animation
185    fn wobble(self) -> Self;
186
187    /// Add shake animation
188    fn shake(self) -> Self;
189
190    /// Add flip animation
191    fn flip(self) -> Self;
192
193    /// Add heartbeat animation
194    fn heartbeat(self) -> Self;
195
196    /// Add hover animation (animation only on hover)
197    fn hover_animation(self, animation: Animation) -> Self;
198
199    /// Add focus animation (animation only on focus)
200    fn focus_animation(self, animation: Animation) -> Self;
201
202    /// Pause animation
203    fn animation_pause(self) -> Self;
204
205    /// Resume animation
206    fn animation_resume(self) -> Self;
207}
208
209impl AnimationUtilities for ClassBuilder {
210    fn animation(self, animation: Animation) -> Self {
211        self.class(format!("animate-{}", animation.to_class_name()))
212    }
213
214    fn animation_with_duration(self, animation: Animation, duration_ms: u32) -> Self {
215        self.class(format!("animate-{}", animation.to_class_name()))
216            .class(format!("duration-{}", duration_ms))
217    }
218
219    fn animation_once(self, animation: Animation) -> Self {
220        self.class(format!("animate-{}", animation.to_class_name()))
221            .class("animation-iteration-count-1".to_string())
222    }
223
224    fn animation_repeat(self, animation: Animation, count: u32) -> Self {
225        self.class(format!("animate-{}", animation.to_class_name()))
226            .class(format!("animation-iteration-count-{}", count))
227    }
228
229    fn fade_in(self) -> Self {
230        self.animation(Animation::FadeIn)
231    }
232
233    fn fade_out(self) -> Self {
234        self.animation(Animation::FadeOut)
235    }
236
237    fn slide_in_left(self) -> Self {
238        self.animation(Animation::SlideInLeft)
239    }
240
241    fn slide_in_right(self) -> Self {
242        self.animation(Animation::SlideInRight)
243    }
244
245    fn slide_in_top(self) -> Self {
246        self.animation(Animation::SlideInTop)
247    }
248
249    fn slide_in_bottom(self) -> Self {
250        self.animation(Animation::SlideInBottom)
251    }
252
253    fn zoom_in(self) -> Self {
254        self.animation(Animation::ZoomIn)
255    }
256
257    fn zoom_out(self) -> Self {
258        self.animation(Animation::ZoomOut)
259    }
260
261    fn wobble(self) -> Self {
262        self.animation(Animation::Wobble)
263    }
264
265    fn shake(self) -> Self {
266        self.animation(Animation::Shake)
267    }
268
269    fn flip(self) -> Self {
270        self.animation(Animation::Flip)
271    }
272
273    fn heartbeat(self) -> Self {
274        self.animation(Animation::Heartbeat)
275    }
276
277    fn hover_animation(self, animation: Animation) -> Self {
278        self.class(format!("hover:animate-{}", animation.to_class_name()))
279    }
280
281    fn focus_animation(self, animation: Animation) -> Self {
282        self.class(format!("focus:animate-{}", animation.to_class_name()))
283    }
284
285    fn animation_pause(self) -> Self {
286        self.class("animation-play-state-paused".to_string())
287    }
288
289    fn animation_resume(self) -> Self {
290        self.class("animation-play-state-running".to_string())
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    
298    #[test]
299    fn test_animation_utilities() {
300        let classes = ClassBuilder::new()
301            .animation(Animation::None)
302            .animation(Animation::Spin)
303            .animation(Animation::Ping)
304            .animation(Animation::Pulse)
305            .animation(Animation::Bounce)
306            .build();
307        
308        let css_classes = classes.to_css_classes();
309        assert!(css_classes.contains("animate-none"));
310        assert!(css_classes.contains("animate-spin"));
311        assert!(css_classes.contains("animate-ping"));
312        assert!(css_classes.contains("animate-pulse"));
313        assert!(css_classes.contains("animate-bounce"));
314    }
315    
316    /// Test that all Week 12 animation utilities are implemented
317    #[test]
318    fn test_week12_animation_utilities() {
319        // Test all Week 12 animation utilities
320        let classes = ClassBuilder::new()
321            .animation(Animation::None)
322            .animation(Animation::Spin)
323            .animation(Animation::Ping)
324            .animation(Animation::Pulse)
325            .animation(Animation::Bounce)
326            .build();
327
328        let css_classes = classes.to_css_classes();
329
330        // Animations
331        assert!(css_classes.contains("animate-none"));
332        assert!(css_classes.contains("animate-spin"));
333        assert!(css_classes.contains("animate-ping"));
334        assert!(css_classes.contains("animate-pulse"));
335        assert!(css_classes.contains("animate-bounce"));
336    }
337
338    /// Test extended animation utilities
339    #[test]
340    fn test_extended_animation_utilities() {
341        let classes = ClassBuilder::new()
342            .animation(Animation::FadeIn)
343            .animation(Animation::FadeOut)
344            .animation(Animation::SlideInLeft)
345            .animation(Animation::SlideInRight)
346            .animation(Animation::SlideInTop)
347            .animation(Animation::SlideInBottom)
348            .animation(Animation::ZoomIn)
349            .animation(Animation::ZoomOut)
350            .animation(Animation::Wobble)
351            .animation(Animation::Shake)
352            .animation(Animation::Flip)
353            .animation(Animation::Heartbeat)
354            .build();
355
356        let css_classes = classes.to_css_classes();
357        assert!(css_classes.contains("animate-fade-in"));
358        assert!(css_classes.contains("animate-fade-out"));
359        assert!(css_classes.contains("animate-slide-in-left"));
360        assert!(css_classes.contains("animate-slide-in-right"));
361        assert!(css_classes.contains("animate-slide-in-top"));
362        assert!(css_classes.contains("animate-slide-in-bottom"));
363        assert!(css_classes.contains("animate-zoom-in"));
364        assert!(css_classes.contains("animate-zoom-out"));
365        assert!(css_classes.contains("animate-wobble"));
366        assert!(css_classes.contains("animate-shake"));
367        assert!(css_classes.contains("animate-flip"));
368        assert!(css_classes.contains("animate-heartbeat"));
369    }
370
371    /// Test convenience animation methods
372    #[test]
373    fn test_convenience_animation_methods() {
374        let classes = ClassBuilder::new()
375            .fade_in()
376            .fade_out()
377            .slide_in_left()
378            .slide_in_right()
379            .slide_in_top()
380            .slide_in_bottom()
381            .zoom_in()
382            .zoom_out()
383            .wobble()
384            .shake()
385            .flip()
386            .heartbeat()
387            .build();
388
389        let css_classes = classes.to_css_classes();
390        assert!(css_classes.contains("animate-fade-in"));
391        assert!(css_classes.contains("animate-fade-out"));
392        assert!(css_classes.contains("animate-slide-in-left"));
393        assert!(css_classes.contains("animate-slide-in-right"));
394        assert!(css_classes.contains("animate-slide-in-top"));
395        assert!(css_classes.contains("animate-slide-in-bottom"));
396        assert!(css_classes.contains("animate-zoom-in"));
397        assert!(css_classes.contains("animate-zoom-out"));
398        assert!(css_classes.contains("animate-wobble"));
399        assert!(css_classes.contains("animate-shake"));
400        assert!(css_classes.contains("animate-flip"));
401        assert!(css_classes.contains("animate-heartbeat"));
402    }
403
404    /// Test animation with duration
405    #[test]
406    fn test_animation_with_duration() {
407        let classes = ClassBuilder::new()
408            .animation_with_duration(Animation::FadeIn, 1000)
409            .build();
410
411        let css_classes = classes.to_css_classes();
412        assert!(css_classes.contains("animate-fade-in"));
413        assert!(css_classes.contains("duration-1000"));
414    }
415
416    /// Test animation repetition controls
417    #[test]
418    fn test_animation_repetition_controls() {
419        let classes = ClassBuilder::new()
420            .animation_once(Animation::Bounce)
421            .animation_repeat(Animation::Shake, 3)
422            .build();
423
424        let css_classes = classes.to_css_classes();
425        assert!(css_classes.contains("animate-bounce"));
426        assert!(css_classes.contains("animation-iteration-count-1"));
427        assert!(css_classes.contains("animate-shake"));
428        assert!(css_classes.contains("animation-iteration-count-3"));
429    }
430
431    /// Test hover and focus animations
432    #[test]
433    fn test_hover_focus_animations() {
434        let classes = ClassBuilder::new()
435            .hover_animation(Animation::Bounce)
436            .focus_animation(Animation::Pulse)
437            .build();
438
439        let css_classes = classes.to_css_classes();
440        assert!(css_classes.contains("hover:animate-bounce"));
441        assert!(css_classes.contains("focus:animate-pulse"));
442    }
443
444    /// Test animation play state controls
445    #[test]
446    fn test_animation_play_state() {
447        let classes = ClassBuilder::new()
448            .animation_pause()
449            .animation_resume()
450            .build();
451
452        let css_classes = classes.to_css_classes();
453        assert!(css_classes.contains("animation-play-state-paused"));
454        assert!(css_classes.contains("animation-play-state-running"));
455    }
456
457    /// Test animation properties
458    #[test]
459    fn test_animation_properties() {
460        // Test all animations are available
461        let all_animations = Animation::all_values();
462        assert_eq!(all_animations.len(), 17);
463        assert!(all_animations.contains(&Animation::None));
464        assert!(all_animations.contains(&Animation::FadeIn));
465        assert!(all_animations.contains(&Animation::Heartbeat));
466
467        // Test infinite animations
468        assert!(Animation::Spin.is_infinite());
469        assert!(Animation::Heartbeat.is_infinite());
470        assert!(!Animation::FadeIn.is_infinite());
471        assert!(!Animation::ZoomOut.is_infinite());
472
473        // Test durations
474        assert_eq!(Animation::None.duration_ms(), 0);
475        assert_eq!(Animation::FadeIn.duration_ms(), 500);
476        assert_eq!(Animation::Spin.duration_ms(), 1000);
477        assert_eq!(Animation::Heartbeat.duration_ms(), 1500);
478        assert_eq!(Animation::Pulse.duration_ms(), 2000);
479    }
480
481    /// Test animation CSS values
482    #[test]
483    fn test_animation_css_values() {
484        assert_eq!(Animation::None.to_css_value(), "none");
485        assert_eq!(Animation::FadeIn.to_css_value(), "fadeIn 0.5s ease-in");
486        assert_eq!(Animation::SlideInLeft.to_css_value(), "slideInLeft 0.5s ease-out");
487        assert_eq!(Animation::Wobble.to_css_value(), "wobble 1s ease-in-out");
488        assert_eq!(Animation::Heartbeat.to_css_value(), "heartbeat 1.5s ease-in-out infinite");
489    }
490
491    /// Test animation class names
492    #[test]
493    fn test_animation_class_names() {
494        assert_eq!(Animation::FadeIn.to_class_name(), "fade-in");
495        assert_eq!(Animation::SlideInLeft.to_class_name(), "slide-in-left");
496        assert_eq!(Animation::ZoomOut.to_class_name(), "zoom-out");
497        assert_eq!(Animation::Heartbeat.to_class_name(), "heartbeat");
498    }
499
500    /// Test comprehensive animation system
501    #[test]
502    fn test_comprehensive_animation_system() {
503        let classes = ClassBuilder::new()
504            // Basic animations
505            .animation(Animation::Spin)
506            .animation(Animation::Bounce)
507            // Advanced animations
508            .fade_in()
509            .slide_in_right()
510            .zoom_in()
511            // State-based animations
512            .hover_animation(Animation::Shake)
513            .focus_animation(Animation::Wobble)
514            // Animation controls
515            .animation_once(Animation::Flip)
516            .animation_with_duration(Animation::Heartbeat, 2000)
517            .animation_pause()
518            .build();
519
520        let css_classes = classes.to_css_classes();
521
522        // Basic animations
523        assert!(css_classes.contains("animate-spin"));
524        assert!(css_classes.contains("animate-bounce"));
525
526        // Advanced animations
527        assert!(css_classes.contains("animate-fade-in"));
528        assert!(css_classes.contains("animate-slide-in-right"));
529        assert!(css_classes.contains("animate-zoom-in"));
530
531        // State-based animations
532        assert!(css_classes.contains("hover:animate-shake"));
533        assert!(css_classes.contains("focus:animate-wobble"));
534
535        // Animation controls
536        assert!(css_classes.contains("animate-flip"));
537        assert!(css_classes.contains("animation-iteration-count-1"));
538        assert!(css_classes.contains("animate-heartbeat"));
539        assert!(css_classes.contains("duration-2000"));
540        assert!(css_classes.contains("animation-play-state-paused"));
541    }
542}