dioxus_motion/animations/
platform.rs

1//! Platform abstraction for time-related functionality
2//!
3//! Provides cross-platform timing operations for animations.
4//! Supports both web (WASM) and native platforms.
5
6use instant::{Duration, Instant};
7use std::future::Future;
8
9#[cfg(feature = "web")]
10use crate::animations::closure_pool::{create_pooled_closure, register_pooled_callback};
11
12/// Provides platform-agnostic timing operations
13///
14/// Abstracts timing functionality across different platforms,
15/// ensuring consistent animation behavior in both web and native environments.
16pub trait TimeProvider {
17    /// Returns the current instant
18    fn now() -> Instant;
19
20    /// Creates a future that completes after the specified duration
21    fn delay(duration: Duration) -> impl Future<Output = ()>;
22}
23
24/// Default time provider implementation for motion animations
25///
26/// Implements platform-specific timing operations:
27/// - For web: Uses requestAnimationFrame or setTimeout
28/// - For native: Uses tokio's sleep
29#[derive(Debug, Clone, Copy)]
30pub struct MotionTime;
31
32impl TimeProvider for MotionTime {
33    fn now() -> Instant {
34        Instant::now()
35    }
36
37    /// Creates a delay future using platform-specific implementations
38    ///
39    /// # Web
40    /// Uses requestAnimationFrame for short delays (<16ms)
41    /// Uses setTimeout for longer delays
42    ///
43    /// # Native
44    /// Uses tokio::time::sleep
45    #[cfg(feature = "web")]
46    fn delay(_duration: Duration) -> impl Future<Output = ()> {
47        use futures_util::FutureExt;
48        use wasm_bindgen::prelude::*;
49        use web_sys::window;
50
51        const RAF_THRESHOLD_MS: u8 = 16;
52
53        let (sender, receiver) = futures_channel::oneshot::channel::<()>();
54
55        if let Some(window) = window() {
56            // Choose timing method based on duration
57            if _duration.as_millis() <= RAF_THRESHOLD_MS as u128 {
58                // For frame-based timing, use requestAnimationFrame
59                // This is ideal for animation frames (typically 16ms at 60fps)
60
61                // Use pooled closure for better performance
62                let callback_id = register_pooled_callback(Box::new(move || {
63                    let _ = sender.send(());
64                }));
65                let cb = create_pooled_closure(callback_id);
66
67                window
68                    .request_animation_frame(cb.as_ref().unchecked_ref())
69                    .expect("Failed to request animation frame");
70
71                cb.forget();
72            } else {
73                // For longer delays, use setTimeout which is more appropriate
74
75                // Use pooled closure for better performance
76                let callback_id = register_pooled_callback(Box::new(move || {
77                    let _ = sender.send(());
78                }));
79                let cb = create_pooled_closure(callback_id);
80
81                window
82                    .set_timeout_with_callback_and_timeout_and_arguments_0(
83                        cb.as_ref().unchecked_ref(),
84                        _duration.as_millis() as i32,
85                    )
86                    .expect("Failed to set timeout");
87
88                cb.forget();
89            }
90        } else {
91            // Fallback: complete immediately if no window
92            let _ = sender.send(());
93        }
94
95        receiver.map(|_| ())
96    }
97
98    #[cfg(not(feature = "web"))]
99    fn delay(duration: Duration) -> impl Future<Output = ()> {
100        Box::pin(async move {
101            // Threshold-based sleep optimization
102            const MIN_SPIN_THRESHOLD: Duration = Duration::from_millis(1);
103
104            if duration > MIN_SPIN_THRESHOLD {
105                let start = Instant::now();
106
107                // Use tokio sleep for longer durations
108                tokio::time::sleep(duration).await;
109
110                // High precision timing for desktop - only for remaining time
111                let remaining = duration.saturating_sub(start.elapsed());
112                if remaining > Duration::from_micros(100) {
113                    spin_sleep::sleep(remaining);
114                }
115            } else {
116                // For very short durations, skip sleep entirely to avoid CPU waste
117                // This prevents unnecessary context switching for sub-millisecond delays
118                tokio::task::yield_now().await;
119            }
120        })
121    }
122}
123
124/// Type alias for the default time provider
125pub type Time = MotionTime;
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_time_provider_now() {
133        // Test that TimeProvider::now() works consistently
134        let time1 = MotionTime::now();
135        std::thread::sleep(Duration::from_millis(1));
136        let time2 = MotionTime::now();
137
138        assert!(time2 > time1, "Time should advance");
139        assert!(
140            time2.duration_since(time1) >= Duration::from_millis(1),
141            "Time difference should be at least 1ms"
142        );
143    }
144
145    #[cfg(not(feature = "web"))]
146    #[tokio::test]
147    async fn test_desktop_sleep_threshold_optimization() {
148        // Test that very short durations don't use spin sleep
149        let short_duration = Duration::from_micros(500);
150        let start = Instant::now();
151
152        MotionTime::delay(short_duration).await;
153
154        let elapsed = start.elapsed();
155
156        // For very short durations, we should yield instead of sleep
157        // The elapsed time should be minimal (less than 2ms)
158        assert!(
159            elapsed < Duration::from_millis(2),
160            "Short duration sleep took too long: {:?}",
161            elapsed
162        );
163    }
164
165    #[cfg(not(feature = "web"))]
166    #[tokio::test]
167    async fn test_desktop_sleep_longer_duration() {
168        // Test that longer durations use proper sleep
169        let long_duration = Duration::from_millis(10);
170        let start = Instant::now();
171
172        MotionTime::delay(long_duration).await;
173
174        let elapsed = start.elapsed();
175
176        // For longer durations, we should sleep properly
177        // Allow some tolerance for timing variations
178        assert!(
179            elapsed >= Duration::from_millis(8),
180            "Long duration sleep was too short: {:?}",
181            elapsed
182        );
183        assert!(
184            elapsed <= Duration::from_millis(15),
185            "Long duration sleep was too long: {:?}",
186            elapsed
187        );
188    }
189
190    #[cfg(not(feature = "web"))]
191    #[tokio::test]
192    async fn test_desktop_sleep_threshold_boundary() {
193        // Test the 1ms threshold boundary
194        let threshold_duration = Duration::from_millis(1);
195        let start = Instant::now();
196
197        MotionTime::delay(threshold_duration).await;
198
199        let elapsed = start.elapsed();
200
201        // At the threshold, we should still use proper sleep
202        assert!(
203            elapsed >= Duration::from_micros(800),
204            "Threshold duration sleep was too short: {:?}",
205            elapsed
206        );
207        assert!(
208            elapsed <= Duration::from_millis(3),
209            "Threshold duration sleep was too long: {:?}",
210            elapsed
211        );
212    }
213}