Skip to main content

animato_dioxus/
hooks.rs

1//! Dioxus `Signal`-backed animation hooks.
2
3use animato_core::{Animatable, Update};
4use animato_spring::{Decompose, SpringConfig, SpringN};
5use animato_timeline::{Timeline, TimelineState};
6use animato_tween::{KeyframeTrack, Tween, TweenBuilder};
7use dioxus::prelude::{Signal, use_signal};
8use std::fmt;
9use std::sync::{Arc, Mutex};
10
11/// Control handle for a Dioxus signal-backed [`Tween`].
12#[derive(Clone)]
13pub struct TweenHandle<T: Animatable + Send + Sync + 'static> {
14    tween: Arc<Mutex<Tween<T>>>,
15    value: Signal<T>,
16    progress: Signal<f32>,
17    complete: Signal<bool>,
18}
19
20impl<T: Animatable + Send + Sync + 'static> fmt::Debug for TweenHandle<T> {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        f.debug_struct("TweenHandle").finish_non_exhaustive()
23    }
24}
25
26impl<T: Animatable + Send + Sync + 'static> TweenHandle<T> {
27    /// Resume playback, resetting first if the tween has completed.
28    pub fn play(&self) {
29        crate::with_lock(&self.tween, |tween| {
30            if tween.is_complete() {
31                tween.reset();
32            }
33            tween.resume();
34            self.sync(tween);
35        });
36    }
37
38    /// Pause playback.
39    pub fn pause(&self) {
40        crate::with_lock(&self.tween, Tween::pause);
41    }
42
43    /// Resume playback.
44    pub fn resume(&self) {
45        crate::with_lock(&self.tween, Tween::resume);
46    }
47
48    /// Reset the tween to the beginning.
49    pub fn reset(&self) {
50        crate::with_lock(&self.tween, |tween| {
51            tween.reset();
52            self.sync(tween);
53        });
54    }
55
56    /// Reverse direction while preserving the current visual progress.
57    pub fn reverse(&self) {
58        crate::with_lock(&self.tween, |tween| {
59            tween.reverse();
60            self.sync(tween);
61        });
62    }
63
64    /// Seek to normalized progress in `[0.0, 1.0]`.
65    pub fn seek(&self, progress: f32) {
66        crate::with_lock(&self.tween, |tween| {
67            tween.seek(progress);
68            self.sync(tween);
69        });
70    }
71
72    /// Set the playback time scale. Non-finite values become `1.0`.
73    pub fn set_time_scale(&self, scale: f32) {
74        crate::with_lock(&self.tween, |tween| {
75            tween.time_scale = crate::finite_or(scale, 1.0).max(0.0);
76        });
77    }
78
79    /// Current value signal.
80    pub fn value(&self) -> Signal<T> {
81        self.value
82    }
83
84    /// Completion signal.
85    pub fn is_complete(&self) -> Signal<bool> {
86        self.complete
87    }
88
89    /// Raw normalized progress signal.
90    pub fn progress(&self) -> Signal<f32> {
91        self.progress
92    }
93
94    /// Deterministically advance the tween by `dt` seconds.
95    pub fn tick(&self, dt: f32) -> bool {
96        crate::with_lock(&self.tween, |tween| {
97            let running = tween.update(dt.max(0.0));
98            self.sync(tween);
99            running
100        })
101    }
102
103    fn sync(&self, tween: &Tween<T>) {
104        crate::set_signal(self.value, tween.value());
105        crate::set_signal(self.progress, tween.progress());
106        crate::set_signal(self.complete, tween.is_complete());
107    }
108}
109
110/// Create a signal-backed tween hook.
111pub fn use_tween<T>(
112    from: T,
113    to: T,
114    config: impl FnOnce(TweenBuilder<T>) -> TweenBuilder<T>,
115) -> (Signal<T>, TweenHandle<T>)
116where
117    T: Animatable + Send + Sync + 'static,
118{
119    let tween = config(Tween::new(from, to)).build();
120    let value = use_signal({
121        let initial = tween.value();
122        move || initial
123    });
124    let progress = use_signal({
125        let initial = tween.progress();
126        move || initial
127    });
128    let complete = use_signal({
129        let initial = tween.is_complete();
130        move || initial
131    });
132
133    let handle = TweenHandle {
134        tween: Arc::new(Mutex::new(tween)),
135        value,
136        progress,
137        complete,
138    };
139
140    let loop_handle = handle.clone();
141    crate::spawn_animation_loop(move |dt| {
142        loop_handle.tick(dt);
143        true
144    });
145
146    (value, handle)
147}
148
149/// Control handle for a Dioxus signal-backed [`SpringN`].
150#[derive(Clone)]
151pub struct SpringHandle<T: Decompose + Send + Sync + Clone + 'static> {
152    spring: Arc<Mutex<SpringN<T>>>,
153    value: Signal<T>,
154    settled: Signal<bool>,
155}
156
157impl<T: Decompose + Send + Sync + Clone + 'static> fmt::Debug for SpringHandle<T> {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        f.debug_struct("SpringHandle").finish_non_exhaustive()
160    }
161}
162
163impl<T: Decompose + Send + Sync + Clone + 'static> SpringHandle<T> {
164    /// Set a new spring target.
165    pub fn set_target(&self, target: T) {
166        crate::with_lock(&self.spring, |spring| {
167            spring.set_target(target);
168            crate::set_signal(self.settled, spring.is_settled());
169        });
170    }
171
172    /// Snap instantly to a value.
173    pub fn snap_to(&self, value: T) {
174        crate::with_lock(&self.spring, |spring| {
175            spring.snap_to(value);
176            self.sync(spring);
177        });
178    }
179
180    /// Current value signal.
181    pub fn value(&self) -> Signal<T> {
182        self.value
183    }
184
185    /// Settled-state signal.
186    pub fn is_settled(&self) -> Signal<bool> {
187        self.settled
188    }
189
190    /// Deterministically advance the spring by `dt` seconds.
191    pub fn tick(&self, dt: f32) -> bool {
192        crate::with_lock(&self.spring, |spring| {
193            let running = spring.update(dt.max(0.0));
194            self.sync(spring);
195            running
196        })
197    }
198
199    fn sync(&self, spring: &SpringN<T>) {
200        crate::set_signal(self.value, spring.position());
201        crate::set_signal(self.settled, spring.is_settled());
202    }
203}
204
205/// Create a signal-backed spring hook.
206pub fn use_spring<T>(initial: T, config: SpringConfig) -> (Signal<T>, SpringHandle<T>)
207where
208    T: Decompose + Send + Sync + Clone + 'static,
209{
210    let spring = SpringN::new(config, initial.clone());
211    let value = use_signal(move || initial);
212    let settled = use_signal(|| true);
213    let handle = SpringHandle {
214        spring: Arc::new(Mutex::new(spring)),
215        value,
216        settled,
217    };
218
219    let loop_handle = handle.clone();
220    crate::spawn_animation_loop(move |dt| {
221        loop_handle.tick(dt);
222        true
223    });
224
225    (value, handle)
226}
227
228/// Control handle for a Dioxus signal-backed [`Timeline`].
229#[derive(Clone)]
230pub struct TimelineHandle {
231    timeline: Arc<Mutex<Timeline>>,
232    progress: Signal<f32>,
233    complete: Signal<bool>,
234    state: Signal<TimelineState>,
235}
236
237impl fmt::Debug for TimelineHandle {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        f.debug_struct("TimelineHandle").finish_non_exhaustive()
240    }
241}
242
243impl TimelineHandle {
244    /// Start timeline playback.
245    pub fn play(&self) {
246        crate::with_lock(&self.timeline, |timeline| {
247            timeline.play();
248            self.sync(timeline);
249        });
250    }
251
252    /// Pause playback.
253    pub fn pause(&self) {
254        crate::with_lock(&self.timeline, |timeline| {
255            timeline.pause();
256            self.sync(timeline);
257        });
258    }
259
260    /// Resume playback.
261    pub fn resume(&self) {
262        crate::with_lock(&self.timeline, |timeline| {
263            timeline.resume();
264            self.sync(timeline);
265        });
266    }
267
268    /// Reset to the beginning.
269    pub fn reset(&self) {
270        crate::with_lock(&self.timeline, |timeline| {
271            timeline.reset();
272            self.sync(timeline);
273        });
274    }
275
276    /// Seek by normalized progress.
277    pub fn seek(&self, progress: f32) {
278        crate::with_lock(&self.timeline, |timeline| {
279            timeline.seek(progress);
280            self.sync(timeline);
281        });
282    }
283
284    /// Change the time scale multiplier.
285    pub fn set_time_scale(&self, scale: f32) {
286        crate::with_lock(&self.timeline, |timeline| {
287            timeline.set_time_scale(scale);
288            self.sync(timeline);
289        });
290    }
291
292    /// Progress signal.
293    pub fn progress(&self) -> Signal<f32> {
294        self.progress
295    }
296
297    /// Completion signal.
298    pub fn is_complete(&self) -> Signal<bool> {
299        self.complete
300    }
301
302    /// Timeline state signal.
303    pub fn state(&self) -> Signal<TimelineState> {
304        self.state
305    }
306
307    /// Deterministically advance by `dt` seconds.
308    pub fn tick(&self, dt: f32) -> bool {
309        crate::with_lock(&self.timeline, |timeline| {
310            let running = timeline.update(dt.max(0.0));
311            self.sync(timeline);
312            running
313        })
314    }
315
316    fn sync(&self, timeline: &Timeline) {
317        crate::set_signal(self.progress, timeline.progress());
318        crate::set_signal(self.complete, timeline.is_complete());
319        crate::set_signal(self.state, timeline.state());
320    }
321}
322
323/// Create a signal-backed timeline hook.
324pub fn use_timeline(builder: impl FnOnce(Timeline) -> Timeline) -> TimelineHandle {
325    let mut timeline = builder(Timeline::new());
326    timeline.play();
327
328    let progress = use_signal({
329        let initial = timeline.progress();
330        move || initial
331    });
332    let complete = use_signal({
333        let initial = timeline.is_complete();
334        move || initial
335    });
336    let state = use_signal({
337        let initial = timeline.state();
338        move || initial
339    });
340    let handle = TimelineHandle {
341        timeline: Arc::new(Mutex::new(timeline)),
342        progress,
343        complete,
344        state,
345    };
346
347    let loop_handle = handle.clone();
348    crate::spawn_animation_loop(move |dt| {
349        loop_handle.tick(dt);
350        true
351    });
352
353    handle
354}
355
356/// Control handle for a signal-backed keyframe track.
357#[derive(Clone)]
358pub struct KeyframeHandle<T: Animatable + Send + Sync + 'static> {
359    track: Arc<Mutex<KeyframeTrack<T>>>,
360    value: Signal<T>,
361    progress: Signal<f32>,
362    complete: Signal<bool>,
363    paused: Arc<Mutex<bool>>,
364    time_scale: Arc<Mutex<f32>>,
365}
366
367impl<T: Animatable + Send + Sync + 'static> fmt::Debug for KeyframeHandle<T> {
368    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
369        f.debug_struct("KeyframeHandle").finish_non_exhaustive()
370    }
371}
372
373impl<T: Animatable + Send + Sync + 'static> KeyframeHandle<T> {
374    /// Start or resume playback.
375    pub fn play(&self) {
376        self.resume();
377    }
378
379    /// Pause playback.
380    pub fn pause(&self) {
381        crate::with_lock(&self.paused, |paused| *paused = true);
382    }
383
384    /// Resume playback.
385    pub fn resume(&self) {
386        crate::with_lock(&self.paused, |paused| *paused = false);
387    }
388
389    /// Reset to the beginning.
390    pub fn reset(&self) {
391        crate::with_lock(&self.track, |track| {
392            track.reset();
393            self.sync(track);
394        });
395    }
396
397    /// Change playback time scale.
398    pub fn set_time_scale(&self, scale: f32) {
399        crate::with_lock(&self.time_scale, |time_scale| {
400            *time_scale = crate::finite_or(scale, 1.0).max(0.0);
401        });
402    }
403
404    /// Current value signal.
405    pub fn value(&self) -> Signal<T> {
406        self.value
407    }
408
409    /// Progress signal.
410    pub fn progress(&self) -> Signal<f32> {
411        self.progress
412    }
413
414    /// Completion signal.
415    pub fn is_complete(&self) -> Signal<bool> {
416        self.complete
417    }
418
419    /// Deterministically advance by `dt` seconds.
420    pub fn tick(&self, dt: f32) -> bool {
421        let paused = crate::with_lock(&self.paused, |paused| *paused);
422        if paused {
423            return true;
424        }
425
426        let time_scale = crate::with_lock(&self.time_scale, |scale| *scale);
427        crate::with_lock(&self.track, |track| {
428            let running = track.update(dt.max(0.0) * time_scale);
429            self.sync(track);
430            running
431        })
432    }
433
434    fn sync(&self, track: &KeyframeTrack<T>) {
435        if let Some(value) = track.value() {
436            crate::set_signal(self.value, value);
437        }
438        crate::set_signal(self.progress, track.progress());
439        crate::set_signal(self.complete, track.is_complete());
440    }
441}
442
443/// Create a signal-backed keyframe track hook.
444///
445/// The builder must insert at least one keyframe. Empty tracks are ambiguous
446/// because there is no fallback value for the returned `Signal<T>`.
447pub fn use_keyframes<T>(
448    builder: impl FnOnce(KeyframeTrack<T>) -> KeyframeTrack<T>,
449) -> (Signal<T>, KeyframeHandle<T>)
450where
451    T: Animatable + Send + Sync + 'static,
452{
453    let track = builder(KeyframeTrack::new());
454    let initial = track
455        .value()
456        .expect("use_keyframes requires at least one keyframe");
457    let progress_value = track.progress();
458    let complete_value = track.is_complete();
459
460    let value = use_signal(move || initial);
461    let progress = use_signal(move || progress_value);
462    let complete = use_signal(move || complete_value);
463    let handle = KeyframeHandle {
464        track: Arc::new(Mutex::new(track)),
465        value,
466        progress,
467        complete,
468        paused: Arc::new(Mutex::new(false)),
469        time_scale: Arc::new(Mutex::new(1.0)),
470    };
471
472    let loop_handle = handle.clone();
473    crate::spawn_animation_loop(move |dt| {
474        loop_handle.tick(dt);
475        true
476    });
477
478    (value, handle)
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use animato_core::Easing;
485    use animato_timeline::{At, TimelineState};
486    use approx::assert_relative_eq;
487    use dioxus::prelude::*;
488    use std::cell::RefCell;
489
490    thread_local! {
491        static TWEEN_CAPTURE: RefCell<Option<(Signal<f32>, TweenHandle<f32>)>> = const { RefCell::new(None) };
492        static SPRING_CAPTURE: RefCell<Option<(Signal<f32>, SpringHandle<f32>)>> = const { RefCell::new(None) };
493        static TIMELINE_CAPTURE: RefCell<Option<TimelineHandle>> = const { RefCell::new(None) };
494        static KEYFRAME_CAPTURE: RefCell<Option<(Signal<f32>, KeyframeHandle<f32>)>> = const { RefCell::new(None) };
495    }
496
497    #[allow(non_snake_case)]
498    fn TweenHookApp() -> Element {
499        let pair = use_tween(0.0_f32, 10.0, |builder| {
500            builder.duration(1.0).easing(Easing::Linear)
501        });
502        TWEEN_CAPTURE.with(|slot| *slot.borrow_mut() = Some(pair));
503
504        rsx! { div {} }
505    }
506
507    #[allow(non_snake_case)]
508    fn SpringHookApp() -> Element {
509        let pair = use_spring(0.0_f32, SpringConfig::snappy());
510        SPRING_CAPTURE.with(|slot| *slot.borrow_mut() = Some(pair));
511
512        rsx! { div {} }
513    }
514
515    #[allow(non_snake_case)]
516    fn TimelineHookApp() -> Element {
517        let handle = use_timeline(|timeline| {
518            timeline.add(
519                "fade",
520                Tween::new(0.0_f32, 1.0)
521                    .duration(1.0)
522                    .easing(Easing::Linear)
523                    .build(),
524                At::Start,
525            )
526        });
527        TIMELINE_CAPTURE.with(|slot| *slot.borrow_mut() = Some(handle));
528
529        rsx! { div {} }
530    }
531
532    #[allow(non_snake_case)]
533    fn KeyframeHookApp() -> Element {
534        let pair = use_keyframes(|track| track.push(0.0, 0.0_f32).push(1.0, 10.0));
535        KEYFRAME_CAPTURE.with(|slot| *slot.borrow_mut() = Some(pair));
536
537        rsx! { div {} }
538    }
539
540    fn mount_tween() -> (VirtualDom, Signal<f32>, TweenHandle<f32>) {
541        TWEEN_CAPTURE.with(|slot| *slot.borrow_mut() = None);
542        let mut dom = VirtualDom::new(TweenHookApp);
543        dom.rebuild_in_place();
544        let (value, handle) = TWEEN_CAPTURE.with(|slot| {
545            slot.borrow()
546                .as_ref()
547                .cloned()
548                .expect("tween hook captured")
549        });
550        (dom, value, handle)
551    }
552
553    fn mount_spring() -> (VirtualDom, Signal<f32>, SpringHandle<f32>) {
554        SPRING_CAPTURE.with(|slot| *slot.borrow_mut() = None);
555        let mut dom = VirtualDom::new(SpringHookApp);
556        dom.rebuild_in_place();
557        let (value, handle) = SPRING_CAPTURE.with(|slot| {
558            slot.borrow()
559                .as_ref()
560                .cloned()
561                .expect("spring hook captured")
562        });
563        (dom, value, handle)
564    }
565
566    fn mount_timeline() -> (VirtualDom, TimelineHandle) {
567        TIMELINE_CAPTURE.with(|slot| *slot.borrow_mut() = None);
568        let mut dom = VirtualDom::new(TimelineHookApp);
569        dom.rebuild_in_place();
570        let handle = TIMELINE_CAPTURE.with(|slot| {
571            slot.borrow()
572                .as_ref()
573                .cloned()
574                .expect("timeline hook captured")
575        });
576        (dom, handle)
577    }
578
579    fn mount_keyframes() -> (VirtualDom, Signal<f32>, KeyframeHandle<f32>) {
580        KEYFRAME_CAPTURE.with(|slot| *slot.borrow_mut() = None);
581        let mut dom = VirtualDom::new(KeyframeHookApp);
582        dom.rebuild_in_place();
583        let (value, handle) = KEYFRAME_CAPTURE.with(|slot| {
584            slot.borrow()
585                .as_ref()
586                .cloned()
587                .expect("keyframe hook captured")
588        });
589        (dom, value, handle)
590    }
591
592    #[test]
593    fn tween_hook_handle_controls_signal_state() {
594        let (_dom, value, handle) = mount_tween();
595
596        assert_relative_eq!(crate::read_signal(value), 0.0);
597        assert_relative_eq!(crate::read_signal(handle.value()), 0.0);
598        assert_relative_eq!(crate::read_signal(handle.progress()), 0.0);
599        assert!(!crate::read_signal(handle.is_complete()));
600
601        assert!(handle.tick(0.25));
602        assert_relative_eq!(crate::read_signal(value), 2.5, epsilon = 0.001);
603        assert_relative_eq!(crate::read_signal(handle.progress()), 0.25, epsilon = 0.001);
604
605        handle.pause();
606        assert!(handle.tick(0.25));
607        assert_relative_eq!(crate::read_signal(value), 2.5, epsilon = 0.001);
608
609        handle.resume();
610        handle.set_time_scale(f32::NAN);
611        assert!(handle.tick(0.25));
612        assert_relative_eq!(crate::read_signal(value), 5.0, epsilon = 0.001);
613
614        handle.seek(1.0);
615        assert_relative_eq!(crate::read_signal(value), 10.0, epsilon = 0.001);
616        assert!(!handle.tick(0.0));
617        assert!(crate::read_signal(handle.is_complete()));
618        handle.play();
619        assert!(!crate::read_signal(handle.is_complete()));
620
621        handle.seek(0.25);
622        assert_relative_eq!(crate::read_signal(value), 2.5, epsilon = 0.001);
623        handle.reverse();
624        assert_relative_eq!(crate::read_signal(value), 2.5, epsilon = 0.001);
625        handle.reset();
626        assert_relative_eq!(crate::read_signal(value), 10.0, epsilon = 0.001);
627        assert_relative_eq!(crate::read_signal(handle.progress()), 0.0, epsilon = 0.001);
628    }
629
630    #[test]
631    fn spring_hook_handle_tracks_target_and_snap() {
632        let (_dom, value, handle) = mount_spring();
633
634        assert_relative_eq!(crate::read_signal(value), 0.0);
635        assert!(crate::read_signal(handle.is_settled()));
636
637        handle.set_target(1.0);
638        assert!(!crate::read_signal(handle.is_settled()));
639        assert!(handle.tick(1.0 / 60.0));
640        assert!(crate::read_signal(handle.value()) > 0.0);
641
642        handle.snap_to(2.0);
643        assert_relative_eq!(crate::read_signal(value), 2.0, epsilon = 0.001);
644        assert!(crate::read_signal(handle.is_settled()));
645        assert!(!handle.tick(0.0));
646    }
647
648    #[test]
649    fn timeline_hook_handle_controls_clock_state() {
650        let (_dom, handle) = mount_timeline();
651
652        assert_eq!(crate::read_signal(handle.state()), TimelineState::Playing);
653        assert_relative_eq!(crate::read_signal(handle.progress()), 0.0);
654        assert!(!crate::read_signal(handle.is_complete()));
655
656        assert!(handle.tick(0.25));
657        assert_relative_eq!(crate::read_signal(handle.progress()), 0.25, epsilon = 0.001);
658
659        handle.pause();
660        assert_eq!(crate::read_signal(handle.state()), TimelineState::Paused);
661        assert!(handle.tick(0.5));
662        assert_relative_eq!(crate::read_signal(handle.progress()), 0.25, epsilon = 0.001);
663
664        handle.resume();
665        handle.set_time_scale(2.0);
666        assert!(handle.tick(0.25));
667        assert_relative_eq!(crate::read_signal(handle.progress()), 0.75, epsilon = 0.001);
668
669        handle.seek(1.0);
670        assert!(crate::read_signal(handle.is_complete()));
671        assert_eq!(crate::read_signal(handle.state()), TimelineState::Completed);
672
673        handle.reset();
674        assert_relative_eq!(crate::read_signal(handle.progress()), 0.0, epsilon = 0.001);
675        assert_eq!(crate::read_signal(handle.state()), TimelineState::Idle);
676
677        handle.play();
678        assert_eq!(crate::read_signal(handle.state()), TimelineState::Playing);
679    }
680
681    #[test]
682    fn keyframe_hook_handle_controls_track_state() {
683        let (_dom, value, handle) = mount_keyframes();
684
685        assert_relative_eq!(crate::read_signal(value), 0.0);
686        assert_relative_eq!(crate::read_signal(handle.progress()), 0.0);
687        assert!(!crate::read_signal(handle.is_complete()));
688
689        assert!(handle.tick(0.25));
690        assert_relative_eq!(crate::read_signal(value), 2.5, epsilon = 0.001);
691
692        handle.pause();
693        assert!(handle.tick(0.25));
694        assert_relative_eq!(crate::read_signal(value), 2.5, epsilon = 0.001);
695
696        handle.play();
697        handle.set_time_scale(f32::INFINITY);
698        assert!(handle.tick(0.25));
699        assert_relative_eq!(crate::read_signal(value), 5.0, epsilon = 0.001);
700
701        handle.set_time_scale(2.0);
702        assert!(!handle.tick(0.25));
703        assert_relative_eq!(crate::read_signal(value), 10.0, epsilon = 0.001);
704        assert!(crate::read_signal(handle.is_complete()));
705
706        handle.reset();
707        assert_relative_eq!(crate::read_signal(value), 0.0, epsilon = 0.001);
708        assert!(!crate::read_signal(handle.is_complete()));
709    }
710}