use std::sync::LazyLock;
use std::time::{Duration, Instant};
use ratatui::buffer::{Buffer, Cell};
use ratatui::layout::Rect;
use crate::ui::config::UI;
use crate::ui::element::EventListener;
type Ms = u64;
static START: LazyLock<Instant> = LazyLock::new(Instant::now);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct Tick {
t: Ms,
dt: Ms,
}
impl Tick {
pub fn new(dt: Ms) -> Self {
Self {
t: Ms::try_from(START.elapsed().as_millis()).unwrap_or(Ms::MAX),
dt,
}
}
pub fn at(t: Ms, dt: Ms) -> Self {
Self { t, dt }
}
pub const fn t(self) -> Ms {
self.t
}
pub const fn dt(self) -> Ms {
self.dt
}
}
pub struct Beat {
interval: Duration,
origin: Instant,
next: Instant,
last: Instant,
}
impl Beat {
pub fn new(interval: Duration) -> Self {
assert!(
!interval.is_zero(),
"a beat needs a positive interval; zero is infinite frequency"
);
let now = Instant::now();
Self {
interval,
origin: now,
next: now + interval,
last: now,
}
}
pub fn wait(&self) -> Duration {
self.wait_at(Instant::now())
}
pub fn strike(&mut self) -> Option<Tick> {
self.strike_at(Instant::now())
}
fn wait_at(&self, now: Instant) -> Duration {
self.next.saturating_duration_since(now)
}
fn strike_at(&mut self, now: Instant) -> Option<Tick> {
if now < self.next {
return None;
}
let dt = now.duration_since(self.last);
self.last = now;
let late = now.duration_since(self.next).as_nanos();
let skipped = late / self.interval.as_nanos() + 1;
self.next += self.interval * u32::try_from(skipped).unwrap_or(u32::MAX);
Some(Tick::at(
millis(now.duration_since(self.origin)),
millis(dt),
))
}
}
fn millis(span: Duration) -> Ms {
Ms::try_from(span.as_millis()).unwrap_or(Ms::MAX)
}
pub fn busy_frame(tick: &Tick) -> &'static str {
let frames = UI.spinner.active_frames();
let index = tick.t() / UI.spinner.frame_ms;
frames[usize::try_from(index).unwrap_or_default() % frames.len()]
}
pub trait SpinningRod<Output = Cell> {
fn call(&self, tick: &Tick) -> Output;
}
pub struct ExpSpinner<R: SpinningRod> {
fish: Buffer,
rod: R,
}
impl<R: SpinningRod> ExpSpinner<R> {
pub fn new(rod: R) -> Self {
Self {
fish: Buffer::empty(Rect::new(0, 0, 1, 1)),
rod,
}
}
pub fn buffer(&self) -> &Buffer {
&self.fish
}
}
impl<R: SpinningRod> EventListener<Tick> for ExpSpinner<R> {
fn on_event(&mut self, tick: &Tick) {
let stock_fish = self.rod.call(tick);
self.fish[(0, 0)] = stock_fish;
}
}
pub struct BufferedSpinner<R: Iterator<Item = (Cell, Ms)>> {
fish: Buffer,
queue: R,
buffered: Option<Ms>,
}
impl<R: Iterator<Item = (Cell, Ms)>> BufferedSpinner<R> {
pub fn new(queue: R) -> Self {
Self {
fish: Buffer::empty(Rect::new(0, 0, 1, 1)),
queue,
buffered: Some(0),
}
}
pub fn buffer(&self) -> &Buffer {
&self.fish
}
pub fn spinning(&self) -> bool {
self.buffered.is_some()
}
pub fn next(&mut self) {
match self.queue.next() {
Some((stock_fish, dt)) => {
self.buffered = Some(dt);
self.fish[(0, 0)] = stock_fish;
}
None => self.buffered = None,
}
}
}
impl<R: Iterator<Item = (Cell, Ms)>> EventListener<Tick> for BufferedSpinner<R> {
fn on_event(&mut self, tick: &Tick) {
if let Some(held) = self.buffered {
if tick.dt >= held {
self.next();
} else {
self.buffered = Some(held - tick.dt);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const FRAMES: [char; 4] = ['|', '/', '-', '\\'];
#[test]
fn the_busy_marker_advances_with_the_clock_and_wraps() {
let frames = UI.spinner.active_frames();
let at = |frame: usize| busy_frame(&Tick::at(frame as Ms * UI.spinner.frame_ms, 0));
assert_ne!(at(0), at(1));
assert_eq!(at(0), frames[0]);
assert_eq!(at(frames.len()), frames[0]);
assert_eq!(at(frames.len() + 1), frames[1]);
}
struct Clockwise {
period: Ms,
}
impl SpinningRod for Clockwise {
fn call(&self, tick: &Tick) -> Cell {
let mut cell = Cell::EMPTY;
cell.set_char(FRAMES[(tick.t() / self.period) as usize % FRAMES.len()]);
cell
}
}
fn frame(buffer: &Buffer) -> char {
buffer[(0, 0)].symbol().chars().next().unwrap_or(' ')
}
fn scripted() -> BufferedSpinner<impl Iterator<Item = (Cell, Ms)>> {
BufferedSpinner::new(FRAMES.into_iter().map(|glyph| {
let mut cell = Cell::EMPTY;
cell.set_char(glyph);
(cell, 100)
}))
}
const STALLED: [(Ms, Ms); 4] = [(100, 100), (200, 100), (700, 500), (800, 100)];
#[test]
fn evaluating_the_clock_cannot_drift_however_the_ticks_arrive() {
let mut steady = ExpSpinner::new(Clockwise { period: 100 });
let mut stuttering = ExpSpinner::new(Clockwise { period: 100 });
for step in 1..=7 {
steady.on_event(&Tick::at(step * 100, 100));
}
stuttering.on_event(&Tick::at(50, 50));
stuttering.on_event(&Tick::at(700, 650));
assert_eq!(frame(steady.buffer()), frame(stuttering.buffer()));
assert_eq!(frame(steady.buffer()), '\\');
}
#[test]
fn a_stall_costs_the_clock_its_frames_and_the_queue_its_timing() {
let mut clockwise = ExpSpinner::new(Clockwise { period: 100 });
let mut queued = scripted();
let (mut heard_clockwise, mut heard_queued) = (Vec::new(), Vec::new());
for (t, dt) in STALLED {
let tick = Tick::at(t, dt);
clockwise.on_event(&tick);
queued.on_event(&tick);
heard_clockwise.push(frame(clockwise.buffer()));
heard_queued.push(frame(queued.buffer()));
}
assert_eq!(heard_clockwise, ['/', '-', '\\', '|']);
for (t, _) in STALLED {
let due = FRAMES[(t / 100) as usize % FRAMES.len()];
assert!(heard_clockwise.contains(&due), "{due} was never on screen");
}
assert_eq!(heard_queued, ['|', '/', '-', '\\']);
}
#[test]
fn a_stalled_queue_waits_rather_than_skipping_ahead() {
let mut spinner = scripted();
spinner.on_event(&Tick::at(500, 500));
assert_eq!(frame(spinner.buffer()), '|');
assert!(spinner.spinning());
}
#[test]
fn a_scripted_queue_stops_when_its_frames_run_out() {
let mut spinner = scripted();
for step in 1..=5 {
spinner.on_event(&Tick::at(step * 100, 100));
}
assert!(!spinner.spinning());
assert_eq!(frame(spinner.buffer()), '\\');
}
#[test]
fn time_shorter_than_the_held_frame_changes_nothing() {
let mut spinner = scripted();
spinner.on_event(&Tick::at(100, 100));
spinner.on_event(&Tick::at(199, 99));
assert_eq!(frame(spinner.buffer()), '|');
assert!(spinner.spinning());
}
#[test]
fn a_frame_shorter_than_a_tick_still_gets_the_screen() {
let script = [('|', 100), ('!', 1), ('-', 100)];
let mut spinner = BufferedSpinner::new(script.into_iter().map(|(glyph, span)| {
let mut cell = Cell::EMPTY;
cell.set_char(glyph);
(cell, span as Ms)
}));
let mut heard = Vec::new();
for step in 1..=12 {
spinner.on_event(&Tick::at(step * 16, 16));
heard.push(frame(spinner.buffer()));
}
assert!(heard.contains(&'!'), "the accent was skipped: {heard:?}");
assert_eq!(heard.iter().filter(|glyph| **glyph == '!').count(), 1);
}
#[test]
fn a_beat_waits_out_its_interval_before_striking() {
let mut beat = Beat::new(Duration::from_millis(50));
let start = beat.next - Duration::from_millis(50);
assert!(beat.strike_at(start + Duration::from_millis(49)).is_none());
assert_eq!(
beat.wait_at(start + Duration::from_millis(20)),
Duration::from_millis(30)
);
assert!(beat.strike_at(start + Duration::from_millis(50)).is_some());
}
#[test]
fn an_overrun_skips_whole_beats_and_keeps_the_phase() {
let mut beat = Beat::new(Duration::from_millis(50));
let start = beat.next - Duration::from_millis(50);
let tick = beat
.strike_at(start + Duration::from_millis(175))
.expect("a beat was due");
assert_eq!(tick.dt(), 175, "the delta is measured, not nominal");
assert_eq!(beat.next.duration_since(start), Duration::from_millis(200));
}
#[test]
fn successive_beats_stay_on_the_grid_however_late_each_arrives() {
let mut beat = Beat::new(Duration::from_millis(50));
let start = beat.next - Duration::from_millis(50);
for (arrival, grid) in [(52, 100), (137, 150), (151, 200)] {
beat.strike_at(start + Duration::from_millis(arrival))
.expect("a beat was due");
assert_eq!(
beat.next.duration_since(start),
Duration::from_millis(grid),
"arriving at {arrival}ms should leave the grid at {grid}ms"
);
}
}
}