use std::{io::{self, Write, Stdout}, time::{Duration, Instant}};
use crossterm::{cursor, event::Event, execute, queue, terminal};
use crate::{CellFlags, frame::Frame};
pub trait Animation {
fn init(&mut self, initial: Frame);
fn initial_frame(&self) -> Frame { Frame::from_terminal() }
fn update(&mut self, dt: Duration) -> 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,
start: Instant
}
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(),
start: Instant::now()
}
}
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.animation.initial_frame());
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();
if crossterm::event::poll(Duration::ZERO).unwrap_or(false) {
let event = crossterm::event::read()?;
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.start.elapsed());
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 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 {
if let Some(last_frame) = self.last_frame.as_ref() {
if last_frame.get_cell(row, col) != Some(&cells[row][col]) {
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}")?;
}
} else {
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(())
}
}