use crate::component::{Component, EventCx, MeasureCx};
use crate::event::Event;
use crate::geom::{Rect, Size};
use crate::layout::Constraint;
use crate::render::RenderCx;
use crate::style::Style;
const BLOCKS: &[&str] = &[" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
pub struct ProgressBar {
ratio: f64,
width: u16,
style: Style,
track_style: Style,
label: Option<String>,
}
impl ProgressBar {
pub fn new() -> Self {
Self {
ratio: 0.0,
width: 20,
style: Style::default().fg(crate::style::Color::Cyan),
track_style: Style::default().fg(crate::style::Color::Gray),
label: None,
}
}
pub fn label(mut self, show: bool) -> Self {
self.label = if show { Some(String::new()) } else { None };
self
}
pub fn ratio(mut self, ratio: f64) -> Self {
self.ratio = ratio.clamp(0.0, 1.0);
self
}
pub fn width(mut self, width: u16) -> Self {
self.width = width;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn track_style(mut self, style: Style) -> Self {
self.track_style = style;
self
}
pub fn set_ratio(&mut self, ratio: f64, cx: &mut EventCx) {
let r = ratio.clamp(0.0, 1.0);
if (self.ratio - r).abs() > f64::EPSILON {
self.ratio = r;
cx.invalidate_paint();
}
}
}
impl Component for ProgressBar {
fn render(&self, cx: &mut RenderCx) {
let has_label = self.label.is_some();
let bar_width = if has_label { self.width.saturating_sub(5) } else { self.width };
let filled = (self.ratio * bar_width as f64) as u16;
let whole = filled.min(bar_width);
let frac = ((self.ratio * bar_width as f64) - whole as f64) * 8.0;
let frac_idx = (frac as usize).min(BLOCKS.len() - 1);
if whole > 0 {
cx.set_style(self.style.clone());
cx.text("█".repeat(whole as usize));
}
if whole < bar_width {
if frac_idx > 0 {
cx.set_style(self.style.clone());
cx.text(BLOCKS[frac_idx]);
}
let frac_used = if frac_idx > 0 { 1 } else { 0 };
let remaining = bar_width.saturating_sub(whole).saturating_sub(frac_used);
if remaining > 0 {
cx.set_style(self.track_style.clone());
cx.text("░".repeat(remaining as usize));
}
}
if has_label {
let pct = format!(" {:3}%", (self.ratio * 100.0) as u8);
cx.set_style(self.style.clone());
cx.text(&pct);
}
cx.set_style(self.track_style.clone());
cx.line("");
}
fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
Size { width: self.width, height: 1 }
}
fn event(&mut self, _event: &Event, _cx: &mut EventCx) {}
fn layout(&mut self, _rect: Rect, _cx: &mut crate::component::LayoutCx) {}
fn focusable(&self) -> bool { false }
fn style(&self) -> Style { self.style.clone() }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testbuffer::TestBuffer;
#[test]
fn test_empty() {
let mut tb = TestBuffer::new(30, 1);
tb.render(&ProgressBar::new().ratio(0.0).width(30));
let line = (0..30).map(|_| "░").collect::<String>();
tb.assert_line(0, &line);
}
#[test]
fn test_full() {
let mut tb = TestBuffer::new(30, 1);
tb.render(&ProgressBar::new().ratio(1.0).width(30));
let line = (0..30).map(|_| "█").collect::<String>();
tb.assert_line(0, &line);
}
#[test]
fn test_with_label() {
let mut tb = TestBuffer::new(30, 1);
tb.render(&ProgressBar::new().ratio(0.5).width(30).label(true));
assert!(tb.buffer.cells.iter().any(|c| c.symbol == "%"));
}
}