use std::sync::LazyLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{sync::Arc, time::Duration};
use dashmap::DashMap;
use gpui::*;
use parking_lot::RwLock;
use smol::channel::{self, Receiver, Sender};
use crate::animation::{AnimationPriority, Event};
use crate::interpolate::State;
pub mod color;
pub mod general;
pub mod position;
pub trait Transition: Send + Sync + 'static {
fn run(&self, start: std::time::Instant, duration: std::time::Duration) -> f32 {
let t = (start.elapsed().as_secs_f32() / duration.as_secs_f32()).min(1.0);
self.calculate(t)
}
fn calculate(&self, t: f32) -> f32;
}
pub trait IntoArcTransition<T: Transition + 'static> {
fn into_arc(self) -> Arc<T>;
}
impl<T: Transition + 'static> IntoArcTransition<T> for T {
fn into_arc(self) -> Arc<T> {
Arc::new(self)
}
}
impl<T: Transition + 'static> IntoArcTransition<T> for Arc<T> {
fn into_arc(self) -> Arc<T> {
self
}
}
pub(crate) struct PersistentContext {
event: Event,
style: StyleRefinement,
duration: Duration,
transition: Arc<dyn Transition>,
priority: AnimationPriority,
}
pub(crate) struct ActiveAnimation {
event: Event,
duration: Duration,
origin_duration: Duration,
transition: Arc<dyn Transition>,
ver: usize,
persistent: bool,
}
pub(crate) struct TransitionRegistry {
initialized: AtomicBool,
rem_size: RwLock<Pixels>,
states: DashMap<ElementId, State<StyleRefinement>>,
active_animations: DashMap<ElementId, ActiveAnimation>,
saved_contexts: DashMap<ElementId, PersistentContext>,
wakeup_tx: Sender<()>,
wakeup_rx: Receiver<()>,
}
pub(crate) static TRANSITION_REGISTRY: LazyLock<TransitionRegistry> = LazyLock::new(|| {
let (tx, rx) = channel::unbounded();
TransitionRegistry {
initialized: AtomicBool::new(false),
rem_size: RwLock::new(Pixels::from(16.)),
states: DashMap::with_capacity_and_shard_amount(64, 4),
active_animations: DashMap::with_capacity_and_shard_amount(32, 4),
saved_contexts: DashMap::with_capacity_and_shard_amount(16, 4),
wakeup_tx: tx,
wakeup_rx: rx,
}
});
impl TransitionRegistry {
pub fn init(window: &mut Window, cx: &mut App) {
let new_rem = window.rem_size();
if *TRANSITION_REGISTRY.rem_size.read() != new_rem {
*TRANSITION_REGISTRY.rem_size.write() = new_rem;
}
if !TRANSITION_REGISTRY.initialized.swap(true, Ordering::SeqCst) {
cx.spawn(Self::animation_tick).detach();
}
}
pub fn rem_size() -> Pixels {
*TRANSITION_REGISTRY.rem_size.read()
}
pub fn save_persistent_context(
id: &ElementId,
style: &StyleRefinement,
duration: Duration,
transition: Arc<dyn Transition>,
priority: AnimationPriority,
) {
if let Some(active_anim) = TRANSITION_REGISTRY.active_animations.get(id)
&& active_anim.persistent
{
TRANSITION_REGISTRY.saved_contexts.insert(
id.clone(),
PersistentContext {
event: active_anim.event.clone(),
style: style.clone(),
duration,
transition,
priority,
},
);
}
}
pub fn remove_persistent_context(id: &ElementId, event: Event) {
TRANSITION_REGISTRY
.saved_contexts
.remove_if(id, |_, ctx| ctx.event.eq(&event));
}
pub fn background_animated_task(
id: ElementId,
event: Event,
duration: Duration,
origin_duration: Duration,
transition: Arc<dyn Transition>,
ver: usize,
persistent: bool,
) {
TRANSITION_REGISTRY.active_animations.insert(
id,
ActiveAnimation {
event,
duration,
origin_duration,
transition,
ver,
persistent,
},
);
if TRANSITION_REGISTRY.wakeup_tx.try_send(()).is_err() {
#[cfg(debug_assertions)]
eprintln!("[gpui-animation] wakeup channel closed, animation tick may be dead");
}
}
pub async fn animation_tick(cx: &mut AsyncApp) {
let frame_duration = Duration::from_secs_f32(1. / 120.);
let registry = &*TRANSITION_REGISTRY;
let mut ids: Vec<ElementId> = Vec::with_capacity(32);
let mut completed: Vec<(ElementId, Duration, Arc<dyn Transition>)> = Vec::with_capacity(8);
loop {
ids.clear();
completed.clear();
ids.extend(registry.active_animations.iter().map(|e| e.key().clone()));
let mut changed = false;
for id in &ids {
let Some(active) = registry.active_animations.get(id) else {
continue;
};
let Some(mut state) = registry.states.get_mut(id) else {
drop(active);
registry.active_animations.remove(id);
continue;
};
changed = true;
let done = state.animated(
active.ver,
active.duration,
&active.transition,
active.persistent,
);
if done {
if active.event.ne(&Event::NONE) {
state.priority = AnimationPriority::Lowest;
completed.push((
id.clone(),
active.origin_duration,
active.transition.clone(),
));
}
drop(active);
drop(state);
registry.active_animations.remove(id);
}
}
let still_animating = !registry.active_animations.is_empty();
if changed && still_animating {
cx.update(|cx| cx.refresh_windows()).ok();
}
for (id, origin_duration, transition) in &completed {
if let Some((_, ctx)) = registry.saved_contexts.remove(id) {
if let Some(mut state) = registry.states.get_mut(id) {
state.priority = ctx.priority;
state.to = ctx.style;
let (ver, dt) = state.pre_animated(ctx.duration);
Self::background_animated_task(
id.clone(),
ctx.event.clone(),
dt,
dt,
ctx.transition.clone(),
ver,
true,
);
}
} else if let Some(mut state) = registry.states.get_mut(id) {
state.priority = AnimationPriority::Lowest;
let (ver, dt) = state.pre_animated(*origin_duration);
state.to = state.origin.clone();
Self::background_animated_task(
id.clone(),
Event::NONE,
dt,
dt,
transition.clone(),
ver,
false,
);
}
}
if changed && !still_animating {
cx.update(|cx| cx.refresh_windows()).ok();
}
if registry.active_animations.is_empty() {
registry.wakeup_rx.recv().await.ok();
} else {
smol::Timer::after(frame_duration).await;
}
}
}
#[inline]
pub fn with_state_default<R>(
id: ElementId,
default: &StyleRefinement,
f: impl FnOnce(&mut State<StyleRefinement>) -> R,
) -> R {
let mut state = TRANSITION_REGISTRY
.states
.entry(id)
.or_insert_with(|| State::new(default.clone()));
f(&mut *state)
}
#[inline]
pub fn state_mut(
id: ElementId,
) -> Option<dashmap::mapref::one::RefMut<'static, ElementId, State<StyleRefinement>>> {
if !TRANSITION_REGISTRY
.initialized
.load(std::sync::atomic::Ordering::Relaxed)
{
return None;
}
TRANSITION_REGISTRY.states.get_mut(&id)
}
pub fn reset_state(id: &ElementId) {
TRANSITION_REGISTRY.states.remove(id);
TRANSITION_REGISTRY.active_animations.remove(id);
TRANSITION_REGISTRY.saved_contexts.remove(id);
}
}