use std::time::{Duration, Instant};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use tachyonfx::{fx, Effect, EffectManager, Interpolation};
use super::theme;
const MAX_FRAME_DELTA: Duration = Duration::from_millis(32);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum EffectKey {
#[default]
Splash,
DialogTransition,
TaskRow(String),
ViewTransition,
EmptyState,
}
pub struct AnimationState {
manager: EffectManager<EffectKey>,
last_frame: Instant,
targeted_effects: Vec<(Rect, Effect)>,
pub enabled: bool,
}
impl std::fmt::Debug for AnimationState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AnimationState")
.field("enabled", &self.enabled)
.field("targeted_effects", &self.targeted_effects.len())
.finish()
}
}
impl Default for AnimationState {
fn default() -> Self {
Self::new()
}
}
impl AnimationState {
pub fn new() -> Self {
Self {
manager: EffectManager::default(),
last_frame: Instant::now(),
targeted_effects: Vec::new(),
enabled: true,
}
}
pub fn has_active_effects(&self) -> bool {
self.enabled && (self.manager.is_running() || !self.targeted_effects.is_empty())
}
pub fn process(&mut self, buf: &mut Buffer, area: Rect) {
if !self.enabled {
return;
}
let elapsed = self.last_frame.elapsed().min(MAX_FRAME_DELTA);
self.last_frame = Instant::now();
self.manager.process_effects(elapsed, buf, area);
self.targeted_effects.retain_mut(|(rect, effect)| {
if rect.x < area.width && rect.y < area.height {
effect.process(elapsed, buf, *rect);
}
!effect.done()
});
}
pub fn spawn(&mut self, key: EffectKey, effect: Effect) {
if !self.enabled {
return;
}
self.manager.add_unique_effect(key, effect);
}
pub fn spawn_anonymous(&mut self, effect: Effect) {
if !self.enabled {
return;
}
self.manager.add_effect(effect);
}
pub fn spawn_targeted(&mut self, area: Rect, effect: Effect) {
if !self.enabled {
return;
}
self.targeted_effects.push((area, effect));
}
pub fn cancel(&mut self, key: EffectKey) {
self.manager.cancel_unique_effect(key);
}
pub fn clear_targeted_effects(&mut self) {
self.targeted_effects.clear();
}
pub fn start_splash(&mut self) {
let effect = fx::sequence(&[
fx::coalesce((800, Interpolation::CubicOut)),
fx::sleep(400),
fx::fade_to(theme::BG_DARK, theme::BG_DARK, (500, Interpolation::CubicIn)),
]);
self.spawn(EffectKey::Splash, effect);
}
pub fn cancel_splash(&mut self) {
self.cancel(EffectKey::Splash);
}
pub fn start_dialog_open(&mut self) {
let effect = fx::fade_from(theme::BG_DARK, theme::BG_DARK, (200, Interpolation::CubicOut));
self.spawn(EffectKey::DialogTransition, effect);
}
pub fn start_quick_capture_open(&mut self) {
let effect = fx::fade_from(theme::BG_DARK, theme::BG_DARK, (150, Interpolation::CubicOut));
self.spawn(EffectKey::DialogTransition, effect);
}
pub fn start_dialog_close(&mut self) {
self.cancel(EffectKey::DialogTransition);
}
pub fn start_task_complete(&mut self, area: Rect) {
let effect = fx::sequence(&[
fx::fade_from(theme::SUCCESS, theme::SUCCESS, (300, Interpolation::CubicOut)),
fx::fade_to(theme::BG_DARK, theme::BG_DARK, (500, Interpolation::CubicIn)),
]);
self.spawn_targeted(area, effect);
}
pub fn start_task_dissolve(&mut self, area: Rect) {
let effect = fx::dissolve((300, Interpolation::CubicOut));
self.spawn_targeted(area, effect);
}
pub fn start_task_new(&mut self, area: Rect) {
let effect = fx::coalesce((600, Interpolation::CubicOut));
self.spawn_targeted(area, effect);
}
pub fn start_priority_flash(&mut self, area: Rect, color: Color) {
let effect = fx::fade_from(color, color, (500, Interpolation::CubicOut));
self.spawn_targeted(area, effect);
}
pub fn start_empty_state_ambient(&mut self) {
let effect = fx::ping_pong(fx::hsl_shift(
Some([0.0, 0.0, 10.0]), None, (2000, Interpolation::SineInOut),
));
self.spawn(EffectKey::EmptyState, effect);
}
pub fn cancel_empty_state(&mut self) {
self.cancel(EffectKey::EmptyState);
}
pub fn start_view_transition(&mut self) {
self.cancel(EffectKey::ViewTransition);
}
}