use std::{fmt::Debug, sync::mpsc::Sender};
use ratatui::{
buffer::Buffer,
layout::{Margin, Rect},
style::Color,
};
use tachyonfx::{
fx::*,
CellFilter::{AllOf, Inner, Not, Outer, RefArea, Text},
Duration, Effect, EffectManager, Interpolation, IntoEffect, Motion, RefRect,
};
use crate::{
event::{GlimEvent, GlitchState},
gruvbox::Gruvbox::{Dark0, Dark0Hard, Dark3},
};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FxId {
#[default]
ConfigPopup,
Glitch,
Notification,
PipelineActionsPopup,
ProjectDetailsPopup,
}
pub struct EffectRegistry {
effects: EffectManager<FxId>,
sender: Sender<GlimEvent>,
screen_area: RefRect,
animations_enabled: bool,
}
impl EffectRegistry {
pub fn new(sender: Sender<GlimEvent>) -> Self {
Self {
effects: EffectManager::default(),
screen_area: RefRect::default(),
sender,
animations_enabled: true, }
}
pub fn apply(&mut self, event: &GlimEvent) {
use GlimEvent::*;
match event {
GlitchOverride(g) => self.register_glitch_effect(*g),
ProjectDetailsClose => self.register_close_popup(FxId::ProjectDetailsPopup),
PipelineActionsClose => self.register_close_popup(FxId::PipelineActionsPopup),
ConfigClose => self.register_close_popup(FxId::ConfigPopup),
ConfigUpdate(config) => self.animations_enabled = config.animations,
_ => (),
}
}
pub fn update_screen_area(&self, screen_area: Rect) {
self.screen_area.set(screen_area);
}
pub fn process_effects(&mut self, duration: Duration, buf: &mut Buffer, area: Rect) {
let effective_duration = if self.animations_enabled {
duration
} else {
Duration::from_secs(3600) };
self.effects
.process_effects(effective_duration, buf, area);
}
pub fn register_projects_table_new_data(&mut self, exclude_popup_area: Option<RefRect>) {
let filter = match exclude_popup_area {
Some(area) => AllOf(vec![Inner(Margin::new(1, 1)), Not(RefArea(area).into())]),
None => Inner(Margin::new(1, 1)),
};
let fx = parallel(&[
coalesce(550),
sweep_in(
Motion::LeftToRight,
50,
0,
Dark0Hard,
(450, Interpolation::QuadIn),
),
])
.with_filter(filter);
self.effects.add_effect(fx);
}
pub fn register_glitch_effect(&mut self, glitch_state: GlitchState) {
let fx = if self.animations_enabled {
match glitch_state {
GlitchState::Normal => default_glitch_effect(),
GlitchState::RampedUp => Glitch::builder()
.action_ms(100..200)
.action_start_delay_ms(0..500)
.cell_glitch_ratio(0.05)
.build()
.into_effect(),
}
} else {
consume_tick()
};
self.add_unique(FxId::Glitch, fx);
}
pub fn register_default_glitch_effect(&mut self) {
self.register_glitch_effect(GlitchState::Normal);
}
pub fn register_project_details(&mut self, popup_area: RefRect) {
self.register_popup(FxId::ProjectDetailsPopup, popup_area);
}
pub fn register_pipeline_actions(&mut self, popup_area: RefRect) {
self.register_popup(FxId::PipelineActionsPopup, popup_area);
}
pub fn register_config_popup(&mut self, popup_area: RefRect) {
self.register_popup(FxId::ConfigPopup, popup_area);
}
fn register_popup(&mut self, id: FxId, popup_area: RefRect) {
let fx = parallel(&[
dynamic_area(popup_area.clone(), open_window_fx(Dark0)),
dim_screen_behind_popup(self.screen_area(), popup_area),
]);
self.add_unique(id, fx);
}
fn register_close_popup(&mut self, id: FxId) {
let fx = fade_from(Dark3, Dark0Hard, (300, Interpolation::CircIn));
self.add_unique(id, fx);
}
pub fn register_notification_effect(&mut self, content_area: RefRect) {
use tachyonfx::Interpolation::{SineIn, SineOut};
use crate::gruvbox::Gruvbox::Dark0Hard;
let main_fx = sequence(&[
parallel(&[
draw_border(Duration::from_millis(100)),
dissolve(Duration::from_millis(100)),
]),
fade_from_fg(Dark0Hard, (250, SineOut)),
with_duration(
Duration::from_millis(6000),
repeating(ping_pong(hsl_shift_fg([0.0, 0.0, 25.0], (500, SineOut)))),
),
prolong_end(
Duration::from_millis(100),
fade_to_fg(Dark0Hard, (250, SineIn)),
),
parallel(&[draw_border(Duration::from_millis(150)), coalesce(150)]),
]);
let fx = sequence(&[
dynamic_area(content_area, main_fx),
self.dispatch(GlimEvent::NotificationDismiss),
]);
self.add_unique(FxId::Notification, fx);
}
fn dispatch(&mut self, event: GlimEvent) -> Effect {
dispatch_event(self.sender.clone(), event)
}
fn screen_area(&self) -> RefRect {
self.screen_area.clone()
}
fn add_unique(&mut self, id: FxId, fx: Effect) {
self.effects.add_unique_effect(id, fx);
}
}
fn default_glitch_effect() -> Effect {
Glitch::builder()
.action_ms(100..500)
.action_start_delay_ms(0..2000)
.cell_glitch_ratio(0.0015)
.build()
.into_effect()
}
fn open_window_fx<C: Into<Color>>(bg: C) -> Effect {
let margin = Margin::new(1, 1);
let border_text = AllOf(vec![Outer(margin), Text]);
let border_decorations = AllOf(vec![Outer(margin), Not(Text.into())]);
let bg = bg.into();
parallel(&[
fade_from(Dark0, Dark0, (320, Interpolation::QuadOut)).with_filter(border_decorations),
sequence(&[
timed_never_complete(Duration::from_millis(320), fade_to(Dark0, Dark0, 0)),
fade_from(Dark0, Dark0, (320, Interpolation::QuadOut)),
])
.with_filter(border_text),
sequence(&[
with_duration(
Duration::from_millis(270),
parallel(&[
never_complete(dissolve(0)), never_complete(fade_to(bg, bg, 0)),
]),
),
parallel(&[
coalesce(Duration::from_millis(120)),
fade_from(bg, bg, (130, Interpolation::QuadOut)),
sweep_in(Motion::UpToDown, 10, 0, bg, (130, Interpolation::Linear)),
]),
])
.with_filter(Inner(margin)),
])
}
fn dim_screen_behind_popup(screen_area: RefRect, popup_area: RefRect) -> Effect {
let screen = RefArea(screen_area);
let popup = RefArea(popup_area);
let behind_popup = AllOf(vec![screen, Not(popup.into())]);
never_complete(fade_to(Dark3, Dark0Hard, (1150, Interpolation::QuadIn)))
.with_filter(behind_popup)
}
fn draw_border(duration: Duration) -> Effect {
effect_fn((), duration, |_, _, cells| {
cells.for_each(|(_, cell)| {
cell.set_char('─');
});
})
}
fn dispatch_event<T: Clone + Debug + Send + 'static>(sender: Sender<T>, event: T) -> Effect {
effect_fn_buf(Some(event), 1, move |e, _, _| {
if let Some(e) = e.take() {
let _ = sender.send(e);
}
})
}