use std::cell::{Cell, RefCell};
use std::rc::{Rc, Weak};
use std::time::{Duration, Instant};
use crate::runtime::signal::Signal;
thread_local! {
static FRAME_SIGNAL: RefCell<Option<Signal<u32>>> = const { RefCell::new(None) };
static PENDING: Cell<bool> = const { Cell::new(false) };
static SHARED_REGISTRY: RefCell<Option<AnimRegistry>> = const { RefCell::new(None) };
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Easing {
#[default]
Linear,
EaseIn,
EaseOut,
EaseInOut,
}
impl Easing {
pub fn sample(self, progress: f32) -> f32 {
let t = progress.clamp(0.0, 1.0);
match self {
Self::Linear => t,
Self::EaseIn => t * t,
Self::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
Self::EaseInOut => {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - ((-2.0 * t + 2.0).powi(2) / 2.0)
}
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AnimationConfig {
duration: Duration,
easing: Easing,
}
impl AnimationConfig {
pub fn new(duration: Duration) -> Self {
Self {
duration,
easing: Easing::Linear,
}
}
pub fn easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
pub fn duration(&self) -> Duration {
self.duration
}
pub fn easing_curve(&self) -> Easing {
self.easing
}
}
impl Default for AnimationConfig {
fn default() -> Self {
Self::new(Duration::from_millis(150))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AnimSlot {
config: AnimationConfig,
raw_progress: f32,
started_at: Option<Instant>,
}
impl AnimSlot {
pub fn new(config: AnimationConfig) -> Self {
Self {
config,
raw_progress: 0.0,
started_at: None,
}
}
pub fn config(&self) -> AnimationConfig {
self.config
}
pub fn raw_progress(&self) -> f32 {
self.raw_progress
}
pub fn is_playing(&self) -> bool {
self.started_at.is_some()
}
fn update(&mut self, now: Instant) {
let Some(started_at) = self.started_at else {
return;
};
let next = if self.config.duration.is_zero() {
1.0
} else {
now.saturating_duration_since(started_at).as_secs_f32()
/ self.config.duration.as_secs_f32()
};
self.raw_progress = next.clamp(0.0, 1.0);
if self.raw_progress >= 1.0 {
self.started_at = None;
}
}
fn eased_progress(&self) -> f32 {
self.config.easing.sample(self.raw_progress)
}
}
#[derive(Clone, Debug)]
pub struct AnimationHandle {
slot: Rc<RefCell<AnimSlot>>,
active_slots: Rc<RefCell<Vec<Weak<RefCell<AnimSlot>>>>>,
}
impl AnimationHandle {
fn mark_active(&self) {
let weak = Rc::downgrade(&self.slot);
let mut active = self.active_slots.borrow_mut();
if !active.iter().any(|slot| slot.ptr_eq(&weak)) {
active.push(weak);
}
}
pub fn progress(&self) -> f32 {
let now = Instant::now();
let (progress, playing) = {
let mut slot = self.slot.borrow_mut();
slot.update(now);
(slot.eased_progress(), slot.is_playing())
};
if playing {
animation_frame_signal().get();
request_animation_frame();
}
progress
}
pub fn play(&self) {
let now = Instant::now();
let mut slot = self.slot.borrow_mut();
slot.update(now);
if slot.raw_progress >= 1.0 {
slot.raw_progress = 0.0;
}
let elapsed = slot.config.duration.mul_f32(slot.raw_progress);
slot.started_at = Some(now.checked_sub(elapsed).unwrap_or(now));
drop(slot);
self.mark_active();
request_animation_frame();
}
pub fn reset(&self) {
let mut slot = self.slot.borrow_mut();
slot.raw_progress = 0.0;
slot.started_at = None;
drop(slot);
self.active_slots.borrow_mut().retain(|active| {
active
.upgrade()
.is_some_and(|slot| !Rc::ptr_eq(&slot, &self.slot))
});
}
pub fn is_playing(&self) -> bool {
let mut slot = self.slot.borrow_mut();
slot.update(Instant::now());
slot.is_playing()
}
pub fn raw_progress(&self) -> f32 {
let mut slot = self.slot.borrow_mut();
slot.update(Instant::now());
slot.raw_progress()
}
}
#[derive(Clone, Debug, Default)]
pub struct AnimRegistry {
slots: Rc<RefCell<Vec<Weak<RefCell<AnimSlot>>>>>,
active_slots: Rc<RefCell<Vec<Weak<RefCell<AnimSlot>>>>>,
}
impl AnimRegistry {
pub fn shared() -> Self {
SHARED_REGISTRY.with(|cell| {
let mut registry = cell.borrow_mut();
registry.get_or_insert_with(Self::default).clone()
})
}
pub fn register(&self, slot: AnimSlot) -> AnimationHandle {
let slot = Rc::new(RefCell::new(slot));
self.slots.borrow_mut().push(Rc::downgrade(&slot));
AnimationHandle {
slot,
active_slots: self.active_slots.clone(),
}
}
pub fn live_slots(&self) -> usize {
let mut slots = self.slots.borrow_mut();
slots.retain(|slot| slot.strong_count() > 0);
slots.len()
}
#[cfg(test)]
pub(crate) fn active_slots(&self) -> usize {
let mut active = self.active_slots.borrow_mut();
active.retain(|slot| {
slot.upgrade()
.is_some_and(|slot| slot.borrow().is_playing())
});
active.len()
}
pub(crate) fn tick_active(&self, now: Instant) -> bool {
let mut active = self.active_slots.borrow_mut();
active.retain(|slot| {
let Some(slot) = slot.upgrade() else {
return false;
};
let mut slot = slot.borrow_mut();
slot.update(now);
slot.is_playing()
});
!active.is_empty()
}
}
pub fn animation_frame_signal() -> Signal<u32> {
FRAME_SIGNAL.with(|cell| {
let mut guard = cell.borrow_mut();
if guard.is_none() {
*guard = Some(Signal::new(0u32));
}
guard.as_ref().unwrap().clone()
})
}
pub fn request_animation_frame() {
PENDING.with(|f| f.set(true));
}
pub(crate) fn take_animation_pending() -> bool {
PENDING.with(|f| {
let was = f.get();
f.set(false);
was
})
}
pub(crate) fn tick_animation_frame() {
FRAME_SIGNAL.with(|cell| {
if let Some(signal) = cell.borrow().as_ref() {
signal.update(|v| *v = v.wrapping_add(1));
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn easing_curves_clamp_and_sample_expected_points() {
assert_eq!(Easing::Linear.sample(-1.0), 0.0);
assert_eq!(Easing::Linear.sample(2.0), 1.0);
assert_eq!(Easing::EaseIn.sample(0.5), 0.25);
assert_eq!(Easing::EaseOut.sample(0.5), 0.75);
assert_eq!(Easing::EaseInOut.sample(0.5), 0.5);
}
#[test]
fn registry_registers_live_slot() {
let registry = AnimRegistry::default();
assert_eq!(registry.live_slots(), 0);
let handle = registry.register(AnimSlot::new(AnimationConfig::default()));
assert_eq!(registry.live_slots(), 1);
drop(handle);
assert_eq!(registry.live_slots(), 0);
}
#[test]
fn handle_progress_applies_easing() {
let registry = AnimRegistry::default();
let handle = registry.register(AnimSlot {
config: AnimationConfig::new(Duration::from_secs(1)).easing(Easing::EaseIn),
raw_progress: 0.0,
started_at: Some(Instant::now() - Duration::from_millis(500)),
});
let progress = handle.progress();
assert!((0.20..=0.35).contains(&progress), "progress was {progress}");
assert!(handle.is_playing());
}
#[test]
fn handle_progress_completes_elapsed_animation() {
let registry = AnimRegistry::default();
let handle = registry.register(AnimSlot {
config: AnimationConfig::new(Duration::from_millis(10)),
raw_progress: 0.0,
started_at: Some(Instant::now() - Duration::from_secs(1)),
});
assert_eq!(handle.progress(), 1.0);
assert!(!handle.is_playing());
}
#[test]
fn play_and_reset_control_slot_state() {
let registry = AnimRegistry::default();
let handle = registry.register(AnimSlot::new(AnimationConfig::new(Duration::from_secs(1))));
assert_eq!(handle.progress(), 0.0);
handle.play();
assert!(handle.is_playing());
handle.reset();
assert_eq!(handle.progress(), 0.0);
assert!(!handle.is_playing());
}
}