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)]
125#[allow(clippy::unwrap_used, clippy::expect_used)]
126mod tests {
127 use super::*;
128
129 // === Construction tests ===
130
131 #[test]
132 fn frames_variant_constructs() {
133 let icon = AnimatedIcon::Frames {
134 frames: vec![IconData::Svg(b"<svg>f1</svg>".to_vec())],
135 frame_duration_ms: 83,
136 repeat: Repeat::Infinite,
137 };
138 assert!(matches!(
139 icon,
140 AnimatedIcon::Frames {
141 frame_duration_ms: 83,
142 ..
143 }
144 ));
145 }
146
147 #[test]
148 fn transform_variant_constructs() {
149 let icon = AnimatedIcon::Transform {
150 icon: IconData::Svg(b"<svg>spinner</svg>".to_vec()),
151 animation: TransformAnimation::Spin { duration_ms: 1000 },
152 };
153 assert!(matches!(
154 icon,
155 AnimatedIcon::Transform {
156 animation: TransformAnimation::Spin { duration_ms: 1000 },
157 ..
158 }
159 ));
160 }
161
162 #[test]
163 #[allow(clippy::clone_on_copy)]
164 fn repeat_is_copy_clone_debug_eq_hash() {
165 let r = Repeat::Infinite;
166 let r2 = r; // Copy
167 let r3 = r.clone(); // Clone
168 assert_eq!(r2, r3); // PartialEq + Eq
169 // Hash: just verify it compiles
170 use std::hash::Hash;
171 let mut hasher = std::collections::hash_map::DefaultHasher::new();
172 r.hash(&mut hasher);
173 let _ = format!("{r:?}"); // Debug
174 }
175
176 #[test]
177 #[allow(clippy::clone_on_copy)]
178 fn transform_animation_is_copy_clone_debug_eq_hash() {
179 let a = TransformAnimation::Spin { duration_ms: 500 };
180 let a2 = a; // Copy
181 let a3 = a.clone(); // Clone
182 assert_eq!(a2, a3); // PartialEq + Eq
183 use std::hash::Hash;
184 let mut hasher = std::collections::hash_map::DefaultHasher::new();
185 a.hash(&mut hasher);
186 let _ = format!("{a:?}"); // Debug
187 }
188
189 #[test]
190 fn animated_icon_is_clone_debug_eq_not_copy() {
191 let icon = AnimatedIcon::Frames {
192 frames: vec![IconData::Svg(b"<svg/>".to_vec())],
193 frame_duration_ms: 100,
194 repeat: Repeat::Infinite,
195 };
196 let cloned = icon.clone(); // Clone
197 assert_eq!(icon, cloned); // PartialEq + Eq
198 let _ = format!("{icon:?}"); // Debug
199 // NOT Copy -- contains Vec, so this is inherently non-Copy
200 }
201
202 // === first_frame() tests ===
203
204 #[test]
205 fn first_frame_frames_with_items() {
206 let f0 = IconData::Svg(b"<svg>frame0</svg>".to_vec());
207 let f1 = IconData::Svg(b"<svg>frame1</svg>".to_vec());
208 let icon = AnimatedIcon::Frames {
209 frames: vec![f0.clone(), f1],
210 frame_duration_ms: 100,
211 repeat: Repeat::Infinite,
212 };
213 assert_eq!(icon.first_frame(), Some(&f0));
214 }
215
216 #[test]
217 fn first_frame_frames_empty() {
218 let icon = AnimatedIcon::Frames {
219 frames: vec![],
220 frame_duration_ms: 100,
221 repeat: Repeat::Infinite,
222 };
223 assert_eq!(icon.first_frame(), None);
224 }
225
226 #[test]
227 fn first_frame_transform() {
228 let data = IconData::Svg(b"<svg>spin</svg>".to_vec());
229 let icon = AnimatedIcon::Transform {
230 icon: data.clone(),
231 animation: TransformAnimation::Spin { duration_ms: 1000 },
232 };
233 assert_eq!(icon.first_frame(), Some(&data));
234 }
235}