Skip to main content

fui/
animation.rs

1use crate::frame_scheduler::mark_needs_commit;
2use std::cell::RefCell;
3use std::rc::Rc;
4
5const MAX_FRAME_DELTA_MS: f64 = 100.0;
6
7fn clamp_unit(value: f32) -> f32 {
8    value.clamp(0.0, 1.0)
9}
10
11fn clamp_frame_delta(delta_ms: f64) -> f64 {
12    delta_ms.clamp(0.0, MAX_FRAME_DELTA_MS)
13}
14
15fn mix_float(from: f32, to: f32, amount: f32) -> f32 {
16    from + ((to - from) * clamp_unit(amount))
17}
18
19fn mix_color_channel(from: u32, to: u32, amount: f32) -> u32 {
20    let mixed = (from as f32) + (((to as f32) - (from as f32)) * clamp_unit(amount));
21    mixed.round().clamp(0.0, 255.0) as u32
22}
23
24fn mix_color(from: u32, to: u32, amount: f32) -> u32 {
25    let fr = (from >> 24) & 0xFF;
26    let fg = (from >> 16) & 0xFF;
27    let fb = (from >> 8) & 0xFF;
28    let fa = from & 0xFF;
29    let tr = (to >> 24) & 0xFF;
30    let tg = (to >> 16) & 0xFF;
31    let tb = (to >> 8) & 0xFF;
32    let ta = to & 0xFF;
33    (mix_color_channel(fr, tr, amount) << 24)
34        | (mix_color_channel(fg, tg, amount) << 16)
35        | (mix_color_channel(fb, tb, amount) << 8)
36        | mix_color_channel(fa, ta, amount)
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum Easing {
41    Linear,
42    CubicIn,
43    CubicOut,
44    CubicInOut,
45    QuadOut,
46}
47
48impl Easing {
49    pub fn sample(self, progress: f32) -> f32 {
50        let t = clamp_unit(progress);
51        match self {
52            Self::Linear => t,
53            Self::CubicIn => t * t * t,
54            Self::CubicOut => {
55                let offset = t - 1.0;
56                (offset * offset * offset) + 1.0
57            }
58            Self::CubicInOut => {
59                if t < 0.5 {
60                    4.0 * t * t * t
61                } else {
62                    let offset = (-2.0 * t) + 2.0;
63                    1.0 - ((offset * offset * offset) * 0.5)
64                }
65            }
66            Self::QuadOut => {
67                let offset = 1.0 - t;
68                1.0 - (offset * offset)
69            }
70        }
71    }
72}
73
74pub struct Easings;
75
76impl Easings {
77    pub fn linear() -> Easing {
78        Easing::Linear
79    }
80
81    pub fn cubic_in() -> Easing {
82        Easing::CubicIn
83    }
84
85    pub fn cubic_out() -> Easing {
86        Easing::CubicOut
87    }
88
89    pub fn cubic_in_out() -> Easing {
90        Easing::CubicInOut
91    }
92
93    pub fn quad_out() -> Easing {
94        Easing::QuadOut
95    }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq)]
99pub struct AnimationTiming {
100    pub duration_ms: f64,
101    pub easing: Easing,
102}
103
104impl AnimationTiming {
105    pub fn new(duration_ms: f64) -> Self {
106        Self {
107            duration_ms: duration_ms.max(0.0),
108            easing: Easing::Linear,
109        }
110    }
111
112    pub fn with_easing(duration_ms: f64, easing: Easing) -> Self {
113        Self {
114            duration_ms: duration_ms.max(0.0),
115            easing,
116        }
117    }
118}
119
120trait AnimationDriver {
121    fn on_start(&mut self, _timestamp_ms: f64) {}
122    fn on_sample(&mut self, eased_progress: f32, linear_progress: f32);
123    fn on_stop(&mut self, _finished: bool) {}
124}
125
126struct AnimationState {
127    timing: AnimationTiming,
128    running: bool,
129    started: bool,
130    last_timestamp_ms: f64,
131    elapsed_ms: f64,
132    driver: Box<dyn AnimationDriver>,
133}
134
135#[derive(Clone)]
136pub struct Animation {
137    inner: Rc<RefCell<AnimationState>>,
138}
139
140impl Animation {
141    fn new(timing: AnimationTiming, driver: Box<dyn AnimationDriver>) -> Self {
142        Self {
143            inner: Rc::new(RefCell::new(AnimationState {
144                timing,
145                running: false,
146                started: false,
147                last_timestamp_ms: 0.0,
148                elapsed_ms: 0.0,
149                driver,
150            })),
151        }
152    }
153
154    pub fn is_running(&self) -> bool {
155        self.inner.borrow().running
156    }
157
158    pub fn cancel(&self) {
159        get_animation_manager().cancel(self);
160    }
161
162    pub fn finish(&self) {
163        get_animation_manager().finish(self);
164    }
165
166    pub fn dispose(&self) {
167        self.cancel();
168    }
169
170    fn attach(&self, known_timestamp_ms: f64, has_known_timestamp: bool) {
171        {
172            let mut state = self.inner.borrow_mut();
173            state.running = true;
174            state.started = false;
175            state.last_timestamp_ms = 0.0;
176            state.elapsed_ms = 0.0;
177        }
178        if has_known_timestamp {
179            self.start_internal(known_timestamp_ms);
180        }
181    }
182
183    fn tick(&self, timestamp_ms: f64) {
184        if !self.is_running() {
185            return;
186        }
187        if !self.inner.borrow().started {
188            self.start_internal(timestamp_ms);
189            return;
190        }
191        let (duration_ms, easing, last_timestamp_ms) = {
192            let state = self.inner.borrow();
193            (
194                state.timing.duration_ms,
195                state.timing.easing,
196                state.last_timestamp_ms,
197            )
198        };
199        let delta_ms = clamp_frame_delta(timestamp_ms - last_timestamp_ms);
200        let should_finish = {
201            let mut state = self.inner.borrow_mut();
202            state.last_timestamp_ms = timestamp_ms;
203            state.elapsed_ms += delta_ms;
204            let progress = if duration_ms <= 0.0 {
205                1.0
206            } else {
207                clamp_unit((state.elapsed_ms / duration_ms) as f32)
208            };
209            state.driver.on_sample(easing.sample(progress), progress);
210            progress >= 1.0
211        };
212        if should_finish {
213            self.stop(true);
214        }
215    }
216
217    fn cancel_internal(&self) {
218        if self.is_running() {
219            self.stop(false);
220        }
221    }
222
223    fn finish_internal(&self) {
224        if !self.is_running() {
225            return;
226        }
227        if !self.inner.borrow().started {
228            let mut state = self.inner.borrow_mut();
229            state.started = true;
230            state.last_timestamp_ms = 0.0;
231            state.elapsed_ms = state.timing.duration_ms;
232            state.driver.on_start(0.0);
233        }
234        self.inner.borrow_mut().driver.on_sample(1.0, 1.0);
235        self.stop(true);
236    }
237
238    fn start_internal(&self, timestamp_ms: f64) {
239        let duration_ms = {
240            let mut state = self.inner.borrow_mut();
241            state.started = true;
242            state.last_timestamp_ms = timestamp_ms;
243            state.elapsed_ms = 0.0;
244            state.driver.on_start(timestamp_ms);
245            state.timing.duration_ms
246        };
247        if duration_ms <= 0.0 {
248            self.inner.borrow_mut().driver.on_sample(1.0, 1.0);
249            self.stop(true);
250            return;
251        }
252        let easing = self.inner.borrow().timing.easing;
253        self.inner
254            .borrow_mut()
255            .driver
256            .on_sample(easing.sample(0.0), 0.0);
257    }
258
259    fn stop(&self, finished: bool) {
260        let mut state = self.inner.borrow_mut();
261        state.running = false;
262        state.driver.on_stop(finished);
263    }
264}
265
266#[derive(Default)]
267struct AnimationManagerState {
268    active_animations: Vec<Animation>,
269    last_timestamp_ms: f64,
270    has_last_timestamp: bool,
271}
272
273thread_local! {
274    static ANIMATION_MANAGER: RefCell<AnimationManagerState> = const { RefCell::new(AnimationManagerState {
275        active_animations: Vec::new(),
276        last_timestamp_ms: 0.0,
277        has_last_timestamp: false,
278    }) };
279}
280
281#[derive(Clone, Copy, Debug, Default)]
282pub struct AnimationManager;
283
284impl AnimationManager {
285    pub fn start(&self, animation: Animation) -> Animation {
286        self.cancel(&animation);
287        ANIMATION_MANAGER.with(|slot| {
288            let mut state = slot.borrow_mut();
289            animation.attach(state.last_timestamp_ms, state.has_last_timestamp);
290            if animation.is_running() {
291                state.active_animations.push(animation.clone());
292            }
293        });
294        mark_needs_commit();
295        animation
296    }
297
298    pub fn cancel(&self, animation: &Animation) {
299        ANIMATION_MANAGER.with(|slot| {
300            let mut state = slot.borrow_mut();
301            if let Some(index) = state
302                .active_animations
303                .iter()
304                .position(|candidate| Rc::ptr_eq(&candidate.inner, &animation.inner))
305            {
306                state.active_animations.swap_remove(index);
307                animation.cancel_internal();
308            }
309        });
310    }
311
312    pub fn finish(&self, animation: &Animation) {
313        ANIMATION_MANAGER.with(|slot| {
314            let mut state = slot.borrow_mut();
315            if let Some(index) = state
316                .active_animations
317                .iter()
318                .position(|candidate| Rc::ptr_eq(&candidate.inner, &animation.inner))
319            {
320                state.active_animations.swap_remove(index);
321                animation.finish_internal();
322            }
323        });
324    }
325
326    pub fn tick(&self, timestamp_ms: f64) {
327        let active_animations = ANIMATION_MANAGER.with(|slot| {
328            let mut state = slot.borrow_mut();
329            state.last_timestamp_ms = timestamp_ms;
330            state.has_last_timestamp = true;
331            state.active_animations.clone()
332        });
333        let mut completed = Vec::new();
334        let mut has_active = false;
335        for animation in active_animations {
336            animation.tick(timestamp_ms);
337            if animation.is_running() {
338                has_active = true;
339            } else {
340                completed.push(animation);
341            }
342        }
343        if !completed.is_empty() {
344            ANIMATION_MANAGER.with(|slot| {
345                let mut state = slot.borrow_mut();
346                state.active_animations.retain(|candidate| {
347                    !completed.iter().any(|completed_animation| {
348                        Rc::ptr_eq(&candidate.inner, &completed_animation.inner)
349                    })
350                });
351            });
352        }
353        if has_active {
354            mark_needs_commit();
355        }
356    }
357
358    pub fn has_active_animations(&self) -> bool {
359        ANIMATION_MANAGER.with(|slot| !slot.borrow().active_animations.is_empty())
360    }
361
362    pub fn reset(&self) {
363        ANIMATION_MANAGER.with(|slot| {
364            let mut state = slot.borrow_mut();
365            for animation in state.active_animations.drain(..) {
366                animation.cancel_internal();
367            }
368            state.last_timestamp_ms = 0.0;
369            state.has_last_timestamp = false;
370        });
371    }
372}
373
374pub fn get_animation_manager() -> AnimationManager {
375    AnimationManager
376}
377
378pub fn tick_animations(timestamp_ms: f64) {
379    get_animation_manager().tick(timestamp_ms);
380}
381
382pub fn reset_animations() {
383    get_animation_manager().reset();
384}
385
386struct FloatAnimationDriver {
387    from: f32,
388    to: f32,
389    handler: Box<dyn Fn(f32)>,
390}
391
392impl AnimationDriver for FloatAnimationDriver {
393    fn on_sample(&mut self, eased_progress: f32, _linear_progress: f32) {
394        (self.handler)(mix_float(self.from, self.to, eased_progress));
395    }
396}
397
398struct ColorAnimationDriver {
399    from: u32,
400    to: u32,
401    handler: Box<dyn Fn(u32)>,
402}
403
404impl AnimationDriver for ColorAnimationDriver {
405    fn on_sample(&mut self, eased_progress: f32, _linear_progress: f32) {
406        (self.handler)(mix_color(self.from, self.to, eased_progress));
407    }
408}
409
410pub fn animate_float(
411    from_value: f32,
412    to_value: f32,
413    timing: AnimationTiming,
414    handler: impl Fn(f32) + 'static,
415) -> Animation {
416    get_animation_manager().start(Animation::new(
417        timing,
418        Box::new(FloatAnimationDriver {
419            from: from_value,
420            to: to_value,
421            handler: Box::new(handler),
422        }),
423    ))
424}
425
426pub fn animate_float_with<Owner: 'static>(
427    owner: Owner,
428    from_value: f32,
429    to_value: f32,
430    timing: AnimationTiming,
431    handler: impl Fn(&Owner, f32) + 'static,
432) -> Animation {
433    animate_float(from_value, to_value, timing, move |value| {
434        handler(&owner, value);
435    })
436}
437
438pub fn animate_color(
439    from_value: u32,
440    to_value: u32,
441    timing: AnimationTiming,
442    handler: impl Fn(u32) + 'static,
443) -> Animation {
444    get_animation_manager().start(Animation::new(
445        timing,
446        Box::new(ColorAnimationDriver {
447            from: from_value,
448            to: to_value,
449            handler: Box::new(handler),
450        }),
451    ))
452}
453
454pub fn animate_color_with<Owner: 'static>(
455    owner: Owner,
456    from_value: u32,
457    to_value: u32,
458    timing: AnimationTiming,
459    handler: impl Fn(&Owner, u32) + 'static,
460) -> Animation {
461    animate_color(from_value, to_value, timing, move |value| {
462        handler(&owner, value);
463    })
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469    use crate::bridge_callbacks::__fui_on_frame;
470    use crate::ffi::{self, Call};
471    use crate::frame_scheduler::reset_commit_state;
472    use std::cell::Cell;
473    use std::rc::Rc;
474
475    #[test]
476    fn advances_float_animations_from_frame_timestamps() {
477        reset_animations();
478        reset_commit_state();
479        let value = Rc::new(Cell::new(-1.0));
480        let value_for_handler = value.clone();
481        let animation = animate_float(10.0, 30.0, AnimationTiming::new(200.0), move |next| {
482            value_for_handler.set(next);
483        });
484        let manager = get_animation_manager();
485
486        manager.tick(1000.0);
487        assert_eq!(value.get(), 10.0);
488        assert!(animation.is_running());
489
490        manager.tick(1050.0);
491        assert_eq!(value.get(), 15.0);
492
493        manager.tick(1150.0);
494        assert_eq!(value.get(), 25.0);
495
496        manager.tick(1250.0);
497        assert_eq!(value.get(), 30.0);
498        assert!(!animation.is_running());
499        assert!(!manager.has_active_animations());
500    }
501
502    #[test]
503    fn clamps_stale_frame_gaps_instead_of_jumping_to_completion() {
504        reset_animations();
505        reset_commit_state();
506        let value = Rc::new(Cell::new(-1.0));
507        let value_for_handler = value.clone();
508        let manager = get_animation_manager();
509        animate_float(0.0, 100.0, AnimationTiming::new(200.0), move |next| {
510            value_for_handler.set(next);
511        });
512
513        manager.tick(1000.0);
514        manager.tick(1800.0);
515
516        assert_eq!(value.get(), 50.0);
517        assert!(manager.has_active_animations());
518    }
519
520    #[test]
521    fn supports_owner_bound_float_and_color_helpers() {
522        reset_animations();
523        reset_commit_state();
524        let float_owner = Rc::new(Cell::new(-1.0));
525        let color_owner = Rc::new(Cell::new(0u32));
526        let manager = get_animation_manager();
527
528        animate_float_with(
529            float_owner.clone(),
530            4.0,
531            12.0,
532            AnimationTiming::new(80.0),
533            |owner, value| owner.set(value),
534        );
535        animate_color_with(
536            color_owner.clone(),
537            0x000000FF,
538            0xFF0000FF,
539            AnimationTiming::new(80.0),
540            |owner, value| owner.set(value),
541        );
542
543        manager.tick(500.0);
544        assert_eq!(float_owner.get(), 4.0);
545        assert_eq!(color_owner.get(), 0x000000FF);
546
547        manager.tick(540.0);
548        assert_eq!(float_owner.get(), 8.0);
549        assert_eq!(color_owner.get(), 0x800000FF);
550
551        manager.tick(580.0);
552        assert_eq!(float_owner.get(), 12.0);
553        assert_eq!(color_owner.get(), 0xFF0000FF);
554    }
555
556    #[test]
557    fn cancels_and_finishes_animations_through_the_animation_object() {
558        reset_animations();
559        reset_commit_state();
560        let value = Rc::new(Cell::new(-1.0));
561        let first_value = value.clone();
562        let animation = animate_float(0.0, 100.0, AnimationTiming::new(100.0), move |next| {
563            first_value.set(next);
564        });
565        let manager = get_animation_manager();
566
567        manager.tick(100.0);
568        manager.tick(140.0);
569        assert_eq!(value.get(), 40.0);
570
571        animation.cancel();
572        manager.tick(200.0);
573        assert_eq!(value.get(), 40.0);
574        assert!(!animation.is_running());
575
576        let second_value = value.clone();
577        let second = animate_float(0.0, 100.0, AnimationTiming::new(100.0), move |next| {
578            second_value.set(next);
579        });
580        manager.tick(300.0);
581        second.finish();
582        assert_eq!(value.get(), 100.0);
583        assert!(!second.is_running());
584        assert!(!manager.has_active_animations());
585    }
586
587    #[test]
588    fn uses_the_shared_frame_hook_to_tick_active_animations() {
589        reset_animations();
590        reset_commit_state();
591        let owner = Rc::new(Cell::new(-1.0));
592        animate_float_with(
593            owner.clone(),
594            2.0,
595            6.0,
596            AnimationTiming::new(100.0),
597            |slot, value| {
598                slot.set(value);
599            },
600        );
601
602        __fui_on_frame(1000.0);
603        assert_eq!(crate::frame_signal::frame_time_signal().value(), 1000.0);
604        assert_eq!(owner.get(), 2.0);
605
606        __fui_on_frame(1050.0);
607        assert_eq!(owner.get(), 4.0);
608    }
609
610    #[test]
611    fn requests_render_when_animation_starts() {
612        reset_animations();
613        reset_commit_state();
614        ffi::test::reset();
615        animate_float(0.0, 1.0, AnimationTiming::new(50.0), |_| {});
616        let calls = ffi::test::take_calls();
617        assert_eq!(
618            calls
619                .iter()
620                .filter(|call| matches!(call, Call::RequestRender))
621                .count(),
622            1
623        );
624    }
625}