Skip to main content

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}