oracle_lib/ui/
animation.rs1use std::time::{Duration, Instant};
4
5#[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 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#[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 pub fn start(&mut self) {
77 self.start_time = Some(Instant::now());
78 }
79
80 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 pub fn is_complete(&self) -> bool {
98 self.start_time
99 .map(|t| t.elapsed() >= self.duration)
100 .unwrap_or(false)
101 }
102
103 pub fn is_running(&self) -> bool {
105 self.start_time.is_some() && !self.is_complete()
106 }
107
108 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#[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, }
133 }
134}
135
136impl SmoothScroll {
137 pub fn new() -> Self {
138 Self::default()
139 }
140
141 pub fn with_smoothness(mut self, smoothness: f64) -> Self {
143 self.smoothness = smoothness.clamp(0.01, 1.0);
144 self
145 }
146
147 pub fn scroll_to(&mut self, target: f64) {
149 self.target = target;
150 }
151
152 pub fn scroll_by(&mut self, delta: f64) {
154 self.target += delta;
155 }
156
157 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 if diff.abs() < 0.5 {
165 self.current = self.target;
166 self.velocity = 0.0;
167 }
168 }
169
170 pub fn position(&self) -> usize {
172 self.current.max(0.0) as usize
173 }
174
175 pub fn position_f64(&self) -> f64 {
177 self.current
178 }
179
180 pub fn is_scrolling(&self) -> bool {
182 self.velocity.abs() > 0.1
183 }
184
185 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#[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#[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; self.min_value + sin * (self.max_value - self.min_value)
292 }
293}
294
295#[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, pub transition_progress: f64, }
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 pub fn update(&mut self) {
318 self.list_scroll.update();
319 self.inspector_scroll.update();
320 self.search_cursor.update();
321
322 if self.selection_highlight < 1.0 {
324 self.selection_highlight = (self.selection_highlight + 0.2).min(1.0);
325 }
326
327 if self.transition_progress < 1.0 {
329 self.transition_progress = (self.transition_progress + 0.15).min(1.0);
330 }
331 }
332
333 pub fn on_selection_change(&mut self) {
335 self.selection_highlight = 0.0;
336 }
337
338 pub fn on_tab_change(&mut self) {
340 self.transition_progress = 0.0;
341 }
342
343 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}