use ratatui::layout::Rect;
use ratatui::style::Style;
use crate::anim;
use crate::geometry::Size;
use crate::surface::Surface;
use crate::view::{RenderCtx, View};
const EIGHTHS: [char; 9] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
pub struct ProgressBar {
fraction: Option<f32>,
frame: u64,
show_percent: bool,
filled: Option<ratatui::style::Color>,
track: Option<ratatui::style::Color>,
}
impl ProgressBar {
pub fn determinate(fraction: f32) -> Self {
Self {
fraction: Some(fraction.clamp(0.0, 1.0)),
frame: 0,
show_percent: false,
filled: None,
track: None,
}
}
pub fn indeterminate(frame: u64) -> Self {
Self {
fraction: None,
frame,
show_percent: false,
filled: None,
track: None,
}
}
pub fn percent(mut self, show: bool) -> Self {
self.show_percent = show;
self
}
pub fn colors(mut self, filled: ratatui::style::Color, track: ratatui::style::Color) -> Self {
self.filled = Some(filled);
self.track = Some(track);
self
}
pub fn percent_value(&self) -> Option<u8> {
self.fraction.map(|f| (f * 100.0).round() as u8)
}
fn render_determinate(
&self,
area: Rect,
surface: &mut Surface,
ctx: &RenderCtx,
fraction: f32,
) {
let filled = self.filled.unwrap_or(ctx.theme.accent);
let track = self.track.unwrap_or(ctx.theme.dim);
let suffix = if self.show_percent {
Some(format!(" {:>3}%", (fraction * 100.0).round() as u16))
} else {
None
};
let suffix_w = suffix.as_ref().map(|s| s.len() as u16).unwrap_or(0);
let bar_w = area.width.saturating_sub(suffix_w);
if bar_w == 0 {
return;
}
let total_eighths = (fraction * bar_w as f32 * 8.0).round() as u32;
let full = (total_eighths / 8) as u16;
let remainder = (total_eighths % 8) as usize;
for i in 0..bar_w {
let x = area.x + i;
if i < full {
surface.set(x, area.y, '█', Style::default().fg(filled));
} else if i == full && remainder > 0 {
surface.set(
x,
area.y,
EIGHTHS[remainder],
Style::default().fg(filled).bg(track),
);
} else {
surface.set(x, area.y, ' ', Style::default().bg(track));
}
}
if let Some(suffix) = suffix {
surface.set_string(
area.x + bar_w,
area.y,
&suffix,
Style::default().fg(ctx.theme.muted),
);
}
}
fn render_indeterminate(&self, area: Rect, surface: &mut Surface, ctx: &RenderCtx) {
let filled = self.filled.unwrap_or(ctx.theme.accent);
let track = self.track.unwrap_or(ctx.theme.dim);
let w = area.width;
let seg = (w / 3).max(1);
let travel = w.saturating_sub(seg);
let pos = (anim::ping_pong(self.frame, 60) * travel as f32).round() as u16;
for i in 0..w {
let x = area.x + i;
let within = i >= pos && i < pos.saturating_add(seg);
if within {
surface.set(x, area.y, '█', Style::default().fg(filled));
} else {
surface.set(x, area.y, '░', Style::default().fg(track));
}
}
}
}
impl View for ProgressBar {
fn measure(&self, available: Size) -> Size {
Size::new(available.width, 1)
}
fn render(&self, area: Rect, surface: &mut Surface, ctx: &RenderCtx) {
if area.width == 0 || area.height == 0 {
return;
}
let row = Rect::new(area.x, area.y, area.width, 1);
match self.fraction {
Some(fraction) => self.render_determinate(row, surface, ctx, fraction),
None => self.render_indeterminate(row, surface, ctx),
}
}
}