Skip to main content

animato_dioxus/
lib.rs

1//! # animato-dioxus
2//!
3//! First-class Dioxus integration for Animato.
4//!
5//! This crate exposes Dioxus `Signal`-backed hooks, style helpers,
6//! presence/list/transition components, gesture bindings, platform detection,
7//! and portable native-window animation handles. The animation engines remain
8//! the renderer-agnostic Animato tween, spring, timeline, keyframe, and physics
9//! types; Dioxus only owns reactive state and component rendering.
10
11#![deny(missing_docs)]
12#![deny(missing_debug_implementations)]
13
14use dioxus::prelude::{ReadableExt, WritableExt};
15
16#[cfg(feature = "css")]
17mod css;
18#[cfg(feature = "gesture")]
19mod gesture;
20mod hooks;
21#[cfg(feature = "list")]
22mod list;
23#[cfg(feature = "motion")]
24mod motion;
25#[cfg(feature = "native")]
26mod native;
27#[cfg(feature = "platform")]
28mod platform;
29#[cfg(feature = "presence")]
30mod presence;
31#[cfg(feature = "scroll")]
32mod scroll;
33#[cfg(feature = "transition")]
34mod transition;
35
36#[cfg(feature = "css")]
37pub use css::{AnimatedStyle, css_spring, css_tween};
38#[cfg(feature = "gesture")]
39pub use gesture::{
40    DragAxis, DragConfig, DragConstraints, DragHandle, Gesture, GestureConfig, PinchHandle,
41    SwipeConfig, SwipeDirection, SwipeEvent, use_drag, use_gesture, use_pinch, use_swipe,
42};
43pub use hooks::{
44    KeyframeHandle, SpringHandle, TimelineHandle, TweenHandle, use_keyframes, use_spring,
45    use_timeline, use_tween,
46};
47#[cfg(feature = "list")]
48pub use list::{AnimatedFor, stable_key};
49#[cfg(feature = "motion")]
50pub use motion::{MotionConfig, MotionHandle, use_motion};
51#[cfg(feature = "native")]
52pub use native::{
53    NativeWindowState, WindowAnimationHandle, WindowSpringHandle, use_window_animation,
54    use_window_spring,
55};
56#[cfg(feature = "platform")]
57pub use platform::{AnimationBackend, PlatformAdapter};
58#[cfg(feature = "presence")]
59pub use presence::{AnimatePresence, PresenceAnimation};
60#[cfg(feature = "scroll")]
61pub use scroll::{
62    ScrollAxis, ScrollConfig, ScrollProgressCalculator, ScrollTriggerConfig, ScrollTriggerHandle,
63    use_scroll_progress, use_scroll_trigger, use_scroll_velocity,
64};
65#[cfg(feature = "transition")]
66pub use transition::{PageTransition, TransitionMode, route_transition_key};
67
68pub(crate) fn finite_or(value: f32, fallback: f32) -> f32 {
69    if value.is_finite() { value } else { fallback }
70}
71
72pub(crate) fn set_signal<T: 'static>(signal: dioxus::prelude::Signal<T>, value: T) {
73    let mut signal = signal;
74    signal.set(value);
75}
76
77#[allow(dead_code)]
78pub(crate) fn read_signal<T: Clone + 'static>(signal: dioxus::prelude::Signal<T>) -> T {
79    signal.read().clone()
80}
81
82pub(crate) fn with_lock<T, R>(
83    value: &std::sync::Arc<std::sync::Mutex<T>>,
84    f: impl FnOnce(&mut T) -> R,
85) -> R {
86    let mut guard = value
87        .lock()
88        .unwrap_or_else(|poisoned| poisoned.into_inner());
89    f(&mut guard)
90}
91
92pub(crate) fn spawn_animation_loop(tick: impl FnMut(f32) -> bool + 'static) {
93    #[cfg(all(target_arch = "wasm32", feature = "web"))]
94    {
95        spawn_raf_loop(tick);
96        return;
97    }
98
99    #[cfg(not(target_arch = "wasm32"))]
100    {
101        use dioxus::prelude::use_future;
102        use std::time::{Duration, Instant};
103
104        let mut tick = Some(tick);
105        use_future(move || {
106            let mut tick = tick.take();
107            async move {
108                let Some(mut tick) = tick.take() else {
109                    return;
110                };
111                let mut last = Instant::now();
112                loop {
113                    let now = Instant::now();
114                    let dt = now.duration_since(last).as_secs_f32().min(0.25);
115                    last = now;
116                    if !tick(dt) {
117                        break;
118                    }
119                    std::thread::sleep(Duration::from_millis(16));
120                }
121            }
122        });
123    }
124
125    #[cfg(all(target_arch = "wasm32", not(feature = "web")))]
126    {
127        let _ = tick;
128    }
129}
130
131#[cfg(all(target_arch = "wasm32", feature = "web"))]
132fn spawn_raf_loop(tick: impl FnMut(f32) -> bool + 'static) {
133    use std::cell::{Cell, RefCell};
134    use std::rc::Rc;
135    use wasm_bindgen::JsCast;
136    use wasm_bindgen::closure::Closure;
137
138    let Some(window) = web_sys::window() else {
139        return;
140    };
141
142    let tick = Rc::new(RefCell::new(Box::new(tick) as Box<dyn FnMut(f32) -> bool>));
143    let last_timestamp = Rc::new(Cell::new(None::<f64>));
144    let callback: Rc<RefCell<Option<Closure<dyn FnMut()>>>> = Rc::new(RefCell::new(None));
145    let callback_ref = Rc::clone(&callback);
146    let tick_ref = Rc::clone(&tick);
147    let last_ref = Rc::clone(&last_timestamp);
148    let window_ref = window.clone();
149
150    *callback.borrow_mut() = Some(Closure::wrap(Box::new(move || {
151        let now = window_ref
152            .performance()
153            .map(|performance| performance.now())
154            .unwrap_or(0.0);
155        let dt = last_ref
156            .replace(Some(now))
157            .map(|last| ((now - last) / 1000.0).max(0.0) as f32)
158            .unwrap_or(0.0)
159            .min(0.25);
160
161        if (tick_ref.borrow_mut())(dt)
162            && let Some(callback) = callback_ref.borrow().as_ref()
163        {
164            let _ = window_ref.request_animation_frame(callback.as_ref().unchecked_ref());
165        }
166    }) as Box<dyn FnMut()>));
167
168    if let Some(callback) = callback.borrow().as_ref() {
169        let _ = window.request_animation_frame(callback.as_ref().unchecked_ref());
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use dioxus::prelude::*;
177    use std::cell::RefCell;
178    use std::sync::{Arc, Mutex};
179
180    thread_local! {
181        static SIGNAL_CAPTURE: RefCell<Option<Signal<i32>>> = const { RefCell::new(None) };
182    }
183
184    #[allow(non_snake_case)]
185    fn SignalHelperApp() -> Element {
186        let signal = use_signal(|| 1_i32);
187        SIGNAL_CAPTURE.with(|slot| *slot.borrow_mut() = Some(signal));
188
189        rsx! { div {} }
190    }
191
192    #[test]
193    fn finite_or_replaces_non_finite_values() {
194        assert_eq!(finite_or(2.0, 1.0), 2.0);
195        assert_eq!(finite_or(f32::NAN, 1.0), 1.0);
196        assert_eq!(finite_or(f32::INFINITY, 1.0), 1.0);
197    }
198
199    #[test]
200    fn signal_helpers_read_and_write_values() {
201        SIGNAL_CAPTURE.with(|slot| *slot.borrow_mut() = None);
202        let mut dom = VirtualDom::new(SignalHelperApp);
203        dom.rebuild_in_place();
204        let signal =
205            SIGNAL_CAPTURE.with(|slot| slot.borrow().as_ref().copied().expect("signal captured"));
206
207        assert_eq!(read_signal(signal), 1);
208        set_signal(signal, 4);
209        assert_eq!(read_signal(signal), 4);
210    }
211
212    #[test]
213    fn with_lock_updates_inner_value() {
214        let value = Arc::new(Mutex::new(1_i32));
215        let updated = with_lock(&value, |inner| {
216            *inner += 2;
217            *inner
218        });
219
220        assert_eq!(updated, 3);
221        assert_eq!(*value.lock().expect("lock"), 3);
222    }
223}