use std::time::Duration;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use super::Engine;
use crate::runtime::App;
#[derive(Debug, Default, Clone, Copy)]
pub(super) struct Pacing {
last: Option<Duration>,
urgent: bool,
}
impl<A: App> Engine<A> {
pub(super) fn frame_is_urgent(&mut self) {
self.pacing.urgent = true;
}
pub(super) fn frame_drawn(&mut self, now: Duration) {
self.pacing.last = Some(now);
self.pacing.urgent = false;
}
pub(super) fn catch_up(&mut self, now: Duration) {
let Some(size) = self.screen.filter(|_| self.dirty && self.tree.is_some()) else {
return;
};
let pacing = self.pacing;
let mut unseen = Buffer::empty(Rect::new(0, 0, size.width, size.height));
self.render(&mut unseen, now);
self.pacing = pacing;
self.dirty = true;
}
fn frame_wanted(&self, now: Duration) -> bool {
self.dirty || self.deadline().is_some_and(|deadline| deadline <= now)
}
pub(crate) fn frame_due(&self, now: Duration) -> bool {
if !self.frame_wanted(now) {
return false;
}
if self.pacing.urgent {
return true;
}
match (self.frame_gap(), self.pacing.last) {
(Some(gap), Some(last)) => now.saturating_sub(last) >= gap,
_ => true,
}
}
pub(crate) fn frame_deadline(&self, now: Duration) -> Option<Duration> {
if self.pacing.urgent || !self.frame_wanted(now) {
return None;
}
let at = self.pacing.last?.checked_add(self.frame_gap()?)?;
(at > now).then_some(at)
}
fn frame_gap(&self) -> Option<Duration> {
self.app.frame_limit().gap(self.env.remote())
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use super::super::{Engine, TaskMode};
use crate::env::Env;
use crate::event::{Event, KeyEvent};
use crate::runtime::{App, Command, FrameLimit};
use crate::widget::View;
use crate::widgets::Text;
const MILLISECOND: Duration = Duration::from_millis(1);
struct Program {
limit: FrameLimit,
lines: usize,
}
impl App for Program {
type Msg = ();
fn update(&mut self, (): ()) -> Command<()> {
self.lines += 1;
Command::none()
}
fn frame_limit(&self) -> FrameLimit {
self.limit
}
fn view(&self, ui: &mut View<'_, ()>) {
ui.add(Text::new(format!("{} lines", self.lines)));
}
}
fn running(limit: FrameLimit) -> (Engine<Program>, Buffer) {
let mut engine = Engine::new(Program { limit, lines: 0 }, Env::builtin(), TaskMode::Inline);
let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 1));
engine.render(&mut buffer, Duration::ZERO);
(engine, buffer)
}
fn frames_in_a_second(limit: FrameLimit) -> usize {
let (mut engine, mut buffer) = running(limit);
let mut frames = 0;
for step in 1..=1000 {
let now = step * MILLISECOND;
engine.update(());
if engine.frame_due(now) {
engine.render(&mut buffer, now);
frames += 1;
}
}
frames
}
#[test]
fn a_program_writing_without_pause_is_drawn_at_the_limit_and_no_more() {
assert_eq!(frames_in_a_second(FrameLimit::per_second(20)), 20, "20 frames a second");
assert_eq!(frames_in_a_second(FrameLimit::per_second(10)), 10);
assert!(frames_in_a_second(FrameLimit::default()) <= 60, "the default, on a local connection");
assert_eq!(frames_in_a_second(FrameLimit::default()), 58);
}
#[test]
fn without_a_limit_every_wanted_frame_is_drawn() {
assert_eq!(frames_in_a_second(FrameLimit::none()), 1000);
}
#[test]
fn the_frame_after_a_key_is_drawn_at_once_while_the_limit_holds_the_others_back() {
let (mut engine, mut buffer) = running(FrameLimit::per_second(20));
engine.update(());
assert!(!engine.frame_due(MILLISECOND), "a line the program wrote waits for the gap");
engine.handle(Event::Key(KeyEvent::press("x")), MILLISECOND);
assert!(engine.frame_due(MILLISECOND), "the character the user typed is echoed at once");
engine.render(&mut buffer, MILLISECOND);
engine.update(());
assert!(!engine.frame_due(2 * MILLISECOND), "the limit counts from the frame just drawn");
assert!(engine.frame_due(Duration::from_millis(51)), "a gap after it");
}
#[test]
fn a_frame_built_between_two_events_is_not_a_drawn_frame() {
let (mut engine, _) = running(FrameLimit::per_second(20));
engine.update(());
engine.handle(Event::PointerOutside, 10 * MILLISECOND);
assert!(engine.frame_due(Duration::from_millis(50)), "the gap still counts from the frame drawn at zero");
assert!(!engine.frame_due(Duration::from_millis(49)), "and the limit still holds");
}
#[test]
fn the_loop_wakes_when_a_held_frame_may_be_drawn_and_not_while_nothing_waits() {
let (mut engine, mut buffer) = running(FrameLimit::per_second(20));
assert_eq!(engine.frame_deadline(MILLISECOND), None, "nothing is waiting to be drawn");
engine.update(());
assert_eq!(engine.frame_deadline(MILLISECOND), Some(Duration::from_millis(50)), "the rest of the gap");
engine.handle(Event::Key(KeyEvent::press("x")), MILLISECOND);
assert_eq!(engine.frame_deadline(MILLISECOND), None, "input is not waited for");
engine.render(&mut buffer, MILLISECOND);
assert_eq!(engine.frame_deadline(MILLISECOND), None, "the frame was drawn");
}
#[test]
fn the_first_frame_of_a_run_is_never_held_back() {
let mut engine =
Engine::new(Program { limit: FrameLimit::per_second(1), lines: 0 }, Env::builtin(), TaskMode::Inline);
assert!(engine.frame_due(Duration::ZERO), "a second before the first frame would be an empty screen");
engine.render(&mut Buffer::empty(Rect::new(0, 0, 20, 1)), Duration::ZERO);
engine.update(());
assert!(!engine.frame_due(Duration::from_millis(999)));
assert!(engine.frame_due(Duration::from_secs(1)));
}
}