use std::time::{Duration, Instant};
const FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const THRESHOLD: Duration = Duration::from_millis(200);
#[derive(Debug, Default)]
pub struct Loader {
armed_at: Option<Instant>,
frame: usize,
pub label: String,
}
impl Loader {
pub fn new() -> Self {
Loader::default()
}
pub fn arm(&mut self, label: impl Into<String>) {
self.armed_at = Some(Instant::now());
self.frame = 0;
self.label = label.into();
}
pub fn disarm(&mut self) {
self.armed_at = None;
self.label.clear();
}
#[allow(dead_code)]
pub fn is_armed(&self) -> bool {
self.armed_at.is_some()
}
pub fn tick(&mut self) {
if self.armed_at.is_some() {
self.frame = (self.frame + 1) % FRAMES.len();
}
}
pub fn is_visible(&self) -> bool {
match self.armed_at {
Some(t) => t.elapsed() >= THRESHOLD,
None => false,
}
}
pub fn elapsed(&self) -> Option<Duration> {
self.armed_at.map(|armed_at| armed_at.elapsed())
}
pub fn elapsed_label(&self) -> String {
format_elapsed(self.elapsed().unwrap_or_default())
}
pub fn current_frame(&self) -> &'static str {
FRAMES[self.frame % FRAMES.len()]
}
#[cfg(test)]
fn force_armed_at(&mut self, instant: Instant) {
self.armed_at = Some(instant);
}
}
fn format_elapsed(duration: Duration) -> String {
let total_millis = duration.as_millis();
let minutes = total_millis / 60_000;
let seconds = (total_millis / 1_000) % 60;
let tenths = (total_millis / 100) % 10;
format!("{minutes:02}:{seconds:02}.{tenths}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn not_visible_right_after_arm() {
let mut l = Loader::new();
l.arm("rodando idea");
assert!(l.is_armed());
assert!(!l.is_visible(), "loader não deve aparecer antes de 200ms");
}
#[test]
fn visible_after_threshold() {
let mut l = Loader::new();
l.arm("rodando prd");
l.force_armed_at(Instant::now() - Duration::from_millis(250));
assert!(l.is_visible(), "loader deve aparecer após 200ms");
}
#[test]
fn disarm_hides() {
let mut l = Loader::new();
l.arm("x");
l.disarm();
assert!(!l.is_armed());
assert!(!l.is_visible());
}
#[test]
fn tick_advances_frame() {
let mut l = Loader::new();
l.arm("x");
let f0 = l.current_frame();
l.tick();
let f1 = l.current_frame();
assert_ne!(f0, f1);
}
#[test]
fn elapsed_label_is_human_readable() {
let mut l = Loader::new();
l.arm("x");
l.force_armed_at(Instant::now() - Duration::from_millis(65_432));
assert!(l.elapsed_label().starts_with("01:05."));
}
}