use crate::animation::{Animation, AnimationInstance, Easing, FillMode};
use crate::hooks::context::{RenderCallback, current_context};
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
#[derive(Clone)]
pub struct TransitionHandle {
current: Arc<RwLock<f32>>,
target: Arc<RwLock<f32>>,
instance: Arc<RwLock<Option<AnimationInstance>>>,
duration: Duration,
easing: Easing,
last_tick: Arc<RwLock<Instant>>,
render_callback: Option<RenderCallback>,
}
impl TransitionHandle {
pub fn get(&self) -> f32 {
if let Some(ref instance) = *self.instance.read().unwrap() {
if instance.is_running() {
return instance.get();
}
}
*self.current.read().unwrap()
}
pub fn get_i32(&self) -> i32 {
self.get().round() as i32
}
pub fn get_usize(&self) -> usize {
self.get().round().max(0.0) as usize
}
pub fn target(&self) -> f32 {
*self.target.read().unwrap()
}
pub fn set(&self, value: f32) {
let current = self.get();
*self.target.write().unwrap() = value;
if (current - value).abs() < 0.001 {
*self.current.write().unwrap() = value;
*self.instance.write().unwrap() = None;
return;
}
let anim = Animation::new()
.from(current)
.to(value)
.duration(self.duration)
.easing(self.easing)
.fill_mode(FillMode::Forwards);
let mut instance = anim.start();
instance.play();
*self.instance.write().unwrap() = Some(instance);
*self.last_tick.write().unwrap() = Instant::now();
self.trigger_render();
}
pub fn set_immediate(&self, value: f32) {
*self.current.write().unwrap() = value;
*self.target.write().unwrap() = value;
*self.instance.write().unwrap() = None;
self.trigger_render();
}
pub fn is_transitioning(&self) -> bool {
self.instance
.read()
.unwrap()
.as_ref()
.is_some_and(|i| i.is_running())
}
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 mut instance_guard = self.instance.write().unwrap();
if let Some(ref mut instance) = *instance_guard {
let was_running = instance.is_running();
instance.tick(delta);
if instance.is_completed() {
*self.current.write().unwrap() = *self.target.read().unwrap();
*instance_guard = None;
} else if was_running && instance.is_running() {
drop(instance_guard);
self.trigger_render();
}
}
}
fn trigger_render(&self) {
if let Some(callback) = &self.render_callback {
callback();
}
}
}
impl std::fmt::Debug for TransitionHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TransitionHandle")
.field("current", &self.get())
.field("target", &self.target())
.field("transitioning", &self.is_transitioning())
.finish()
}
}
#[derive(Clone)]
struct TransitionStorage {
handle: TransitionHandle,
}
pub fn use_transition(initial: f32, duration: Duration) -> TransitionHandle {
use_transition_with_easing(initial, duration, Easing::EaseInOut)
}
pub fn use_transition_with_easing(
initial: f32,
duration: Duration,
easing: Easing,
) -> TransitionHandle {
let ctx = current_context().expect("use_transition must be called within a component");
let mut ctx_ref = ctx.write().unwrap();
let render_callback = ctx_ref.get_render_callback();
let storage = ctx_ref.use_hook(|| TransitionStorage {
handle: TransitionHandle {
current: Arc::new(RwLock::new(initial)),
target: Arc::new(RwLock::new(initial)),
instance: Arc::new(RwLock::new(None)),
duration,
easing,
last_tick: Arc::new(RwLock::new(Instant::now())),
render_callback: render_callback.clone(),
},
});
storage
.get::<TransitionStorage>()
.map(|s| s.handle)
.unwrap_or_else(|| TransitionHandle {
current: Arc::new(RwLock::new(initial)),
target: Arc::new(RwLock::new(initial)),
instance: Arc::new(RwLock::new(None)),
duration,
easing,
last_tick: Arc::new(RwLock::new(Instant::now())),
render_callback,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::animation::DurationExt;
use crate::hooks::context::{HookContext, with_hooks};
#[test]
fn test_transition_handle_basic() {
let handle = TransitionHandle {
current: Arc::new(RwLock::new(0.0)),
target: Arc::new(RwLock::new(0.0)),
instance: Arc::new(RwLock::new(None)),
duration: Duration::from_millis(100),
easing: Easing::Linear,
last_tick: Arc::new(RwLock::new(Instant::now())),
render_callback: None,
};
assert_eq!(handle.get(), 0.0);
assert!(!handle.is_transitioning());
}
#[test]
fn test_transition_set() {
let handle = TransitionHandle {
current: Arc::new(RwLock::new(0.0)),
target: Arc::new(RwLock::new(0.0)),
instance: Arc::new(RwLock::new(None)),
duration: Duration::from_millis(100),
easing: Easing::Linear,
last_tick: Arc::new(RwLock::new(Instant::now())),
render_callback: None,
};
handle.set(100.0);
assert!(handle.is_transitioning());
assert_eq!(handle.target(), 100.0);
}
#[test]
fn test_transition_immediate() {
let handle = TransitionHandle {
current: Arc::new(RwLock::new(0.0)),
target: Arc::new(RwLock::new(0.0)),
instance: Arc::new(RwLock::new(None)),
duration: Duration::from_millis(100),
easing: Easing::Linear,
last_tick: Arc::new(RwLock::new(Instant::now())),
render_callback: None,
};
handle.set_immediate(50.0);
assert!(!handle.is_transitioning());
assert_eq!(handle.get(), 50.0);
assert_eq!(handle.target(), 50.0);
}
#[test]
fn test_use_transition_in_context() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
let handle = with_hooks(ctx.clone(), || use_transition(0.0, 100.ms()));
assert_eq!(handle.get(), 0.0);
handle.set(100.0);
assert!(handle.is_transitioning());
}
#[test]
fn test_transition_persistence() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
let handle1 = with_hooks(ctx.clone(), || use_transition(0.0, 100.ms()));
handle1.set(50.0);
let handle2 = with_hooks(ctx.clone(), || use_transition(999.0, 999.ms()));
assert_eq!(handle2.target(), 50.0);
}
#[test]
fn test_transition_no_change() {
let handle = TransitionHandle {
current: Arc::new(RwLock::new(50.0)),
target: Arc::new(RwLock::new(50.0)),
instance: Arc::new(RwLock::new(None)),
duration: Duration::from_millis(100),
easing: Easing::Linear,
last_tick: Arc::new(RwLock::new(Instant::now())),
render_callback: None,
};
handle.set(50.0);
assert!(!handle.is_transitioning());
}
}