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();
}
}
pub fn try_get(&self) -> Option<f32> {
let instance_guard = self.instance.read().ok()?;
if let Some(ref instance) = *instance_guard {
if instance.is_running() {
return Some(instance.get());
}
}
self.current.read().ok().map(|g| *g)
}
pub fn try_get_i32(&self) -> Option<i32> {
self.try_get().map(|v| v.round() as i32)
}
pub fn try_get_usize(&self) -> Option<usize> {
self.try_get().map(|v| v.round().max(0.0) as usize)
}
pub fn try_target(&self) -> Option<f32> {
self.target.read().ok().map(|g| *g)
}
pub fn try_set(&self, value: f32) -> bool {
let current = match self.try_get() {
Some(v) => v,
None => return false,
};
if self.target.write().ok().map(|mut g| *g = value).is_none() {
return false;
}
if (current - value).abs() < 0.001 {
if self.current.write().ok().map(|mut g| *g = value).is_none() {
return false;
}
if self.instance.write().ok().map(|mut g| *g = None).is_none() {
return false;
}
return true;
}
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();
if self
.instance
.write()
.ok()
.map(|mut g| *g = Some(instance))
.is_none()
{
return false;
}
if self
.last_tick
.write()
.ok()
.map(|mut g| *g = Instant::now())
.is_none()
{
return false;
}
self.trigger_render();
true
}
pub fn try_set_immediate(&self, value: f32) -> bool {
if self.current.write().ok().map(|mut g| *g = value).is_none() {
return false;
}
if self.target.write().ok().map(|mut g| *g = value).is_none() {
return false;
}
if self.instance.write().ok().map(|mut g| *g = None).is_none() {
return false;
}
self.trigger_render();
true
}
pub fn try_is_transitioning(&self) -> Option<bool> {
self.instance
.read()
.ok()
.map(|g| g.as_ref().is_some_and(|i| i.is_running()))
}
}
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,
}
fn new_transition_handle(
initial: f32,
duration: Duration,
easing: Easing,
render_callback: Option<RenderCallback>,
) -> TransitionHandle {
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,
}
}
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 Some(ctx) = current_context() else {
return new_transition_handle(initial, duration, easing, None);
};
let Ok(mut ctx_ref) = ctx.write() else {
return new_transition_handle(initial, duration, easing, None);
};
let render_callback = ctx_ref.get_render_callback();
let storage = ctx_ref.use_hook(|| TransitionStorage {
handle: new_transition_handle(initial, duration, easing, render_callback.clone()),
});
storage
.get::<TransitionStorage>()
.map(|s| s.handle)
.unwrap_or_else(|| new_transition_handle(initial, duration, easing, 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());
}
#[test]
fn test_use_transition_without_context_does_not_panic() {
let handle = use_transition(0.0, 100.ms());
assert_eq!(handle.get(), 0.0);
handle.set(100.0);
assert!(handle.is_transitioning());
assert_eq!(handle.target(), 100.0);
}
}