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;
use crate::style::{Color, Paint};
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 paint_resolved(&self) -> bool;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn as_any(&self) -> &dyn Any;
}
struct TypedEntry<T: Lerp + PartialEq + 'static> {
current: T,
target: T,
transition: Option<Transition<T>>,
touched: Cell<bool>,
paint_resolved: 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 paint_resolved(&self) -> bool {
self.paint_resolved.get()
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn as_any(&self) -> &dyn Any {
self
}
}
thread_local! {
static RENDER_REGISTRY: RefCell<Option<std::rc::Rc<AnimationRegistry>>> =
const { RefCell::new(None) };
}
pub(crate) struct RenderRegistryScope(Option<std::rc::Rc<AnimationRegistry>>);
impl Drop for RenderRegistryScope {
fn drop(&mut self) {
RENDER_REGISTRY.with(|slot| *slot.borrow_mut() = self.0.take());
}
}
pub(crate) fn set_render_registry(registry: std::rc::Rc<AnimationRegistry>) -> RenderRegistryScope {
let prev = RENDER_REGISTRY.with(|slot| slot.borrow_mut().replace(registry));
RenderRegistryScope(prev)
}
pub(crate) fn resolve_render_paint_slot(slot: u16) -> Option<Color> {
RENDER_REGISTRY.with(|installed| {
installed
.borrow()
.as_ref()
.and_then(|registry| registry.resolve_paint_slot(slot))
})
}
#[derive(Default)]
pub(crate) struct AnimationRegistry {
entries: RefCell<HashMap<Key, Box<dyn DynEntry>>>,
color_slots: RefCell<Vec<Key>>,
slot_by_key: RefCell<HashMap<Key, u16>>,
generation: Cell<u64>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct TransitionTick {
pub(crate) view_changed: bool,
pub(crate) paint_changed: bool,
}
impl AnimationRegistry {
pub(crate) fn transition<T: Lerp + PartialEq + 'static>(
&self,
key: Key,
target: T,
config: TransitionConfig,
) -> T {
self.advance(key, target, config, false)
}
pub(crate) fn animated_paint(
&self,
key: Key,
target: Color,
config: TransitionConfig,
) -> Paint {
let current = self.advance(key.clone(), target, config, true);
match self.slot_for(key) {
Some(slot) => Paint::Animated {
slot,
fallback: current,
},
None => Paint::Solid(current),
}
}
fn slot_for(&self, key: Key) -> Option<u16> {
if let Some(slot) = self.slot_by_key.borrow().get(&key) {
return Some(*slot);
}
let mut slots = self.color_slots.borrow_mut();
let slot = u16::try_from(slots.len()).ok()?;
slots.push(key.clone());
self.slot_by_key.borrow_mut().insert(key, slot);
Some(slot)
}
pub(crate) fn resolve_paint_slot(&self, slot: u16) -> Option<Color> {
let key = self.color_slots.borrow().get(slot as usize)?.clone();
let entries = self.entries.borrow();
let entry = entries.get(&key)?;
let typed = entry.as_any().downcast_ref::<TypedEntry<Color>>()?;
Some(typed.current)
}
fn advance<T: Lerp + PartialEq + 'static>(
&self,
key: Key,
target: T,
config: TransitionConfig,
paint_resolved: bool,
) -> 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),
paint_resolved: Cell::new(paint_resolved),
})
});
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 !paint_resolved {
typed.paint_resolved.set(false);
}
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) -> TransitionTick {
let mut entries = self.entries.borrow_mut();
let mut result = TransitionTick::default();
for entry in entries.values_mut() {
if entry.tick(dt) {
if entry.paint_resolved() {
result.paint_changed = true;
} else {
result.view_changed = true;
}
}
}
if result.view_changed {
self.generation
.set(self.generation.get().wrapping_add(1).max(1));
}
result
}
pub(crate) fn end_frame_gc(&self) {
let mut entries = self.entries.borrow_mut();
let before = entries.len();
entries.retain(|_, e| e.touched());
debug_assert!(
self.color_slots.borrow().len() >= self.slot_by_key.borrow().len(),
"slot table and reverse map must stay consistent"
);
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.view_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_eq!(
reg.tick(Duration::from_millis(16)),
TransitionTick::default()
);
}
#[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));
}
}