use crate::{Event, Result, TerminalWindow, Window};
use crossterm::terminal;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FrameProfile {
pub input_poll_time: Duration,
pub update_time: Duration,
pub draw_time: Duration,
pub frame_time: Duration,
pub events_processed: usize,
pub budget: Option<Duration>,
pub over_budget: bool,
}
pub struct App<S> {
window: TerminalWindow,
state: S,
frame_rate: Option<Duration>,
frame_budget: Option<Duration>,
frame_profile_hook: Option<Box<dyn FnMut(&FrameProfile)>>,
}
impl<S> App<S> {
pub fn new(initial_state: S) -> Result<Self> {
let mut window = TerminalWindow::new()?;
window.set_auto_flush(false);
Ok(App {
window,
state: initial_state,
frame_rate: None, frame_budget: None,
frame_profile_hook: None,
})
}
pub fn window(&self) -> &TerminalWindow {
&self.window
}
pub fn window_mut(&mut self) -> &mut TerminalWindow {
&mut self.window
}
pub fn with_frame_rate(mut self, frame_rate: Duration) -> Self {
self.frame_rate = Some(frame_rate);
self
}
pub fn with_frame_budget(mut self, budget: Duration) -> Self {
self.frame_budget = Some(budget);
self
}
pub fn set_frame_profile_hook<F>(&mut self, hook: F)
where
F: FnMut(&FrameProfile) + 'static,
{
self.frame_profile_hook = Some(Box::new(hook));
}
pub fn clear_frame_profile_hook(&mut self) {
self.frame_profile_hook = None;
}
pub fn run<U, D>(&mut self, mut update: U, mut draw: D) -> Result<()>
where
U: FnMut(&mut S, Event) -> bool, D: FnMut(&mut S, &mut dyn Window) -> Result<()>,
{
let mut last_tick = Instant::now();
loop {
let frame_start = Instant::now();
const MAX_EVENTS_PER_FRAME: usize = 256;
let mut events_processed = 0usize;
let input_poll_start = Instant::now();
let mut update_time = Duration::ZERO;
for _ in 0..MAX_EVENTS_PER_FRAME {
match self.window.poll_input()? {
Some(event) => {
events_processed = events_processed.saturating_add(1);
let update_start = Instant::now();
if !update(&mut self.state, event) {
return Ok(()); }
update_time = update_time.saturating_add(update_start.elapsed());
}
None => break, }
}
let input_poll_time = input_poll_start.elapsed();
if let Some(frame_rate) = self.frame_rate
&& last_tick.elapsed() >= frame_rate
{
let update_start = Instant::now();
if !update(&mut self.state, Event::Frame) {
break; }
update_time = update_time.saturating_add(update_start.elapsed());
last_tick = Instant::now();
}
if let Ok((cols, rows)) = terminal::size() {
self.window.handle_resize(cols, rows);
}
self.window.clear_screen()?;
let draw_start = Instant::now();
draw(&mut self.state, &mut self.window)?;
let draw_time = draw_start.elapsed();
let frame_time = frame_start.elapsed();
let over_budget = self.frame_budget.is_some_and(|budget| frame_time > budget);
if let Some(hook) = self.frame_profile_hook.as_mut() {
hook(&FrameProfile {
input_poll_time,
update_time,
draw_time,
frame_time,
events_processed,
budget: self.frame_budget,
over_budget,
});
}
if let Some(budget) = self.frame_budget {
let remaining = budget.saturating_sub(frame_time);
if !remaining.is_zero() {
std::thread::sleep(remaining);
}
} else {
std::thread::sleep(Duration::from_millis(1));
}
}
Ok(())
}
}