use a central animation loop (#2268)

instead of each shimmer needing to have its own animation thread, have
render_ref schedule a new frame if it wants one and coalesce to the
earliest next frame. this also makes the animations
frame-timing-independent, based on start time instead of frame count.
This commit is contained in:
Jeremy Rose
2025-08-14 16:59:47 -04:00
committed by GitHub
parent fd2b059504
commit 20cd61e2a4
5 changed files with 97 additions and 161 deletions

View File

@@ -1,51 +1,35 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::OnceLock;
use std::time::Duration;
use std::time::Instant;
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::text::Span;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
static PROCESS_START: OnceLock<Instant> = OnceLock::new();
#[derive(Debug)]
pub(crate) struct FrameTicker {
running: Arc<AtomicBool>,
fn elapsed_since_start() -> Duration {
let start = PROCESS_START.get_or_init(Instant::now);
start.elapsed()
}
impl FrameTicker {
pub(crate) fn new(app_event_tx: AppEventSender) -> Self {
let running = Arc::new(AtomicBool::new(true));
let running_clone = running.clone();
let app_event_tx_clone = app_event_tx.clone();
std::thread::spawn(move || {
while running_clone.load(Ordering::Relaxed) {
std::thread::sleep(Duration::from_millis(100));
app_event_tx_clone.send(AppEvent::RequestRedraw);
}
});
Self { running }
}
}
impl Drop for FrameTicker {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
}
}
pub(crate) fn shimmer_spans(text: &str, frame_idx: usize) -> Vec<Span<'static>> {
pub(crate) fn shimmer_spans(text: &str) -> Vec<Span<'static>> {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() {
return Vec::new();
}
// Use time-based sweep synchronized to process start.
let padding = 10usize;
let period = chars.len() + padding * 2;
let pos = frame_idx % period;
let sweep_seconds = 2.5f32;
let pos_f =
(elapsed_since_start().as_secs_f32() % sweep_seconds) / sweep_seconds * (period as f32);
let pos = pos_f as usize;
let has_true_color = supports_color::on_cached(supports_color::Stream::Stdout)
.map(|level| level.has_16m)
.unwrap_or(false);
let band_half_width = 6.0;
let band_half_width = 3.0;
let mut spans: Vec<Span<'static>> = Vec::with_capacity(chars.len());
for (i, ch) in chars.iter().enumerate() {