use crate::animation::{Animation, AnimationInstance, AnimationState};
use crate::hooks::context::{RenderCallback, current_context};
use std::sync::{Arc, RwLock};
use std::time::Instant;
#[derive(Clone)]
pub struct AnimationHandle {
instance: Arc<RwLock<AnimationInstance>>,
last_tick: Arc<RwLock<Instant>>,
render_callback: Option<RenderCallback>,
}
impl AnimationHandle {
pub fn get(&self) -> f32 {
self.instance.read().unwrap().get()
}
pub fn get_i32(&self) -> i32 {
self.instance.read().unwrap().get_i32()
}
pub fn get_usize(&self) -> usize {
self.instance.read().unwrap().get_usize()
}
pub fn play(&self) {
self.instance.write().unwrap().play();
self.trigger_render();
}
pub fn pause(&self) {
self.instance.write().unwrap().pause();
}
pub fn reset(&self) {
self.instance.write().unwrap().reset();
self.trigger_render();
}
pub fn is_running(&self) -> bool {
self.instance.read().unwrap().is_running()
}
pub fn is_completed(&self) -> bool {
self.instance.read().unwrap().is_completed()
}
pub fn state(&self) -> AnimationState {
self.instance.read().unwrap().state()
}
pub fn progress(&self) -> f32 {
self.instance.read().unwrap().progress()
}
pub fn tick(&self) {
let now = Instant::now();
let delta = {
let mut last = self.last_tick.write().unwrap();
let delta = now.duration_since(*last);
*last = now;
delta
};
let was_running = self.is_running();
self.instance.write().unwrap().tick(delta);
if was_running && self.is_running() {
self.trigger_render();
}
}
fn trigger_render(&self) {
if let Some(callback) = &self.render_callback {
callback();
}
}
pub fn try_get(&self) -> Option<f32> {
self.instance.read().ok().map(|g| g.get())
}
pub fn try_get_i32(&self) -> Option<i32> {
self.instance.read().ok().map(|g| g.get_i32())
}
pub fn try_get_usize(&self) -> Option<usize> {
self.instance.read().ok().map(|g| g.get_usize())
}
pub fn try_play(&self) -> bool {
if let Ok(mut guard) = self.instance.write() {
guard.play();
self.trigger_render();
true
} else {
false
}
}
pub fn try_pause(&self) -> bool {
if let Ok(mut guard) = self.instance.write() {
guard.pause();
true
} else {
false
}
}
pub fn try_reset(&self) -> bool {
if let Ok(mut guard) = self.instance.write() {
guard.reset();
self.trigger_render();
true
} else {
false
}
}
pub fn try_is_running(&self) -> Option<bool> {
self.instance.read().ok().map(|g| g.is_running())
}
pub fn try_is_completed(&self) -> Option<bool> {
self.instance.read().ok().map(|g| g.is_completed())
}
pub fn try_state(&self) -> Option<AnimationState> {
self.instance.read().ok().map(|g| g.state())
}
pub fn try_progress(&self) -> Option<f32> {
self.instance.read().ok().map(|g| g.progress())
}
}
impl std::fmt::Debug for AnimationHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let instance = self.instance.read().unwrap();
f.debug_struct("AnimationHandle")
.field("value", &instance.get())
.field("state", &instance.state())
.finish()
}
}
#[derive(Clone)]
struct AnimationStorage {
handle: AnimationHandle,
}
fn new_animation_handle(
animation: Animation,
render_callback: Option<RenderCallback>,
) -> AnimationHandle {
let instance = animation.start();
AnimationHandle {
instance: Arc::new(RwLock::new(instance)),
last_tick: Arc::new(RwLock::new(Instant::now())),
render_callback,
}
}
pub fn use_animation(init: impl FnOnce() -> Animation) -> AnimationHandle {
let animation = init();
let Some(ctx) = current_context() else {
return new_animation_handle(animation, None);
};
let Ok(mut ctx_ref) = ctx.write() else {
return new_animation_handle(animation, None);
};
let render_callback = ctx_ref.get_render_callback();
let animation_for_hook = animation.clone();
let storage = ctx_ref.use_hook(|| {
AnimationStorage {
handle: new_animation_handle(animation_for_hook, render_callback.clone()),
}
});
storage
.get::<AnimationStorage>()
.map(|s| s.handle)
.unwrap_or_else(|| new_animation_handle(animation, render_callback))
}
pub fn use_animation_auto(init: impl FnOnce() -> Animation) -> AnimationHandle {
let handle = use_animation(init);
if handle.state() == AnimationState::Idle {
handle.play();
}
handle
}
#[cfg(test)]
mod tests {
use super::*;
use crate::animation::{DurationExt, Easing};
use crate::hooks::context::{HookContext, with_hooks};
#[test]
fn test_animation_handle_basic() {
let anim = Animation::new()
.from(0.0)
.to(100.0)
.duration(100.ms())
.easing(Easing::Linear);
let handle = AnimationHandle {
instance: Arc::new(RwLock::new(anim.start())),
last_tick: Arc::new(RwLock::new(Instant::now())),
render_callback: None,
};
assert_eq!(handle.state(), AnimationState::Idle);
assert_eq!(handle.get(), 0.0);
handle.play();
assert!(handle.is_running());
}
#[test]
fn test_use_animation_in_context() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
let handle = with_hooks(ctx.clone(), || {
use_animation(|| Animation::new().from(0.0).to(100.0).duration(100.ms()))
});
assert_eq!(handle.state(), AnimationState::Idle);
handle.play();
assert!(handle.is_running());
}
#[test]
fn test_animation_persistence() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
let handle1 = with_hooks(ctx.clone(), || {
use_animation(|| Animation::new().from(0.0).to(100.0).duration(100.ms()))
});
handle1.play();
handle1.instance.write().unwrap().tick(50.ms());
let handle2 = with_hooks(ctx.clone(), || {
use_animation(|| Animation::new().from(999.0).to(999.0).duration(999.ms()))
});
assert!(handle2.is_running());
assert!(handle2.get() > 0.0);
}
#[test]
fn test_use_animation_without_context_does_not_panic() {
let handle = use_animation(|| Animation::new().from(0.0).to(10.0).duration(100.ms()));
assert_eq!(handle.state(), AnimationState::Idle);
handle.play();
assert!(handle.is_running());
}
}