Files
llmx/codex-rs/tui/src/ascii_animation.rs
2025-09-27 13:25:09 -07:00

112 lines
3.0 KiB
Rust

use std::convert::TryFrom;
use std::time::Duration;
use std::time::Instant;
use rand::Rng as _;
use crate::frames::ALL_VARIANTS;
use crate::frames::FRAME_TICK_DEFAULT;
use crate::tui::FrameRequester;
/// Drives ASCII art animations shared across popups and onboarding widgets.
pub(crate) struct AsciiAnimation {
request_frame: FrameRequester,
variants: &'static [&'static [&'static str]],
variant_idx: usize,
frame_tick: Duration,
start: Instant,
}
impl AsciiAnimation {
pub(crate) fn new(request_frame: FrameRequester) -> Self {
Self::with_variants(request_frame, ALL_VARIANTS, 0)
}
pub(crate) fn with_variants(
request_frame: FrameRequester,
variants: &'static [&'static [&'static str]],
variant_idx: usize,
) -> Self {
assert!(
!variants.is_empty(),
"AsciiAnimation requires at least one animation variant",
);
let clamped_idx = variant_idx.min(variants.len() - 1);
Self {
request_frame,
variants,
variant_idx: clamped_idx,
frame_tick: FRAME_TICK_DEFAULT,
start: Instant::now(),
}
}
pub(crate) fn schedule_next_frame(&self) {
let tick_ms = self.frame_tick.as_millis();
if tick_ms == 0 {
self.request_frame.schedule_frame();
return;
}
let elapsed_ms = self.start.elapsed().as_millis();
let rem_ms = elapsed_ms % tick_ms;
let delay_ms = if rem_ms == 0 {
tick_ms
} else {
tick_ms - rem_ms
};
if let Ok(delay_ms_u64) = u64::try_from(delay_ms) {
self.request_frame
.schedule_frame_in(Duration::from_millis(delay_ms_u64));
} else {
self.request_frame.schedule_frame();
}
}
pub(crate) fn current_frame(&self) -> &'static str {
let frames = self.frames();
if frames.is_empty() {
return "";
}
let tick_ms = self.frame_tick.as_millis();
if tick_ms == 0 {
return frames[0];
}
let elapsed_ms = self.start.elapsed().as_millis();
let idx = ((elapsed_ms / tick_ms) % frames.len() as u128) as usize;
frames[idx]
}
pub(crate) fn pick_random_variant(&mut self) -> bool {
if self.variants.len() <= 1 {
return false;
}
let mut rng = rand::rng();
let mut next = self.variant_idx;
while next == self.variant_idx {
next = rng.random_range(0..self.variants.len());
}
self.variant_idx = next;
self.request_frame.schedule_frame();
true
}
#[allow(dead_code)]
pub(crate) fn request_frame(&self) {
self.request_frame.schedule_frame();
}
fn frames(&self) -> &'static [&'static str] {
self.variants[self.variant_idx]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_tick_must_be_nonzero() {
assert!(FRAME_TICK_DEFAULT.as_millis() > 0);
}
}