use crate::render::Cell;
use crate::style::Color;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum SpinnerStyle {
#[default]
Dots,
Line,
Circle,
Arrow,
Box,
Bounce,
}
impl SpinnerStyle {
pub fn frames(&self) -> &'static [&'static str] {
match self {
SpinnerStyle::Dots => &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
SpinnerStyle::Line => &["|", "/", "-", "\\"],
SpinnerStyle::Circle => &["◐", "◓", "◑", "◒"],
SpinnerStyle::Arrow => &["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
SpinnerStyle::Box => &["▖", "▘", "▝", "▗"],
SpinnerStyle::Bounce => &["⠁", "⠂", "⠄", "⠂"],
}
}
}
pub struct Spinner {
style: SpinnerStyle,
frame: usize,
label: Option<String>,
fg: Option<Color>,
props: WidgetProps,
}
impl Spinner {
pub fn new() -> Self {
Self {
style: SpinnerStyle::default(),
frame: 0,
label: None,
fg: Some(Color::CYAN),
props: WidgetProps::new(),
}
}
pub fn style(mut self, style: SpinnerStyle) -> Self {
self.style = style;
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
pub fn frame(&self) -> usize {
self.frame
}
pub fn tick(&mut self) {
let frames = self.style.frames();
self.frame = (self.frame + 1) % frames.len();
}
pub fn reset(&mut self) {
self.frame = 0;
}
pub fn set_frame(&mut self, frame: usize) {
let frames = self.style.frames();
self.frame = frame % frames.len();
}
}
impl Default for Spinner {
fn default() -> Self {
Self::new()
}
}
impl View for Spinner {
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width == 0 || area.height == 0 {
return;
}
let frames = self.style.frames();
let current_frame = frames[self.frame % frames.len()];
if let Some(ch) = current_frame.chars().next() {
let mut cell = Cell::new(ch);
cell.fg = self.fg;
ctx.set(0, 0, cell);
}
if let Some(ref label) = self.label {
let mut x: u16 = 2; for ch in label.chars() {
if x >= area.width {
break;
}
let cell = Cell::new(ch);
ctx.set(x, 0, cell);
x += 1;
}
}
}
crate::impl_view_meta!("Spinner");
}
impl_styled_view!(Spinner);
impl_props_builders!(Spinner);
pub fn spinner() -> Spinner {
Spinner::new()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Rect;
use crate::render::Buffer;
#[test]
fn test_spinner_new() {
let s = Spinner::new();
assert_eq!(s.frame(), 0);
}
#[test]
fn test_spinner_tick() {
let mut s = Spinner::new();
s.tick();
assert_eq!(s.frame(), 1);
s.tick();
assert_eq!(s.frame(), 2);
}
#[test]
fn test_spinner_reset() {
let mut s = Spinner::new();
s.tick();
s.tick();
s.reset();
assert_eq!(s.frame(), 0);
}
#[test]
fn test_spinner_styles() {
let _ = Spinner::new().style(SpinnerStyle::Dots);
let _ = Spinner::new().style(SpinnerStyle::Line);
let _ = Spinner::new().style(SpinnerStyle::Bounce);
}
#[test]
fn test_spinner_render_no_panic() {
let mut buf = Buffer::new(10, 1);
let area = Rect::new(0, 0, 10, 1);
let mut ctx = RenderContext::new(&mut buf, area);
let s = Spinner::new().label("Loading...");
s.render(&mut ctx);
}
#[test]
fn test_spinner_helper() {
let s = spinner();
assert_eq!(s.frame(), 0);
}
}