use alloc::string::String;
#[cfg(feature = "rand")]
use rand::Rng as _;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ThrobberState {
index: i8,
}
impl ThrobberState {
pub fn index(&self) -> i8 {
self.index
}
pub fn calc_next(&mut self) {
self.calc_step(1);
}
pub fn calc_step(&mut self, step: i8) {
self.index = if step == 0 {
#[cfg(feature = "rand")]
{
let mut rng = rand::rng();
rng.random()
}
#[cfg(all(not(feature = "rand"), feature = "std"))]
{
let duration = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
(duration.as_nanos() % 0x100
+ duration.as_micros() % 0x100
+ duration.as_millis() % 0x100) as i8
}
#[cfg(all(not(feature = "rand"), not(feature = "std")))]
{
self.index
}
} else {
self.index.checked_add(step).unwrap_or(0)
}
}
pub fn normalize(&mut self, throbber: &Throbber) {
let len = throbber.throbber_set.symbols.len() as i8;
if len <= 0 {
} else {
self.index %= len;
if self.index < 0 {
self.index += len;
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Throbber<'a> {
label: Option<ratatui::text::Span<'a>>,
style: ratatui::style::Style,
throbber_style: ratatui::style::Style,
throbber_set: crate::symbols::throbber::Set,
use_type: crate::symbols::throbber::WhichUse,
}
impl Default for Throbber<'_> {
fn default() -> Self {
Self {
label: None,
style: ratatui::style::Style::default(),
throbber_style: ratatui::style::Style::default(),
throbber_set: crate::symbols::throbber::BRAILLE_SIX,
use_type: crate::symbols::throbber::WhichUse::Spin,
}
}
}
impl<'a> Throbber<'a> {
pub fn label<T>(mut self, label: T) -> Self
where
T: Into<ratatui::text::Span<'a>>,
{
self.label = Some(label.into());
self
}
pub fn style(mut self, style: ratatui::style::Style) -> Self {
self.style = style;
self
}
pub fn throbber_style(mut self, style: ratatui::style::Style) -> Self {
self.throbber_style = style;
self
}
pub fn throbber_set(mut self, set: crate::symbols::throbber::Set) -> Self {
self.throbber_set = set;
self
}
pub fn use_type(mut self, use_type: crate::symbols::throbber::WhichUse) -> Self {
self.use_type = use_type;
self
}
pub fn to_symbol_span(&self, state: &ThrobberState) -> ratatui::text::Span<'a> {
let symbol = match self.use_type {
crate::symbols::throbber::WhichUse::Full => self.throbber_set.full,
crate::symbols::throbber::WhichUse::Empty => self.throbber_set.empty,
crate::symbols::throbber::WhichUse::Spin => {
let mut state = state.clone();
state.normalize(self);
let len = self.throbber_set.symbols.len() as i8;
if 0 <= state.index && state.index < len {
self.throbber_set.symbols[state.index as usize]
} else {
self.throbber_set.empty
}
}
};
let mut symbol_text = String::from(symbol);
symbol_text.push(' ');
let symbol_span =
ratatui::text::Span::styled(symbol_text, self.style).patch_style(self.throbber_style);
symbol_span
}
pub fn to_line(&self, state: &ThrobberState) -> ratatui::text::Line<'a> {
let mut line = ratatui::text::Line::default().style(self.style);
line.spans.push(self.to_symbol_span(state));
if let Some(label) = &self.label.clone() {
line.spans.push(label.clone());
}
line
}
}
impl ratatui::widgets::Widget for Throbber<'_> {
fn render(self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) {
let mut state = ThrobberState::default();
state.calc_step(0);
ratatui::widgets::StatefulWidget::render(self, area, buf, &mut state);
}
}
impl ratatui::widgets::StatefulWidget for Throbber<'_> {
type State = ThrobberState;
fn render(
self,
area: ratatui::layout::Rect,
buf: &mut ratatui::buffer::Buffer,
state: &mut Self::State,
) {
buf.set_style(area, self.style);
let throbber_area = area;
if throbber_area.height < 1 {
return;
}
let symbol = match self.use_type {
crate::symbols::throbber::WhichUse::Full => self.throbber_set.full,
crate::symbols::throbber::WhichUse::Empty => self.throbber_set.empty,
crate::symbols::throbber::WhichUse::Spin => {
state.normalize(&self);
let len = self.throbber_set.symbols.len() as i8;
if 0 <= state.index && state.index < len {
self.throbber_set.symbols[state.index as usize]
} else {
self.throbber_set.empty
}
}
};
let mut symbol_text = String::from(symbol);
symbol_text.push(' ');
let symbol_span = ratatui::text::Span::styled(symbol_text, self.throbber_style);
let (col, row) = buf.set_span(
throbber_area.left(),
throbber_area.top(),
&symbol_span,
symbol_span.width() as u16,
);
if let Some(label) = self.label {
if throbber_area.right() <= col {
return;
}
buf.set_span(col, row, &label, label.width() as u16);
}
}
}
impl<'a> From<Throbber<'a>> for ratatui::text::Span<'a> {
fn from(throbber: Throbber<'a>) -> ratatui::text::Span<'a> {
let mut state = ThrobberState::default();
state.calc_step(0);
throbber.to_symbol_span(&state)
}
}
impl<'a> From<Throbber<'a>> for ratatui::text::Line<'a> {
fn from(throbber: Throbber<'a>) -> ratatui::text::Line<'a> {
let mut state = ThrobberState::default();
state.calc_step(0);
throbber.to_line(&state)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(unused_variables, unused_assignments)]
fn throbber_state_calc_step() {
let mut throbber_state = ThrobberState::default();
assert_eq!(throbber_state.index(), 0);
let mut difference = false;
for _ in 0..100 {
throbber_state.calc_step(0);
assert!((i8::MIN..=i8::MAX).contains(&throbber_state.index()));
if 0 != throbber_state.index() {
difference = true;
}
}
#[cfg(any(feature = "rand", feature = "std"))]
assert!(difference);
}
#[test]
fn throbber_state_normalize() {
let mut throbber_state = ThrobberState::default();
let throbber = Throbber::default();
let len = throbber.throbber_set.symbols.len() as i8;
let max = len - 1;
throbber_state.calc_step(max);
throbber_state.normalize(&throbber);
assert_eq!(throbber_state.index(), max);
throbber_state.calc_next();
throbber_state.normalize(&throbber);
assert_eq!(throbber_state.index(), 0);
throbber_state.calc_step(-1);
throbber_state.normalize(&throbber);
assert_eq!(throbber_state.index(), max);
throbber_state.calc_step(len * -2);
throbber_state.normalize(&throbber);
assert_eq!(throbber_state.index(), max);
}
#[test]
fn throbber_converts_to_span() {
let throbber = Throbber::default().use_type(crate::symbols::throbber::WhichUse::Full);
let span: ratatui::text::Span = throbber.into();
assert_eq!(span.content, "⠿ ");
}
#[test]
fn throbber_converts_to_line() {
let throbber = Throbber::default().use_type(crate::symbols::throbber::WhichUse::Full);
let line: ratatui::text::Line = throbber.into();
assert_eq!(line.spans[0].content, "⠿ ");
}
#[test]
fn throbber_reaches_upper_limit_step_resets_to_zero() {
let mut throbber_state = ThrobberState::default();
for _ in 0..i8::MAX {
throbber_state.calc_next();
}
throbber_state.calc_next();
assert!(throbber_state.index() != i8::MAX);
}
}