use instant::{Duration, Instant};
use std::future::Future;
#[cfg(feature = "web")]
use crate::animations::closure_pool::{create_pooled_closure, register_pooled_callback};
pub trait TimeProvider {
fn now() -> Instant;
fn delay(duration: Duration) -> impl Future<Output = ()>;
}
#[derive(Debug, Clone, Copy)]
pub struct MotionTime;
impl TimeProvider for MotionTime {
fn now() -> Instant {
Instant::now()
}
#[cfg(feature = "web")]
fn delay(_duration: Duration) -> impl Future<Output = ()> {
use futures_util::FutureExt;
use wasm_bindgen::prelude::*;
use web_sys::window;
const RAF_THRESHOLD_MS: u8 = 16;
let (sender, receiver) = futures_channel::oneshot::channel::<()>();
if let Some(window) = window() {
if _duration.as_millis() <= RAF_THRESHOLD_MS as u128 {
let callback_id = register_pooled_callback(Box::new(move || {
let _ = sender.send(());
}));
let cb = create_pooled_closure(callback_id);
window
.request_animation_frame(cb.as_ref().unchecked_ref())
.expect("Failed to request animation frame");
cb.forget();
} else {
let callback_id = register_pooled_callback(Box::new(move || {
let _ = sender.send(());
}));
let cb = create_pooled_closure(callback_id);
window
.set_timeout_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
_duration.as_millis() as i32,
)
.expect("Failed to set timeout");
cb.forget();
}
} else {
let _ = sender.send(());
}
receiver.map(|_| ())
}
#[cfg(not(feature = "web"))]
fn delay(duration: Duration) -> impl Future<Output = ()> {
Box::pin(async move {
const MIN_SPIN_THRESHOLD: Duration = Duration::from_millis(1);
if duration >= MIN_SPIN_THRESHOLD {
let start = Instant::now();
tokio::time::sleep(duration).await;
let remaining = duration.saturating_sub(start.elapsed());
if remaining > Duration::from_micros(100) {
spin_sleep::sleep(remaining);
}
} else {
tokio::task::yield_now().await;
}
})
}
}
pub type Time = MotionTime;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_time_provider_now() {
let time1 = MotionTime::now();
std::thread::sleep(Duration::from_millis(1));
let time2 = MotionTime::now();
assert!(time2 > time1, "Time should advance");
assert!(
time2.duration_since(time1) >= Duration::from_millis(1),
"Time difference should be at least 1ms"
);
}
#[cfg(not(feature = "web"))]
#[tokio::test]
async fn test_desktop_sleep_threshold_optimization() {
let short_duration = Duration::from_micros(500);
let start = Instant::now();
MotionTime::delay(short_duration).await;
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_millis(2),
"Short duration sleep took too long: {:?}",
elapsed
);
}
#[cfg(not(feature = "web"))]
#[tokio::test]
async fn test_desktop_sleep_longer_duration() {
let long_duration = Duration::from_millis(10);
let start = Instant::now();
MotionTime::delay(long_duration).await;
let elapsed = start.elapsed();
assert!(
elapsed >= Duration::from_millis(8),
"Long duration sleep was too short: {:?}",
elapsed
);
assert!(
elapsed <= Duration::from_millis(15),
"Long duration sleep was too long: {:?}",
elapsed
);
}
#[cfg(not(feature = "web"))]
#[tokio::test]
async fn test_desktop_sleep_threshold_boundary() {
let threshold_duration = Duration::from_millis(1);
let start = Instant::now();
MotionTime::delay(threshold_duration).await;
let elapsed = start.elapsed();
assert!(
elapsed >= Duration::from_micros(800),
"Threshold duration sleep was too short: {:?}",
elapsed
);
assert!(
elapsed <= Duration::from_millis(3),
"Threshold duration sleep was too long: {:?}",
elapsed
);
}
}