use crate::backend::Backend;
use crate::terminal::Terminal;
use core::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Flow {
Continue,
Idle,
Exit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Frame {
pub delta: Duration,
pub frame: u64,
}
pub trait App<B: Backend> {
fn update(&mut self, term: &mut Terminal<B>, frame: &Frame) -> Flow;
}
#[must_use]
pub fn step<B: Backend, A: App<B>>(term: &mut Terminal<B>, app: &mut A, frame: &Frame) -> Flow {
app.update(term, frame)
}
#[cfg(feature = "std")]
pub fn run_blocking<B, A>(term: Terminal<B>, app: A) -> Result<(), B::Error>
where
B: Backend,
A: App<B>,
{
run_blocking_with(term, app, RunOptions::default())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct RunOptions {
pub max_fps: Option<u32>,
}
impl RunOptions {
#[must_use]
pub const fn paced(max_fps: u32) -> Self {
Self {
max_fps: Some(max_fps),
}
}
}
#[cfg(feature = "std")]
pub fn run_blocking_with<B, A>(
mut term: Terminal<B>,
mut app: A,
options: RunOptions,
) -> Result<(), B::Error>
where
B: Backend,
A: App<B>,
{
let mut clock = options.max_fps.map(crate::frame_clock::FrameClock::new);
let mut frame_count = 0u64;
let mut last = std::time::Instant::now();
loop {
if let Some(clock) = clock.as_mut() {
let elapsed = last.elapsed();
if let Some(remaining) = clock.step().checked_sub(elapsed) {
std::thread::sleep(remaining);
}
clock.advance(clock.step().max(elapsed));
let _ = clock.tick();
}
let now = std::time::Instant::now();
let delta = now.duration_since(last);
last = now;
let frame = Frame {
delta,
frame: frame_count,
};
frame_count = frame_count.wrapping_add(1);
let present_count_before = term.present_count();
let flow = step(&mut term, &mut app, &frame);
if flow == Flow::Exit {
return Ok(());
}
if flow != Flow::Idle && term.present_count() == present_count_before {
term.present()?;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::Headless;
use crate::event::{Event, KeyCode, KeyEvent, KeyModifiers};
struct Counter {
frames: u64,
}
impl App<Headless> for Counter {
fn update(&mut self, term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
self.frames += 1;
term.surface()
.put((0, 0), '#', crate::style::Style::default());
term.present().expect("present");
if term.has_input() || frame.frame >= 100 {
Flow::Exit
} else {
Flow::Continue
}
}
}
#[test]
fn run_blocking_exits_on_flow_exit() {
let mut backend = Headless::new(4, 1);
backend.push_event(Event::Key(KeyEvent::new(
KeyCode::Char('q'),
KeyModifiers::NONE,
)));
let term = Terminal::new(backend);
let app = Counter { frames: 0 };
run_blocking(term, app).expect("run_blocking");
}
#[test]
fn step_forwards_to_update() {
let mut term = Terminal::new(Headless::new(2, 1));
let mut app = Counter { frames: 0 };
let frame = Frame {
delta: Duration::ZERO,
frame: 200,
};
let flow = step(&mut term, &mut app, &frame);
assert_eq!(flow, Flow::Exit); assert_eq!(app.frames, 1);
}
struct AlwaysIdle {
frames: u64,
}
impl App<Headless> for AlwaysIdle {
fn update(&mut self, _term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
self.frames += 1;
if frame.frame >= 5 {
Flow::Exit
} else {
Flow::Idle
}
}
}
#[test]
fn run_blocking_skips_present_on_idle() {
let term = Terminal::new(Headless::new(2, 1));
let app = AlwaysIdle { frames: 0 };
run_blocking(term, app).expect("run_blocking");
}
struct DrawsAndExits {
frames: u64,
exit_at: u64,
}
impl App<Headless> for DrawsAndExits {
fn update(&mut self, term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
self.frames += 1;
term.surface()
.put((0, 0), 'x', crate::style::Style::default());
if frame.frame >= self.exit_at {
Flow::Exit
} else {
Flow::Continue
}
}
}
#[test]
fn run_blocking_presents_automatically() {
let term = Terminal::new(Headless::new(2, 1));
let app = DrawsAndExits {
frames: 0,
exit_at: 0,
};
run_blocking(term, app).expect("run_blocking");
}
#[test]
fn run_blocking_with_default_options_matches_run_blocking() {
let term = Terminal::new(Headless::new(2, 1));
let app = DrawsAndExits {
frames: 0,
exit_at: 2,
};
run_blocking_with(term, app, RunOptions::default()).expect("run_blocking_with");
}
#[test]
fn run_blocking_with_paced_options_runs_to_completion() {
let term = Terminal::new(Headless::new(2, 1));
let app = DrawsAndExits {
frames: 0,
exit_at: 2,
};
run_blocking_with(term, app, RunOptions::paced(1000)).expect("run_blocking_with");
}
#[test]
fn run_options_paced_sets_max_fps() {
assert_eq!(RunOptions::paced(30).max_fps, Some(30));
assert_eq!(RunOptions::default().max_fps, None);
}
}