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)]
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}