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}