use std::cell::RefCell;
use std::collections::HashMap;
use std::mem::ManuallyDrop;
use std::rc::Weak;
use std::time::Instant;
pub(crate) trait Tickable {
fn tick(&self, now: Instant, scale: f32);
fn is_settled(&self) -> bool;
}
struct Registry {
entries: HashMap<u64, Weak<dyn Tickable>>,
next_id: u64,
scale: f32,
continuous: u32,
}
impl Registry {
fn new() -> Self {
Registry {
entries: HashMap::new(),
next_id: 0,
scale: 1.0,
continuous: 0,
}
}
}
thread_local! {
static REGISTRY: ManuallyDrop<RefCell<Registry>> = ManuallyDrop::new(RefCell::new(Registry::new()));
}
pub(crate) fn next_id() -> u64 {
REGISTRY.with(|r| {
let mut reg = r.borrow_mut();
let id = reg.next_id;
reg.next_id += 1;
id
})
}
pub(crate) fn register(id: u64, weak: Weak<dyn Tickable>) {
REGISTRY.with(|r| {
r.borrow_mut().entries.insert(id, weak);
});
}
fn is_live(weak: &Weak<dyn Tickable>) -> bool {
matches!(weak.upgrade(), Some(anim) if !anim.is_settled())
}
pub fn tick(now: Instant) {
let (scale, live): (f32, Vec<std::rc::Rc<dyn Tickable>>) = REGISTRY.with(|r| {
let reg = r.borrow();
let live = reg.entries.values().filter_map(Weak::upgrade).collect();
(reg.scale, live)
});
for anim in &live {
anim.tick(now, scale);
}
REGISTRY.with(|r| {
r.borrow_mut().entries.retain(|_, weak| is_live(weak));
});
}
pub fn has_active() -> bool {
REGISTRY.with(|r| r.borrow().entries.values().any(is_live))
}
pub struct Continuous(());
impl Continuous {
pub fn new() -> Self {
REGISTRY.with(|r| r.borrow_mut().continuous += 1);
Self(())
}
}
impl Default for Continuous {
fn default() -> Self {
Self::new()
}
}
impl Drop for Continuous {
fn drop(&mut self) {
REGISTRY.with(|r| {
let mut reg = r.borrow_mut();
reg.continuous = reg.continuous.saturating_sub(1);
});
}
}
pub fn has_continuous() -> bool {
REGISTRY.with(|r| r.borrow().continuous > 0)
}
pub fn reset() {
REGISTRY.with(|r| {
let mut reg = r.borrow_mut();
reg.entries.clear();
reg.continuous = 0;
});
}
pub fn set_scale(scale: f32) {
REGISTRY.with(|r| r.borrow_mut().scale = scale.max(0.0));
}
#[cfg(test)]
mod tests {
use super::*;
fn fresh() {
reset();
}
#[test]
fn a_live_guard_keeps_the_loop_awake() {
fresh();
assert!(!has_continuous());
let region = Continuous::new();
assert!(has_continuous());
drop(region);
assert!(!has_continuous());
}
#[test]
fn the_loop_sleeps_only_when_the_last_region_goes() {
fresh();
let first = Continuous::new();
let second = Continuous::new();
drop(first);
assert!(has_continuous(), "one region is still on screen");
drop(second);
assert!(!has_continuous());
}
#[test]
fn a_reload_leaves_no_phantom_region_scheduling_frames() {
fresh();
let region = Continuous::new();
reset();
assert!(!has_continuous());
drop(region);
assert!(!has_continuous(), "the counter must not wrap below zero");
}
#[test]
fn a_continuous_region_is_not_an_active_animation() {
fresh();
let _region = Continuous::new();
assert!(has_continuous());
assert!(!has_active(), "no animation was registered");
}
}