use crate::buffer::ScreenBuffer;
use crate::cell::Cell;
use crate::geometry::Rect;
use crate::style::Style;
use super::{BorderStyle, Widget};
#[derive(Clone, Debug, PartialEq)]
pub enum ProgressMode {
Determinate(f32),
Indeterminate {
phase: usize,
},
}
pub struct ProgressBar {
mode: ProgressMode,
filled_style: Style,
empty_style: Style,
label_style: Style,
show_percentage: bool,
border: BorderStyle,
}
const WAVE_CHARS: &[&str] = &["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
impl ProgressBar {
pub fn new(progress: f32) -> Self {
Self {
mode: ProgressMode::Determinate(progress.clamp(0.0, 1.0)),
filled_style: Style::default().reverse(true),
empty_style: Style::default(),
label_style: Style::default(),
show_percentage: true,
border: BorderStyle::None,
}
}
pub fn indeterminate() -> Self {
Self {
mode: ProgressMode::Indeterminate { phase: 0 },
filled_style: Style::default().reverse(true),
empty_style: Style::default(),
label_style: Style::default(),
show_percentage: false,
border: BorderStyle::None,
}
}
#[must_use]
pub fn with_filled_style(mut self, style: Style) -> Self {
self.filled_style = style;
self
}
#[must_use]
pub fn with_empty_style(mut self, style: Style) -> Self {
self.empty_style = style;
self
}
#[must_use]
pub fn with_label_style(mut self, style: Style) -> Self {
self.label_style = style;
self
}
#[must_use]
pub fn with_show_percentage(mut self, show: bool) -> Self {
self.show_percentage = show;
self
}
#[must_use]
pub fn with_border(mut self, border: BorderStyle) -> Self {
self.border = border;
self
}
pub fn set_progress(&mut self, progress: f32) {
self.mode = ProgressMode::Determinate(progress.clamp(0.0, 1.0));
}
pub fn progress(&self) -> Option<f32> {
match self.mode {
ProgressMode::Determinate(p) => Some(p),
ProgressMode::Indeterminate { .. } => None,
}
}
pub fn tick(&mut self) {
if let ProgressMode::Indeterminate { ref mut phase } = self.mode {
*phase = phase.wrapping_add(1);
}
}
pub fn mode(&self) -> &ProgressMode {
&self.mode
}
}
impl Widget for ProgressBar {
fn render(&self, area: Rect, buf: &mut ScreenBuffer) {
if area.size.width == 0 || area.size.height == 0 {
return;
}
super::border::render_border(area, self.border, self.empty_style.clone(), buf);
let inner = super::border::inner_area(area, self.border);
if inner.size.width == 0 || inner.size.height == 0 {
return;
}
let w = inner.size.width as usize;
let y = inner.position.y;
let x0 = inner.position.x;
match &self.mode {
ProgressMode::Determinate(progress) => {
let filled_count = ((progress * w as f32).round() as usize).min(w);
for i in 0..filled_count {
buf.set(x0 + i as u16, y, Cell::new("█", self.filled_style.clone()));
}
for i in filled_count..w {
buf.set(x0 + i as u16, y, Cell::new("░", self.empty_style.clone()));
}
if self.show_percentage {
let pct = (progress * 100.0).round() as u32;
let label = format!("{pct}%");
let label_len = label.len();
let start = w.saturating_sub(label_len) / 2;
for (i, ch) in label.chars().enumerate() {
let col = start + i;
if col < w {
buf.set(
x0 + col as u16,
y,
Cell::new(ch.to_string(), self.label_style.clone()),
);
}
}
}
}
ProgressMode::Indeterminate { phase } => {
let wave_len = WAVE_CHARS.len();
for i in 0..w {
let char_idx = (i + phase) % (wave_len * 2);
let ch = if char_idx < wave_len {
WAVE_CHARS[char_idx]
} else {
WAVE_CHARS[wave_len * 2 - 1 - char_idx]
};
buf.set(x0 + i as u16, y, Cell::new(ch, self.filled_style.clone()));
}
}
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::geometry::Size;
#[test]
fn create_determinate_zero() {
let bar = ProgressBar::new(0.0);
assert_eq!(bar.progress(), Some(0.0));
}
#[test]
fn create_determinate_half() {
let bar = ProgressBar::new(0.5);
assert_eq!(bar.progress(), Some(0.5));
}
#[test]
fn create_determinate_full() {
let bar = ProgressBar::new(1.0);
assert_eq!(bar.progress(), Some(1.0));
}
#[test]
fn progress_clamped() {
let bar = ProgressBar::new(2.0);
assert_eq!(bar.progress(), Some(1.0));
let bar2 = ProgressBar::new(-0.5);
assert_eq!(bar2.progress(), Some(0.0));
}
#[test]
fn render_determinate_half() {
let bar = ProgressBar::new(0.5).with_show_percentage(false);
let mut buf = ScreenBuffer::new(Size::new(10, 1));
bar.render(Rect::new(0, 0, 10, 1), &mut buf);
assert_eq!(buf.get(0, 0).unwrap().grapheme, "█");
assert_eq!(buf.get(4, 0).unwrap().grapheme, "█");
assert_eq!(buf.get(5, 0).unwrap().grapheme, "░");
assert_eq!(buf.get(9, 0).unwrap().grapheme, "░");
}
#[test]
fn render_determinate_full() {
let bar = ProgressBar::new(1.0).with_show_percentage(false);
let mut buf = ScreenBuffer::new(Size::new(10, 1));
bar.render(Rect::new(0, 0, 10, 1), &mut buf);
for i in 0..10 {
assert_eq!(buf.get(i, 0).unwrap().grapheme, "█");
}
}
#[test]
fn percentage_label_shown() {
let bar = ProgressBar::new(0.5).with_show_percentage(true);
let mut buf = ScreenBuffer::new(Size::new(20, 1));
bar.render(Rect::new(0, 0, 20, 1), &mut buf);
let row: String = (0..20)
.map(|x| buf.get(x, 0).map(|c| c.grapheme.as_str()).unwrap_or(" "))
.collect();
assert!(row.contains("50%"));
}
#[test]
fn set_progress_updates() {
let mut bar = ProgressBar::new(0.0);
bar.set_progress(0.75);
assert_eq!(bar.progress(), Some(0.75));
}
#[test]
fn indeterminate_mode() {
let bar = ProgressBar::indeterminate();
assert!(bar.progress().is_none());
assert!(matches!(
bar.mode(),
ProgressMode::Indeterminate { phase: 0 }
));
}
#[test]
fn tick_advances_indeterminate() {
let mut bar = ProgressBar::indeterminate();
bar.tick();
assert!(matches!(
bar.mode(),
ProgressMode::Indeterminate { phase: 1 }
));
bar.tick();
assert!(matches!(
bar.mode(),
ProgressMode::Indeterminate { phase: 2 }
));
}
#[test]
fn indeterminate_renders() {
let bar = ProgressBar::indeterminate();
let mut buf = ScreenBuffer::new(Size::new(10, 1));
bar.render(Rect::new(0, 0, 10, 1), &mut buf);
let first = buf.get(0, 0).unwrap().grapheme.clone();
assert!(WAVE_CHARS.contains(&first.as_str()) || first == "█" || first == "▏");
}
#[test]
fn border_rendering() {
let bar = ProgressBar::new(0.5)
.with_border(BorderStyle::Single)
.with_show_percentage(false);
let mut buf = ScreenBuffer::new(Size::new(12, 3));
bar.render(Rect::new(0, 0, 12, 3), &mut buf);
assert_eq!(buf.get(0, 0).unwrap().grapheme, "┌");
assert_eq!(buf.get(11, 0).unwrap().grapheme, "┐");
assert_eq!(buf.get(1, 1).unwrap().grapheme, "█");
}
}