Skip to main content

native_theme/model/
animated.rs

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