use std::any::{Any, TypeId};
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::time::Duration;
use crate::animation::transition::{Lerp, Transition, TransitionConfig};
use crate::core::element::Key;
trait DynEntry: Any {
fn entry_type_id(&self) -> TypeId;
fn tick(&mut self, dt: Duration) -> bool;
fn is_animating(&self) -> bool;
fn touched(&self) -> bool;
fn reset_touched(&self);
fn as_any_mut(&mut self) -> &mut dyn Any;
}
struct TypedEntry<T: Lerp + PartialEq + 'static> {
current: T,
target: T,
transition: Option<Transition<T>>,
touched: Cell<bool>,
}
impl<T: Lerp + PartialEq + 'static> DynEntry for TypedEntry<T> {
fn entry_type_id(&self) -> TypeId {
TypeId::of::<T>()
}
fn tick(&mut self, dt: Duration) -> bool {
let Some(transition) = self.transition.as_mut() else {
return false;
};
transition.tick(dt);
let new_current = transition.current();
let changed = new_current != self.current;
self.current = new_current;
if transition.is_complete() {
self.current = self.target.clone();
self.transition = None;
}
changed
}
fn is_animating(&self) -> bool {
self.transition.is_some()
}
fn touched(&self) -> bool {
self.touched.get()
}
fn reset_touched(&self) {
self.touched.set(false);
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Default)]
pub(crate) struct AnimationRegistry {
entries: RefCell<HashMap<Key, Box<dyn DynEntry>>>,
generation: Cell<u64>,
}
impl AnimationRegistry {
pub(crate) fn transition<T: Lerp + PartialEq + 'static>(
&self,
key: Key,
target: T,
config: TransitionConfig,
) -> T {
let mut entries = self.entries.borrow_mut();
let entry = entries.entry(key).or_insert_with(|| {
Box::new(TypedEntry::<T> {
current: target.clone(),
target: target.clone(),
transition: None,
touched: Cell::new(true),
})
});
if entry.entry_type_id() != TypeId::of::<T>() {
panic!(
"Ctx::transition called with a different value type for the same key (existing type id mismatch)"
);
}
let typed: &mut TypedEntry<T> = entry
.as_any_mut()
.downcast_mut()
.expect("type id checked above");
typed.touched.set(true);
if typed.target != target {
let from = typed.current.clone();
typed.target = target.clone();
if config.duration.is_zero() {
typed.current = target.clone();
typed.transition = None;
} else {
typed.transition = Some(Transition::new(
from,
target.clone(),
config.duration,
config.easing,
));
}
}
typed.current.clone()
}
pub(crate) fn tick(&self, dt: Duration) -> bool {
let mut entries = self.entries.borrow_mut();
let mut any_changed = false;
for entry in entries.values_mut() {
if entry.tick(dt) {
any_changed = true;
}
}
if any_changed {
self.generation
.set(self.generation.get().wrapping_add(1).max(1));
}
any_changed
}
pub(crate) fn end_frame_gc(&self) {
let mut entries = self.entries.borrow_mut();
let before = entries.len();
entries.retain(|_, e| e.touched());
if entries.len() != before {
self.generation
.set(self.generation.get().wrapping_add(1).max(1));
}
for e in entries.values() {
e.reset_touched();
}
}
pub(crate) fn has_active(&self) -> bool {
self.entries.borrow().values().any(|e| e.is_animating())
}
pub(crate) fn generation(&self) -> u64 {
self.generation.get()
}
#[cfg(test)]
pub(crate) fn entry_count(&self) -> usize {
self.entries.borrow().len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::animation::easing::Easing;
use crate::style::Color;
fn cfg(ms: u64) -> TransitionConfig {
TransitionConfig {
duration: Duration::from_millis(ms),
easing: Easing::Linear,
}
}
#[test]
fn first_call_returns_target_with_no_transition() {
let reg = AnimationRegistry::default();
let v = reg.transition::<Color>("k".into(), Color::Red, cfg(100));
assert_eq!(v, Color::Red);
assert!(!reg.has_active());
}
#[test]
fn changing_target_starts_transition_and_ticks_toward_it() {
let reg = AnimationRegistry::default();
let v0 = reg.transition::<Color>("k".into(), Color::Red, cfg(100));
assert_eq!(v0, Color::Red);
let v1 = reg.transition::<Color>("k".into(), Color::Blue, cfg(100));
assert_eq!(v1, Color::Red);
assert!(reg.has_active());
let changed = reg.tick(Duration::from_millis(50));
assert!(changed);
let v2 = reg.transition::<Color>("k".into(), Color::Blue, cfg(100));
assert!(v2 != Color::Red && v2 != Color::Blue);
let _ = reg.tick(Duration::from_millis(60));
assert!(!reg.has_active());
let v3 = reg.transition::<Color>("k".into(), Color::Blue, cfg(100));
assert_eq!(v3, Color::Blue);
}
#[test]
fn zero_duration_snaps_immediately() {
let reg = AnimationRegistry::default();
let _ = reg.transition::<Color>("k".into(), Color::Red, cfg(0));
let v = reg.transition::<Color>("k".into(), Color::Blue, cfg(0));
assert_eq!(v, Color::Blue);
assert!(!reg.has_active());
}
#[test]
fn end_frame_gc_drops_untouched_keys() {
let reg = AnimationRegistry::default();
let _ = reg.transition::<Color>("a".into(), Color::Red, cfg(100));
let _ = reg.transition::<Color>("b".into(), Color::Red, cfg(100));
assert_eq!(reg.entry_count(), 2);
reg.end_frame_gc();
assert_eq!(reg.entry_count(), 2);
let _ = reg.transition::<Color>("a".into(), Color::Red, cfg(100));
reg.end_frame_gc();
assert_eq!(reg.entry_count(), 1);
}
#[test]
fn tick_with_no_active_returns_false() {
let reg = AnimationRegistry::default();
let _ = reg.transition::<Color>("k".into(), Color::Red, cfg(100));
assert!(!reg.tick(Duration::from_millis(16)));
}
#[test]
fn f32_transitions_supported() {
let reg = AnimationRegistry::default();
let _ = reg.transition::<f32>("scalar".into(), 0.0, cfg(100));
let _ = reg.transition::<f32>("scalar".into(), 1.0, cfg(100));
let _ = reg.tick(Duration::from_millis(50));
let v = reg.transition::<f32>("scalar".into(), 1.0, cfg(100));
assert!((0.4..=0.6).contains(&v));
}
#[test]
#[should_panic(expected = "different value type")]
fn reusing_key_with_different_type_panics() {
let reg = AnimationRegistry::default();
let _ = reg.transition::<Color>("k".into(), Color::Red, cfg(100));
let _ = reg.transition::<f32>("k".into(), 0.0, cfg(100));
}
}