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}