use std::{
collections::VecDeque, io::{self, Stdout, Write}, time::{Duration, Instant}
};
use crossterm::{cursor, event::Event, execute, queue, terminal};
use crate::{CellFlags, frame::Frame};
pub trait Animation {
fn init(&mut self) {
self.init_with(self.initial_frame());
}
fn init_with(&mut self, initial: Frame);
fn initial_frame(&self) -> Frame {
Frame::from_terminal()
}
fn update(&mut self) -> Frame;
fn is_done(&self) -> bool;
fn is_running(&self) -> bool {
!self.is_done()
}
fn resize(&mut self, w: usize, h: usize);
fn on_event(&mut self, _event: crossterm::event::Event) {
log::trace!("Received event: {:?}", _event);
}
}
struct RawModeGuard;
impl RawModeGuard {
pub fn enter() -> io::Result<Self> {
let mut stdout = io::stdout();
execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)?;
terminal::enable_raw_mode()?;
Ok(Self)
}
}
impl Drop for RawModeGuard {
fn drop(&mut self) {
let mut stdout = io::stdout();
execute!(stdout, terminal::LeaveAlternateScreen, cursor::Show).ok();
terminal::disable_raw_mode().ok();
}
}
pub struct Animator {
animation: Box<dyn Animation>,
last_frame: Option<Frame>,
raw_mode_state: Option<RawModeGuard>,
frame_rate: usize,
last_cols: u16,
last_rows: u16,
out_channel: Stdout,
queued_events: VecDeque<Event>
}
impl Animator {
pub fn new(animation: Box<dyn Animation>) -> Self {
let (last_cols, last_rows) = crossterm::terminal::size().unwrap_or((80, 24));
Self {
animation,
last_frame: None,
raw_mode_state: None,
frame_rate: 24,
last_cols,
last_rows,
out_channel: io::stdout(),
queued_events: VecDeque::new()
}
}
pub fn target_fps(mut self, fps: usize) -> Self {
self.frame_rate = fps;
self
}
pub fn enter_with(animation: Box<dyn Animation>) -> io::Result<Self> {
let mut new = Self::new(animation);
new.enter()?;
Ok(new)
}
pub fn enter(&mut self) -> io::Result<()> {
let guard = RawModeGuard::enter();
self.animation.init();
self.raw_mode_state = Some(guard?);
Ok(())
}
pub fn leave(&mut self) {
self.raw_mode_state = None;
}
pub fn tick(&mut self) -> io::Result<bool> {
let tick_start = Instant::now();
let has_event = crossterm::event::poll(Duration::ZERO).unwrap_or(false);
if has_event || !self.queued_events.is_empty() {
let event = if has_event {
crossterm::event::read().unwrap()
} else {
self.queued_events.pop_front().unwrap()
};
match event {
Event::Resize(cols, rows) => {
if cols != self.last_cols || rows != self.last_rows {
self.animation.resize(cols as usize, rows as usize);
self.last_frame = None; self.last_cols = cols;
self.last_rows = rows;
}
}
Event::Key(key) => {
if key.code == crossterm::event::KeyCode::Char('c')
&& key
.modifiers
.contains(crossterm::event::KeyModifiers::CONTROL)
{
return Err(io::Error::new(io::ErrorKind::Interrupted, "Interrupted"));
} else {
self.animation.on_event(Event::Key(key));
}
}
_ => {
self.animation.on_event(event);
}
}
}
let frame = self.animation.update();
self.render(frame)?;
let tick_duration = tick_start.elapsed().as_millis();
let target = 1000 / self.frame_rate; let sleep_time = target.saturating_sub(tick_duration as usize);
if sleep_time > 0 {
log::trace!("tick completed with {} ms to spare", sleep_time);
std::thread::sleep(Duration::from_millis(sleep_time as u64));
}
Ok(self.animation().is_running())
}
pub fn enqueue_event(&mut self, event: Event) {
self.queued_events.push_back(event);
}
pub fn animation(&self) -> &dyn Animation {
&*self.animation
}
#[allow(clippy::needless_range_loop)]
fn render(&mut self, frame: Frame) -> io::Result<()> {
let cells = frame.into_cells();
let rows = cells.len();
if rows == 0 {
return Ok(());
}
let cols = cells[0].len();
for row in 0..rows {
for col in 0..cols {
let changed = self
.last_frame
.as_ref()
.is_none_or(|f| f.get_cell(row, col) != Some(&cells[row][col]));
if changed {
let cell = &cells[row][col];
if cell.flags().contains(CellFlags::WIDE_CONTINUATION) {
continue;
}
queue!(self.out_channel, cursor::MoveTo(col as u16, row as u16))?;
write!(self.out_channel, "{cell}")?;
}
}
}
self.out_channel.flush()?;
self.last_frame = Some(Frame::from_cells(cells));
Ok(())
}
}