Skip to main content

native_theme/model/
animated.rs

1// Animated icon types: AnimatedIcon, TransformAnimation
2//
3// These types define the data model for animated icons in the native-theme
4// icon system. Frame-based animations supply pre-rendered frames with timing;
5// transform-based animations describe a CSS-like transform on a single icon.
6
7use serde::{Deserialize, Serialize};
8
9use super::icons::IconData;
10
11/// A CSS-like transform animation applied to a single icon.
12///
13/// # Examples
14///
15/// ```
16/// use native_theme::TransformAnimation;
17///
18/// let spin = TransformAnimation::Spin { duration_ms: 1000 };
19/// ```
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[non_exhaustive]
22pub enum TransformAnimation {
23    /// Continuous 360-degree rotation.
24    Spin {
25        /// Full rotation period in milliseconds.
26        duration_ms: u32,
27    },
28}
29
30/// An animated icon, either frame-based or transform-based.
31///
32/// `Frames` carries pre-rendered frames with uniform timing (loops infinitely).
33/// `Transform` carries a single icon and a description of the motion.
34///
35/// # Examples
36///
37/// ```
38/// use native_theme::{AnimatedIcon, IconData, TransformAnimation};
39///
40/// // Frame-based animation (e.g., sprite sheet)
41/// let frames_anim = AnimatedIcon::Frames {
42///     frames: vec![
43///         IconData::Svg(b"<svg>frame1</svg>".to_vec()),
44///         IconData::Svg(b"<svg>frame2</svg>".to_vec()),
45///     ],
46///     frame_duration_ms: 83,
47/// };
48///
49/// // Transform-based animation (e.g., spinning icon)
50/// let spin_anim = AnimatedIcon::Transform {
51///     icon: IconData::Svg(b"<svg>spinner</svg>".to_vec()),
52///     animation: TransformAnimation::Spin { duration_ms: 1000 },
53/// };
54/// ```
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[non_exhaustive]
57pub enum AnimatedIcon {
58    /// A sequence of pre-rendered frames played at a fixed interval (loops infinitely).
59    Frames {
60        /// The individual frames, each a complete icon image.
61        frames: Vec<IconData>,
62        /// Duration of each frame in milliseconds.
63        frame_duration_ms: u32,
64    },
65    /// A single icon with a continuous transform animation.
66    Transform {
67        /// The icon to animate.
68        icon: IconData,
69        /// The transform to apply.
70        animation: TransformAnimation,
71    },
72}
73
74impl AnimatedIcon {
75    /// Return a reference to the first displayable frame.
76    ///
77    /// For `Frames`, returns the first element (or `None` if empty).
78    /// For `Transform`, returns the underlying icon.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use native_theme::{AnimatedIcon, IconData, TransformAnimation};
84    ///
85    /// let frames = AnimatedIcon::Frames {
86    ///     frames: vec![IconData::Svg(b"<svg>f1</svg>".to_vec())],
87    ///     frame_duration_ms: 83,
88    /// };
89    /// assert!(frames.first_frame().is_some());
90    ///
91    /// let transform = AnimatedIcon::Transform {
92    ///     icon: IconData::Svg(b"<svg>spinner</svg>".to_vec()),
93    ///     animation: TransformAnimation::Spin { duration_ms: 1000 },
94    /// };
95    /// assert!(transform.first_frame().is_some());
96    /// ```
97    #[must_use]
98    pub fn first_frame(&self) -> Option<&IconData> {
99        match self {
100            AnimatedIcon::Frames { frames, .. } => frames.first(),
101            AnimatedIcon::Transform { icon, .. } => Some(icon),
102        }
103    }
104}
105
106#[cfg(test)]
107#[allow(clippy::unwrap_used, clippy::expect_used)]
108mod tests {
109    use super::*;
110
111    // === Construction tests ===
112
113    #[test]
114    fn frames_variant_constructs() {
115        let icon = AnimatedIcon::Frames {
116            frames: vec![IconData::Svg(b"<svg>f1</svg>".to_vec())],
117            frame_duration_ms: 83,
118        };
119        assert!(matches!(
120            icon,
121            AnimatedIcon::Frames {
122                frame_duration_ms: 83,
123                ..
124            }
125        ));
126    }
127
128    #[test]
129    fn transform_variant_constructs() {
130        let icon = AnimatedIcon::Transform {
131            icon: IconData::Svg(b"<svg>spinner</svg>".to_vec()),
132            animation: TransformAnimation::Spin { duration_ms: 1000 },
133        };
134        assert!(matches!(
135            icon,
136            AnimatedIcon::Transform {
137                animation: TransformAnimation::Spin { duration_ms: 1000 },
138                ..
139            }
140        ));
141    }
142
143    #[test]
144    #[allow(clippy::clone_on_copy)]
145    fn transform_animation_is_copy_clone_debug_eq_hash() {
146        let a = TransformAnimation::Spin { duration_ms: 500 };
147        let a2 = a; // Copy
148        let a3 = a.clone(); // Clone
149        assert_eq!(a2, a3); // PartialEq + Eq
150        use std::hash::Hash;
151        let mut hasher = std::collections::hash_map::DefaultHasher::new();
152        a.hash(&mut hasher);
153        let _ = format!("{a:?}"); // Debug
154    }
155
156    #[test]
157    fn animated_icon_is_clone_debug_eq_not_copy() {
158        let icon = AnimatedIcon::Frames {
159            frames: vec![IconData::Svg(b"<svg/>".to_vec())],
160            frame_duration_ms: 100,
161        };
162        let cloned = icon.clone(); // Clone
163        assert_eq!(icon, cloned); // PartialEq + Eq
164        let _ = format!("{icon:?}"); // Debug
165        // NOT Copy -- contains Vec, so this is inherently non-Copy
166    }
167
168    // === first_frame() tests ===
169
170    #[test]
171    fn first_frame_frames_with_items() {
172        let f0 = IconData::Svg(b"<svg>frame0</svg>".to_vec());
173        let f1 = IconData::Svg(b"<svg>frame1</svg>".to_vec());
174        let icon = AnimatedIcon::Frames {
175            frames: vec![f0.clone(), f1],
176            frame_duration_ms: 100,
177        };
178        assert_eq!(icon.first_frame(), Some(&f0));
179    }
180
181    #[test]
182    fn first_frame_frames_empty() {
183        let icon = AnimatedIcon::Frames {
184            frames: vec![],
185            frame_duration_ms: 100,
186        };
187        assert_eq!(icon.first_frame(), None);
188    }
189
190    #[test]
191    fn first_frame_transform() {
192        let data = IconData::Svg(b"<svg>spin</svg>".to_vec());
193        let icon = AnimatedIcon::Transform {
194            icon: data.clone(),
195            animation: TransformAnimation::Spin { duration_ms: 1000 },
196        };
197        assert_eq!(icon.first_frame(), Some(&data));
198    }
199}