Skip to main content

oracle_lib/ui/
animation.rs

1//! Animation system for smooth UI transitions
2
3use std::time::{Duration, Instant};
4
5/// Easing functions for smooth animations
6#[derive(Debug, Clone, Copy, Default)]
7pub enum Easing {
8    #[default]
9    Linear,
10    EaseIn,
11    EaseOut,
12    EaseInOut,
13    Bounce,
14}
15
16impl Easing {
17    /// Apply easing function to a progress value (0.0 to 1.0)
18    pub fn apply(self, t: f64) -> f64 {
19        let t = t.clamp(0.0, 1.0);
20        match self {
21            Easing::Linear => t,
22            Easing::EaseIn => t * t * t,
23            Easing::EaseOut => 1.0 - (1.0 - t).powi(3),
24            Easing::EaseInOut => {
25                if t < 0.5 {
26                    4.0 * t * t * t
27                } else {
28                    1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
29                }
30            }
31            Easing::Bounce => {
32                if t < 1.0 / 2.75 {
33                    7.5625 * t * t
34                } else if t < 2.0 / 2.75 {
35                    let t = t - 1.5 / 2.75;
36                    7.5625 * t * t + 0.75
37                } else if t < 2.5 / 2.75 {
38                    let t = t - 2.25 / 2.75;
39                    7.5625 * t * t + 0.9375
40                } else {
41                    let t = t - 2.625 / 2.75;
42                    7.5625 * t * t + 0.984375
43                }
44            }
45        }
46    }
47}
48
49/// A single animation that interpolates between values
50#[derive(Debug, Clone)]
51pub struct Animation {
52    start_value: f64,
53    end_value: f64,
54    duration: Duration,
55    start_time: Option<Instant>,
56    easing: Easing,
57}
58
59impl Animation {
60    pub fn new(start: f64, end: f64, duration: Duration) -> Self {
61        Self {
62            start_value: start,
63            end_value: end,
64            duration,
65            start_time: None,
66            easing: Easing::EaseOut,
67        }
68    }
69
70    pub fn with_easing(mut self, easing: Easing) -> Self {
71        self.easing = easing;
72        self
73    }
74
75    /// Start the animation
76    pub fn start(&mut self) {
77        self.start_time = Some(Instant::now());
78    }
79
80    /// Get current animated value
81    pub fn value(&self) -> f64 {
82        let Some(start) = self.start_time else {
83            return self.start_value;
84        };
85
86        let elapsed = start.elapsed();
87        if elapsed >= self.duration {
88            return self.end_value;
89        }
90
91        let progress = elapsed.as_secs_f64() / self.duration.as_secs_f64();
92        let eased = self.easing.apply(progress);
93        self.start_value + (self.end_value - self.start_value) * eased
94    }
95
96    /// Check if animation is complete
97    pub fn is_complete(&self) -> bool {
98        self.start_time
99            .map(|t| t.elapsed() >= self.duration)
100            .unwrap_or(false)
101    }
102
103    /// Check if animation is running
104    pub fn is_running(&self) -> bool {
105        self.start_time.is_some() && !self.is_complete()
106    }
107
108    /// Reset the animation with new target
109    pub fn retarget(&mut self, new_end: f64) {
110        self.start_value = self.value();
111        self.end_value = new_end;
112        self.start_time = Some(Instant::now());
113    }
114}
115
116/// Smooth scroll state for lists and panels
117#[derive(Debug, Clone)]
118pub struct SmoothScroll {
119    target: f64,
120    current: f64,
121    velocity: f64,
122    smoothness: f64,
123}
124
125impl Default for SmoothScroll {
126    fn default() -> Self {
127        Self {
128            target: 0.0,
129            current: 0.0,
130            velocity: 0.0,
131            smoothness: 0.15, // Lower = smoother, higher = snappier
132        }
133    }
134}
135
136impl SmoothScroll {
137    pub fn new() -> Self {
138        Self::default()
139    }
140
141    /// Set smoothness factor (0.0-1.0, lower is smoother)
142    pub fn with_smoothness(mut self, smoothness: f64) -> Self {
143        self.smoothness = smoothness.clamp(0.01, 1.0);
144        self
145    }
146
147    /// Set target scroll position
148    pub fn scroll_to(&mut self, target: f64) {
149        self.target = target;
150    }
151
152    /// Add to current target
153    pub fn scroll_by(&mut self, delta: f64) {
154        self.target += delta;
155    }
156
157    /// Update animation (call each frame)
158    pub fn update(&mut self) {
159        let diff = self.target - self.current;
160        self.velocity = diff * self.smoothness;
161        self.current += self.velocity;
162
163        // Snap to target if very close
164        if diff.abs() < 0.5 {
165            self.current = self.target;
166            self.velocity = 0.0;
167        }
168    }
169
170    /// Get current scroll position (as integer for rendering)
171    pub fn position(&self) -> usize {
172        self.current.max(0.0) as usize
173    }
174
175    /// Get precise position
176    pub fn position_f64(&self) -> f64 {
177        self.current
178    }
179
180    /// Check if scrolling is active
181    pub fn is_scrolling(&self) -> bool {
182        self.velocity.abs() > 0.1
183    }
184
185    /// Set position immediately (no animation)
186    pub fn set_immediate(&mut self, position: f64) {
187        self.current = position;
188        self.target = position;
189        self.velocity = 0.0;
190    }
191}
192
193/// Fade animation for smooth opacity transitions
194#[derive(Debug, Clone)]
195pub struct Fade {
196    opacity: f64,
197    target_opacity: f64,
198    fade_speed: f64,
199}
200
201impl Default for Fade {
202    fn default() -> Self {
203        Self {
204            opacity: 1.0,
205            target_opacity: 1.0,
206            fade_speed: 0.15,
207        }
208    }
209}
210
211impl Fade {
212    pub fn new() -> Self {
213        Self::default()
214    }
215
216    pub fn fade_in(&mut self) {
217        self.target_opacity = 1.0;
218    }
219
220    pub fn fade_out(&mut self) {
221        self.target_opacity = 0.0;
222    }
223
224    pub fn set_target(&mut self, target: f64) {
225        self.target_opacity = target.clamp(0.0, 1.0);
226    }
227
228    pub fn update(&mut self) {
229        let diff = self.target_opacity - self.opacity;
230        if diff.abs() < 0.01 {
231            self.opacity = self.target_opacity;
232        } else {
233            self.opacity += diff * self.fade_speed;
234        }
235    }
236
237    pub fn opacity(&self) -> f64 {
238        self.opacity
239    }
240
241    pub fn is_visible(&self) -> bool {
242        self.opacity > 0.01
243    }
244}
245
246/// Pulse animation for attention-grabbing effects
247#[derive(Debug, Clone)]
248pub struct Pulse {
249    phase: f64,
250    speed: f64,
251    min_value: f64,
252    max_value: f64,
253}
254
255impl Default for Pulse {
256    fn default() -> Self {
257        Self {
258            phase: 0.0,
259            speed: 0.1,
260            min_value: 0.7,
261            max_value: 1.0,
262        }
263    }
264}
265
266impl Pulse {
267    pub fn new() -> Self {
268        Self::default()
269    }
270
271    pub fn with_range(mut self, min: f64, max: f64) -> Self {
272        self.min_value = min;
273        self.max_value = max;
274        self
275    }
276
277    pub fn with_speed(mut self, speed: f64) -> Self {
278        self.speed = speed;
279        self
280    }
281
282    pub fn update(&mut self) {
283        self.phase += self.speed;
284        if self.phase > std::f64::consts::TAU {
285            self.phase -= std::f64::consts::TAU;
286        }
287    }
288
289    pub fn value(&self) -> f64 {
290        let sin = (self.phase.sin() + 1.0) / 2.0; // Normalize to 0-1
291        self.min_value + sin * (self.max_value - self.min_value)
292    }
293}
294
295/// Collection of UI animation states
296#[derive(Debug, Default)]
297pub struct AnimationState {
298    pub list_scroll: SmoothScroll,
299    pub inspector_scroll: SmoothScroll,
300    pub search_cursor: Pulse,
301    pub selection_highlight: f64, // 0.0-1.0 for selection animation
302    pub transition_progress: f64, // For tab transitions
303}
304
305impl AnimationState {
306    pub fn new() -> Self {
307        Self {
308            list_scroll: SmoothScroll::new().with_smoothness(0.2),
309            inspector_scroll: SmoothScroll::new().with_smoothness(0.15),
310            search_cursor: Pulse::new().with_speed(0.15),
311            selection_highlight: 1.0,
312            transition_progress: 1.0,
313        }
314    }
315
316    /// Update all animations (call each frame)
317    pub fn update(&mut self) {
318        self.list_scroll.update();
319        self.inspector_scroll.update();
320        self.search_cursor.update();
321
322        // Animate selection highlight
323        if self.selection_highlight < 1.0 {
324            self.selection_highlight = (self.selection_highlight + 0.2).min(1.0);
325        }
326
327        // Animate tab transitions
328        if self.transition_progress < 1.0 {
329            self.transition_progress = (self.transition_progress + 0.15).min(1.0);
330        }
331    }
332
333    /// Trigger selection animation
334    pub fn on_selection_change(&mut self) {
335        self.selection_highlight = 0.0;
336    }
337
338    /// Trigger tab transition animation
339    pub fn on_tab_change(&mut self) {
340        self.transition_progress = 0.0;
341    }
342
343    /// Check if any animation is active
344    pub fn is_animating(&self) -> bool {
345        self.list_scroll.is_scrolling()
346            || self.inspector_scroll.is_scrolling()
347            || self.selection_highlight < 1.0
348            || self.transition_progress < 1.0
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn test_easing_bounds() {
358        for easing in [
359            Easing::Linear,
360            Easing::EaseIn,
361            Easing::EaseOut,
362            Easing::EaseInOut,
363        ] {
364            assert!((easing.apply(0.0) - 0.0).abs() < 0.001);
365            assert!((easing.apply(1.0) - 1.0).abs() < 0.001);
366        }
367    }
368
369    #[test]
370    fn test_smooth_scroll() {
371        let mut scroll = SmoothScroll::new();
372        scroll.scroll_to(100.0);
373
374        for _ in 0..50 {
375            scroll.update();
376        }
377
378        assert!((scroll.position_f64() - 100.0).abs() < 1.0);
379    }
380}