use crate::actor::InputEvent;
use crate::buffer::{Buffer, Cell, Rgb};
use crate::layout::Rect;
use super::traits::Widget;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Default)]
pub enum ProgressStyle {
Solid,
Ascii,
#[default]
Block,
Line,
}
#[derive(Debug, Clone)]
pub struct ProgressBarConfig {
pub style: ProgressStyle,
pub filled_fg: Rgb,
pub empty_fg: Rgb,
pub bg: Rgb,
pub show_percentage: bool,
pub percentage_fg: Rgb,
pub label: Option<String>,
pub label_fg: Rgb,
}
impl Default for ProgressBarConfig {
fn default() -> Self {
Self {
style: ProgressStyle::Block,
filled_fg: Rgb::new(0, 200, 100),
empty_fg: Rgb::new(60, 60, 60),
bg: Rgb::new(30, 30, 30),
show_percentage: true,
percentage_fg: Rgb::WHITE,
label: None,
label_fg: Rgb::new(150, 150, 150),
}
}
}
#[derive(Debug)]
pub struct ProgressBar {
progress: f32,
bounds: Rect,
config: ProgressBarConfig,
dirty: bool,
}
impl ProgressBar {
pub fn new(bounds: Rect) -> Self {
Self {
progress: 0.0,
bounds,
config: ProgressBarConfig::default(),
dirty: true,
}
}
pub const fn with_config(bounds: Rect, config: ProgressBarConfig) -> Self {
Self {
progress: 0.0,
bounds,
config,
dirty: true,
}
}
pub const fn set_progress(&mut self, progress: f32) {
self.progress = progress.clamp(0.0, 1.0);
self.dirty = true;
}
pub const fn progress(&self) -> f32 {
self.progress
}
pub fn set_label(&mut self, label: impl Into<String>) {
self.config.label = Some(label.into());
self.dirty = true;
}
pub fn clear_label(&mut self) {
self.config.label = None;
self.dirty = true;
}
pub fn increment(&mut self, delta: f32) {
self.set_progress(self.progress + delta);
}
pub fn is_complete(&self) -> bool {
self.progress >= 1.0
}
const fn style_chars(&self) -> (char, char) {
match self.config.style {
ProgressStyle::Solid => ('█', '░'),
ProgressStyle::Ascii => ('=', ' '),
ProgressStyle::Block => ('▓', '░'),
ProgressStyle::Line => ('─', '─'),
}
}
}
impl Widget for ProgressBar {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
self.dirty = true;
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_precision_loss)]
fn render(&self, buffer: &mut Buffer) {
let x = self.bounds.x;
let y = self.bounds.y;
let width = self.bounds.width as usize;
for i in 0..self.bounds.width {
buffer.set(x + i, y, Cell::new(' ').with_bg(self.config.bg));
}
let label_len = self.config.label.as_ref().map_or(0, |l| l.chars().count() + 1);
let pct_len = if self.config.show_percentage { 5 } else { 0 }; let bar_width = width.saturating_sub(label_len + pct_len);
if bar_width == 0 {
return;
}
let mut offset = x;
if let Some(ref label) = self.config.label {
for c in label.chars().take(width / 3) {
buffer.set(offset, y, Cell::new(c)
.with_fg(self.config.label_fg)
.with_bg(self.config.bg));
offset += 1;
}
buffer.set(offset, y, Cell::new(' ').with_bg(self.config.bg));
offset += 1;
}
let (filled_char, empty_char) = self.style_chars();
let filled_count = (self.progress * bar_width as f32).round() as usize;
for i in 0..bar_width {
let (c, fg) = if i < filled_count {
(filled_char, self.config.filled_fg)
} else {
(empty_char, self.config.empty_fg)
};
buffer.set(offset + i as u16, y, Cell::new(c)
.with_fg(fg)
.with_bg(self.config.bg));
}
offset += bar_width as u16;
if self.config.show_percentage {
let pct = format!(" {:>3}%", (self.progress * 100.0).round() as u32);
for c in pct.chars() {
buffer.set(offset, y, Cell::new(c)
.with_fg(self.config.percentage_fg)
.with_bg(self.config.bg));
offset += 1;
}
}
}
fn handle_input(&mut self, _event: &InputEvent) -> bool {
false
}
fn needs_redraw(&self) -> bool {
self.dirty
}
fn clear_redraw(&mut self) {
self.dirty = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_progress_bar_basic() {
let mut bar = ProgressBar::new(Rect::new(0, 0, 80, 1));
assert_eq!(bar.progress(), 0.0);
bar.set_progress(0.5);
assert_eq!(bar.progress(), 0.5);
bar.set_progress(1.5); assert_eq!(bar.progress(), 1.0);
}
#[test]
fn test_progress_bar_increment() {
let mut bar = ProgressBar::new(Rect::new(0, 0, 80, 1));
bar.increment(0.25);
assert!((bar.progress() - 0.25).abs() < f32::EPSILON);
bar.increment(0.25);
assert!((bar.progress() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_progress_bar_complete() {
let mut bar = ProgressBar::new(Rect::new(0, 0, 80, 1));
assert!(!bar.is_complete());
bar.set_progress(1.0);
assert!(bar.is_complete());
}
}