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;
}
#[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)]
#[non_exhaustive]
pub struct RunOptions {
target_fps: Option<u32>,
event_driven: bool,
idle_wake: Option<Duration>,
}
impl RunOptions {
#[must_use]
pub const fn animated(target_fps: u32) -> Self {
Self {
target_fps: Some(target_fps),
event_driven: false,
idle_wake: None,
}
}
#[must_use]
pub const fn with_target_fps(mut self, target_fps: u32) -> Self {
self.target_fps = Some(target_fps);
self
}
#[must_use]
pub const fn target_fps(&self) -> Option<u32> {
self.target_fps
}
#[must_use]
pub const fn event_driven(mut self, event_driven: bool) -> Self {
self.event_driven = event_driven;
self
}
#[must_use]
pub const fn is_event_driven(&self) -> bool {
self.event_driven
}
#[must_use]
pub const fn with_idle_wake(mut self, idle_wake: Duration) -> Self {
self.idle_wake = Some(idle_wake);
self
}
#[must_use]
pub const fn idle_wake(&self) -> Option<Duration> {
self.idle_wake
}
}
impl Default for RunOptions {
fn default() -> Self {
Self {
target_fps: None,
event_driven: true,
idle_wake: None,
}
}
}
#[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.target_fps().map(crate::frames::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 = app.update(&mut term, &frame);
if flow == Flow::Exit {
return Ok(());
}
if flow != Flow::Idle && term.present_count() == present_count_before {
term.present()?;
}
if flow == Flow::Idle && options.is_event_driven() {
term.wait_for_input(options.idle_wake().unwrap_or(Duration::MAX));
}
}
}
#[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::color::Style::default());
term.present().expect("present");
if term.has_input() || frame.frame >= 100 {
Flow::Exit
} else {
Flow::Continue
}
}
}
#[cfg(feature = "std")]
#[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");
}
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
}
}
}
#[cfg(feature = "std")]
#[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::color::Style::default());
if frame.frame >= self.exit_at {
Flow::Exit
} else {
Flow::Continue
}
}
}
#[cfg(feature = "std")]
#[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");
}
#[cfg(feature = "std")]
#[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");
}
#[cfg(feature = "std")]
#[test]
fn run_blocking_with_animated_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::animated(1000)).expect("run_blocking_with");
}
#[test]
fn run_options_animated_sets_fields() {
let animated = RunOptions::animated(30);
assert_eq!(animated.target_fps(), Some(30));
assert!(!animated.is_event_driven());
assert_eq!(animated.idle_wake(), None);
let default = RunOptions::default();
assert_eq!(default.target_fps(), None);
assert!(default.is_event_driven());
assert_eq!(default.idle_wake(), None);
}
#[test]
fn run_options_setters_override_defaults() {
let options = RunOptions::default()
.with_target_fps(60)
.event_driven(false)
.with_idle_wake(Duration::from_millis(250));
assert_eq!(options.target_fps(), Some(60));
assert!(!options.is_event_driven());
assert_eq!(options.idle_wake(), Some(Duration::from_millis(250)));
}
struct IdleThenExit {
frames: u64,
}
impl App<Headless> for IdleThenExit {
fn update(&mut self, _term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
self.frames += 1;
if frame.frame == 0 {
Flow::Idle
} else {
Flow::Exit
}
}
}
#[cfg(feature = "std")]
#[test]
fn run_blocking_with_non_event_driven_options_does_not_block_on_idle() {
let term = Terminal::new(Headless::new(2, 1));
let app = IdleThenExit { frames: 0 };
let options = RunOptions {
target_fps: None,
event_driven: false,
idle_wake: None,
};
run_blocking_with(term, app, options).expect("run_blocking_with");
}
struct ObservesQueuedEventAfterIdle {
frames: u64,
saw_input_after_idle: bool,
}
impl App<Headless> for ObservesQueuedEventAfterIdle {
fn update(&mut self, term: &mut Terminal<Headless>, frame: &Frame) -> Flow {
self.frames += 1;
if frame.frame == 0 {
return Flow::Idle;
}
self.saw_input_after_idle = term.has_input();
Flow::Exit
}
}
#[test]
fn run_blocking_event_driven_idle_wait_does_not_consume_the_waking_event() {
let mut backend = Headless::new(2, 1);
backend.push_event(Event::Key(KeyEvent::new(
KeyCode::Char('x'),
KeyModifiers::NONE,
)));
let term = Terminal::new(backend);
let mut app = ObservesQueuedEventAfterIdle {
frames: 0,
saw_input_after_idle: false,
};
let mut term = term;
let frame0 = Frame {
delta: Duration::ZERO,
frame: 0,
};
assert_eq!(app.update(&mut term, &frame0), Flow::Idle);
assert!(term.wait_for_input(Duration::MAX));
let frame1 = Frame {
delta: Duration::ZERO,
frame: 1,
};
assert_eq!(app.update(&mut term, &frame1), Flow::Exit);
assert!(app.saw_input_after_idle);
}
}