Skip to main content

oxiui_theme/
anim_tokens.rs

1//! Animation and transition tokens for OxiUI themes.
2//!
3//! Provides [`TransitionSpec`] for CSS-like single-property transitions and
4//! [`AnimationSpec`] for multi-keyframe animations. Standard presets
5//! ([`fade_in`], [`slide_in`], [`scale_up`]) encode common UI motion patterns.
6
7use std::collections::HashMap;
8
9/// The easing function for a transition or animation.
10#[derive(Clone, Debug, PartialEq)]
11pub enum EasingKind {
12    /// Constant velocity.
13    Linear,
14    /// Starts slow, ends fast.
15    EaseIn,
16    /// Starts fast, ends slow.
17    EaseOut,
18    /// Slow at both ends (most natural for UI).
19    EaseInOut,
20    /// Cubic Bézier — `(x1, y1, x2, y2)` control points.
21    CubicBezier(f32, f32, f32, f32),
22}
23
24/// A CSS-like transition specification for a single property.
25#[derive(Clone, Debug, PartialEq)]
26pub struct TransitionSpec {
27    /// Total transition duration in milliseconds.
28    pub duration_ms: u64,
29    /// Delay before the transition starts, in milliseconds.
30    pub delay_ms: u64,
31    /// Easing function to apply over the transition duration.
32    pub easing: EasingKind,
33}
34
35/// A single keyframe in an animation.
36#[derive(Clone, Debug, PartialEq)]
37pub struct AnimationKeyframe {
38    /// Position within the animation (0.0 = start, 1.0 = end).
39    pub offset: f32,
40    /// Property name → value pairs at this keyframe (CSS-like tokens).
41    pub props: HashMap<String, String>,
42}
43
44/// Fill mode for an animation (what values apply before/after the animation).
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum FillMode {
47    /// No fill: default values apply before and after.
48    None,
49    /// After the animation ends, hold the final keyframe values.
50    Forwards,
51    /// Before the animation starts (during `delay`), apply the first keyframe.
52    Backwards,
53    /// Combination of `Forwards` and `Backwards`.
54    Both,
55}
56
57/// How many times an animation repeats.
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum IterationCount {
60    /// A finite number of repetitions.
61    Count(u32),
62    /// Loops indefinitely.
63    Infinite,
64}
65
66/// A multi-keyframe animation specification.
67#[derive(Clone, Debug, PartialEq)]
68pub struct AnimationSpec {
69    /// Ordered keyframes (should be sorted by `offset`).
70    pub keyframes: Vec<AnimationKeyframe>,
71    /// Total single-iteration duration in milliseconds.
72    pub duration_ms: u64,
73    /// Fill behaviour before and after the animation.
74    pub fill_mode: FillMode,
75    /// Number of times the animation plays.
76    pub iteration_count: IterationCount,
77}
78
79// ── Standard presets ─────────────────────────────────────────────────────────
80
81/// Fade-in transition: 150 ms, `ease-in-out` opacity ramp.
82pub fn fade_in() -> TransitionSpec {
83    TransitionSpec {
84        duration_ms: 150,
85        delay_ms: 0,
86        easing: EasingKind::EaseInOut,
87    }
88}
89
90/// Slide-in transition: 200 ms, `ease-out` transform (entering elements).
91pub fn slide_in() -> TransitionSpec {
92    TransitionSpec {
93        duration_ms: 200,
94        delay_ms: 0,
95        easing: EasingKind::EaseOut,
96    }
97}
98
99/// Scale-up transition: 150 ms, `ease-in-out` scale (popovers / tooltips).
100pub fn scale_up() -> TransitionSpec {
101    TransitionSpec {
102        duration_ms: 150,
103        delay_ms: 0,
104        easing: EasingKind::EaseInOut,
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn fade_in_preset_present() {
114        let t = fade_in();
115        assert!(t.duration_ms > 0, "fade_in must have a positive duration");
116        assert_eq!(t.easing, EasingKind::EaseInOut);
117    }
118
119    #[test]
120    fn slide_in_preset_present() {
121        let t = slide_in();
122        assert!(t.duration_ms > 0, "slide_in must have a positive duration");
123        assert_eq!(t.easing, EasingKind::EaseOut);
124    }
125
126    #[test]
127    fn scale_up_preset_present() {
128        let t = scale_up();
129        assert!(t.duration_ms > 0, "scale_up must have a positive duration");
130        assert_eq!(t.easing, EasingKind::EaseInOut);
131    }
132
133    #[test]
134    fn easing_kind_cubic_bezier_stores_values() {
135        let e = EasingKind::CubicBezier(0.25, 0.1, 0.25, 1.0);
136        if let EasingKind::CubicBezier(x1, y1, x2, y2) = e {
137            assert_eq!(x1, 0.25);
138            assert_eq!(y1, 0.1);
139            assert_eq!(x2, 0.25);
140            assert_eq!(y2, 1.0);
141        } else {
142            panic!("unexpected variant");
143        }
144    }
145
146    #[test]
147    fn animation_spec_builds() {
148        let mut props = HashMap::new();
149        props.insert("opacity".to_string(), "0".to_string());
150        let spec = AnimationSpec {
151            keyframes: vec![
152                AnimationKeyframe {
153                    offset: 0.0,
154                    props: props.clone(),
155                },
156                AnimationKeyframe {
157                    offset: 1.0,
158                    props: {
159                        let mut p = HashMap::new();
160                        p.insert("opacity".to_string(), "1".to_string());
161                        p
162                    },
163                },
164            ],
165            duration_ms: 300,
166            fill_mode: FillMode::Forwards,
167            iteration_count: IterationCount::Count(1),
168        };
169        assert_eq!(spec.keyframes.len(), 2);
170        assert_eq!(spec.duration_ms, 300);
171    }
172
173    #[test]
174    fn iteration_count_infinite() {
175        let ic = IterationCount::Infinite;
176        assert_eq!(ic, IterationCount::Infinite);
177    }
178}