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