dioxus_motion/animations/
platform.rs1use instant::{Duration, Instant};
7use std::future::Future;
8
9#[cfg(feature = "web")]
10use crate::animations::closure_pool::{create_pooled_closure, register_pooled_callback};
11
12pub trait TimeProvider {
17 fn now() -> Instant;
19
20 fn delay(duration: Duration) -> impl Future<Output = ()>;
22}
23
24#[derive(Debug, Clone, Copy)]
30pub struct MotionTime;
31
32impl TimeProvider for MotionTime {
33 fn now() -> Instant {
34 Instant::now()
35 }
36
37 #[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 if _duration.as_millis() <= RAF_THRESHOLD_MS as u128 {
58 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 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 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 const MIN_SPIN_THRESHOLD: Duration = Duration::from_millis(1);
103
104 if duration > MIN_SPIN_THRESHOLD {
105 let start = Instant::now();
106
107 tokio::time::sleep(duration).await;
109
110 let remaining = duration.saturating_sub(start.elapsed());
112 if remaining > Duration::from_micros(100) {
113 spin_sleep::sleep(remaining);
114 }
115 } else {
116 tokio::task::yield_now().await;
119 }
120 })
121 }
122}
123
124pub type Time = MotionTime;
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn test_time_provider_now() {
133 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 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 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 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 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 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 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}