use crate::{Event, Result, TerminalWindow, Window};
use std::time::{Duration, Instant};
const MAX_EVENTS_PER_FRAME: usize = 256;
const MAX_SKIPPED_TICKS: usize = 4;
fn advance_tick_deadline(deadline: Instant, interval: Duration, now: Instant) -> Instant {
let mut next = deadline + interval;
for _ in 0..MAX_SKIPPED_TICKS {
if next > now {
return next;
}
next += interval;
}
if next <= now { now + interval } else { next }
}
fn push_coalesced_event(events: &mut Vec<Event>, event: Event) {
match event {
Event::Unknown => {}
Event::MouseMove { x, y } => {
if let Some(Event::MouseMove {
x: previous_x,
y: previous_y,
}) = events.last_mut()
{
*previous_x = x;
*previous_y = y;
} else {
events.push(Event::MouseMove { x, y });
}
}
Event::Resize { width, height } => {
if let Some(Event::Resize {
width: previous_width,
height: previous_height,
}) = events.last_mut()
{
*previous_width = width;
*previous_height = height;
} else {
events.push(Event::Resize { width, height });
}
}
Event::MouseScroll { delta } => {
if let Some(Event::MouseScroll {
delta: previous_delta,
}) = events.last_mut()
{
*previous_delta = previous_delta.saturating_add(delta);
} else {
events.push(Event::MouseScroll { delta });
}
}
Event::MouseScrollHorizontal { delta } => {
if let Some(Event::MouseScrollHorizontal {
delta: previous_delta,
}) = events.last_mut()
{
*previous_delta = previous_delta.saturating_add(delta);
} else {
events.push(Event::MouseScrollHorizontal { delta });
}
}
event => events.push(event),
}
}
#[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 {
assert!(
!frame_rate.is_zero(),
"frame rate interval must be non-zero"
);
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 frame_start = Instant::now();
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();
if let Some(hook) = self.frame_profile_hook.as_mut() {
hook(&FrameProfile {
input_poll_time: Duration::ZERO,
update_time: Duration::ZERO,
draw_time,
frame_time,
events_processed: 0,
budget: self.frame_budget,
over_budget: self.frame_budget.is_some_and(|budget| frame_time > budget),
});
}
let mut next_tick = self.frame_rate.map(|interval| Instant::now() + interval);
let mut pending_events = Vec::with_capacity(MAX_EVENTS_PER_FRAME);
loop {
let input_poll_start = Instant::now();
let first_event = match next_tick {
Some(deadline) => {
let timeout = deadline.saturating_duration_since(Instant::now());
self.window.poll_input_timeout(timeout)?
}
None => Some(self.window.wait_for_input()?),
};
let mut input_poll_time = input_poll_start.elapsed();
let frame_start = Instant::now();
pending_events.clear();
let mut raw_events_read = 0usize;
if let Some(event) = first_event {
raw_events_read = 1;
push_coalesced_event(&mut pending_events, event);
}
while raw_events_read < MAX_EVENTS_PER_FRAME {
let poll_start = Instant::now();
let event = self.window.poll_input()?;
input_poll_time = input_poll_time.saturating_add(poll_start.elapsed());
let Some(event) = event else {
break;
};
raw_events_read += 1;
push_coalesced_event(&mut pending_events, event);
}
let events_processed = pending_events.len();
let mut update_time = Duration::ZERO;
for event in pending_events.drain(..) {
let update_start = Instant::now();
if !update(&mut self.state, event) {
return Ok(());
}
update_time = update_time.saturating_add(update_start.elapsed());
}
let now = Instant::now();
let tick_due = next_tick.is_some_and(|deadline| now >= deadline);
if tick_due {
let update_start = Instant::now();
if !update(&mut self.state, Event::Frame) {
return Ok(());
}
update_time = update_time.saturating_add(update_start.elapsed());
let interval = self
.frame_rate
.expect("tick deadline requires a frame rate");
let deadline = next_tick.expect("tick deadline must exist");
next_tick = Some(advance_tick_deadline(deadline, interval, now));
}
if events_processed == 0 && !tick_due {
continue;
}
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,
});
}
}
}
}