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)]
#[non_exhaustive]
pub struct Frame {
pub delta: Duration,
pub frame: u64,
}
impl Frame {
#[must_use]
pub const fn new(delta: Duration, frame: u64) -> Self {
Self { delta, frame }
}
}
pub trait App<B: Backend> {
fn init(&mut self, _term: &mut Terminal<B>) {}
fn update(&mut self, term: &mut Terminal<B>, frame: &Frame) -> Flow;
}
#[cfg(feature = "std")]
pub fn run_on<B, A>(term: Terminal<B>, app: A) -> Result<(), B::Error>
where
B: Backend,
A: App<B>,
{
run_on_with(term, app, RunOptions::default())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Idle {
Block {
wake: Option<Duration>,
},
Spin,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct RunOptions {
target_fps: Option<u32>,
idle: Idle,
}
impl RunOptions {
#[must_use]
pub const fn animated(target_fps: u32) -> Self {
Self {
target_fps: Some(target_fps),
idle: Idle::Spin,
}
}
#[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.idle = if event_driven {
Idle::Block { wake: None }
} else {
Idle::Spin
};
self
}
#[must_use]
pub const fn is_event_driven(&self) -> bool {
matches!(self.idle, Idle::Block { .. })
}
#[must_use]
pub const fn with_idle_wake(mut self, idle_wake: Duration) -> Self {
self.idle = Idle::Block {
wake: Some(idle_wake),
};
self
}
#[must_use]
pub const fn idle_wake(&self) -> Option<Duration> {
match self.idle {
Idle::Block { wake } => wake,
Idle::Spin => None,
}
}
#[must_use]
pub const fn idle(&self) -> Idle {
self.idle
}
}
impl Default for RunOptions {
fn default() -> Self {
Self {
target_fps: None,
idle: Idle::Block { wake: None },
}
}
}
#[cfg(feature = "std")]
pub fn run_on_with<B, A>(
mut term: Terminal<B>,
mut app: A,
options: RunOptions,
) -> Result<(), B::Error>
where
B: Backend,
A: App<B>,
{
app.init(&mut term);
let frame_budget = options
.target_fps()
.filter(|&fps| fps != 0)
.map(|fps| Duration::from_secs_f64(1.0 / f64::from(fps)));
let mut frame_count = 0u64;
let mut last = std::time::Instant::now();
loop {
if let Some(budget) = frame_budget
&& let Some(remaining) = budget.checked_sub(last.elapsed())
{
std::thread::sleep(remaining);
}
let now = std::time::Instant::now();
let delta = now.duration_since(last);
last = now;
let frame = Frame::new(delta, 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
&& let Idle::Block { wake } = options.idle()
{
term.wait_for_input(wake.unwrap_or(Duration::MAX));
}
}
}
#[cfg(feature = "std")]
pub fn run<B, A>(backend: B, app: A) -> Result<(), B::Error>
where
B: Backend,
A: App<B>,
{
run_on(Terminal::new(backend), app)
}
pub trait Launch {
type Backend: Backend;
type Error;
fn launch<A>(self, app: A, options: RunOptions) -> Result<(), Self::Error>
where
A: App<Self::Backend> + 'static;
}
#[cfg(feature = "std")]
pub fn run_with<B, A>(backend: B, app: A, options: RunOptions) -> Result<(), B::Error>
where
B: Backend,
A: App<B>,
{
run_on_with(Terminal::new(backend), app, options)
}
#[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_on_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_on(term, app).expect("run_on");
}
struct RecordsInit<'a> {
recorded_size: &'a mut Option<crate::grid::Size>,
init_calls: &'a mut u32,
update_ran_before_init: &'a mut bool,
}
impl App<Headless> for RecordsInit<'_> {
fn init(&mut self, term: &mut Terminal<Headless>) {
*self.init_calls += 1;
*self.recorded_size = Some(term.size());
}
fn update(&mut self, _term: &mut Terminal<Headless>, _frame: &Frame) -> Flow {
if *self.init_calls == 0 {
*self.update_ran_before_init = true;
}
Flow::Exit
}
}
#[cfg(feature = "std")]
#[test]
fn run_on_calls_init_once_before_the_first_update_with_the_real_size() {
let mut recorded_size = None;
let mut init_calls = 0;
let mut update_ran_before_init = false;
let term = Terminal::new(Headless::new(9, 5));
run_on(
term,
RecordsInit {
recorded_size: &mut recorded_size,
init_calls: &mut init_calls,
update_ran_before_init: &mut update_ran_before_init,
},
)
.expect("run_on");
assert_eq!(recorded_size, Some(crate::grid::Size::new(9, 5)));
assert_eq!(init_calls, 1);
assert!(!update_ran_before_init);
}
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_on_skips_present_on_idle() {
let term = Terminal::new(Headless::new(2, 1));
let app = AlwaysIdle { frames: 0 };
run_on(term, app).expect("run_on");
}
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_on_presents_automatically() {
let term = Terminal::new(Headless::new(2, 1));
let app = DrawsAndExits {
frames: 0,
exit_at: 0,
};
run_on(term, app).expect("run_on");
}
#[cfg(feature = "std")]
#[test]
fn run_on_with_default_options_matches_run_on() {
let term = Terminal::new(Headless::new(2, 1));
let app = DrawsAndExits {
frames: 0,
exit_at: 2,
};
run_on_with(term, app, RunOptions::default()).expect("run_on_with");
}
#[cfg(feature = "std")]
#[test]
fn run_on_with_animated_options_runs_to_completion() {
let term = Terminal::new(Headless::new(2, 1));
let app = DrawsAndExits {
frames: 0,
exit_at: 2,
};
run_on_with(term, app, RunOptions::animated(1000)).expect("run_on_with");
}
#[cfg(feature = "std")]
#[test]
fn run_on_with_animated_options_paces_updates_to_the_target_fps() {
let term = Terminal::new(Headless::new(2, 1));
let app = DrawsAndExits {
frames: 0,
exit_at: 4,
};
let start = std::time::Instant::now();
run_on_with(term, app, RunOptions::animated(50)).expect("run_on_with");
assert!(start.elapsed() >= Duration::from_millis(4 * 20));
}
#[cfg(feature = "std")]
#[test]
fn run_on_with_target_fps_zero_runs_uncapped_instead_of_panicking() {
let term = Terminal::new(Headless::new(2, 1));
let app = DrawsAndExits {
frames: 0,
exit_at: 2,
};
run_on_with(term, app, RunOptions::animated(0)).expect("run_on_with");
}
#[cfg(feature = "std")]
#[test]
fn run_builds_the_terminal_and_exits_on_flow_exit() {
let mut backend = Headless::new(4, 1);
backend.push_event(Event::Key(KeyEvent::new(
KeyCode::Char('q'),
KeyModifiers::NONE,
)));
let app = Counter { frames: 0 };
run(backend, app).expect("run");
}
#[cfg(feature = "std")]
#[test]
fn run_with_builds_the_terminal_and_honors_options() {
let backend = Headless::new(2, 1);
let app = DrawsAndExits {
frames: 0,
exit_at: 2,
};
run_with(backend, app, RunOptions::animated(1000)).expect("run_with");
}
#[test]
fn run_options_animated_sets_fields() {
let animated = RunOptions::animated(30);
assert_eq!(animated.target_fps(), Some(30));
assert_eq!(animated.idle(), Idle::Spin);
assert!(!animated.is_event_driven());
assert_eq!(animated.idle_wake(), None);
let default = RunOptions::default();
assert_eq!(default.target_fps(), None);
assert_eq!(default.idle(), Idle::Block { wake: 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_eq!(
options.idle(),
Idle::Block {
wake: Some(Duration::from_millis(250))
}
);
assert!(options.is_event_driven());
assert_eq!(options.idle_wake(), Some(Duration::from_millis(250)));
}
#[test]
fn run_options_event_driven_false_switches_to_spin() {
let options = RunOptions::default().event_driven(false);
assert_eq!(options.idle(), Idle::Spin);
assert!(!options.is_event_driven());
assert_eq!(options.idle_wake(), None);
}
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_on_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,
idle: Idle::Spin,
};
run_on_with(term, app, options).expect("run_on_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_on_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::new(Duration::ZERO, 0);
assert_eq!(app.update(&mut term, &frame0), Flow::Idle);
assert!(term.wait_for_input(Duration::MAX));
let frame1 = Frame::new(Duration::ZERO, 1);
assert_eq!(app.update(&mut term, &frame1), Flow::Exit);
assert!(app.saw_input_after_idle);
}
}