use crate::render::Cell;
use crate::style::Color;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ProgressStyle {
#[default]
Block,
Line,
Ascii,
Braille,
}
pub struct Progress {
progress: f32, style: ProgressStyle,
filled_fg: Option<Color>,
filled_bg: Option<Color>,
empty_fg: Option<Color>,
empty_bg: Option<Color>,
show_percentage: bool,
props: WidgetProps,
}
impl Progress {
pub fn new(progress: f32) -> Self {
Self {
progress: progress.clamp(0.0, 1.0),
style: ProgressStyle::default(),
filled_fg: Some(Color::GREEN),
filled_bg: None,
empty_fg: Some(Color::rgb(64, 64, 64)),
empty_bg: None,
show_percentage: false,
props: WidgetProps::new(),
}
}
pub fn progress(mut self, progress: f32) -> Self {
self.progress = progress.clamp(0.0, 1.0);
self
}
pub fn style(mut self, style: ProgressStyle) -> Self {
self.style = style;
self
}
pub fn filled_color(mut self, color: Color) -> Self {
self.filled_fg = Some(color);
self
}
pub fn empty_color(mut self, color: Color) -> Self {
self.empty_fg = Some(color);
self
}
pub fn show_percentage(mut self, show: bool) -> Self {
self.show_percentage = show;
self
}
pub fn value(&self) -> f32 {
self.progress
}
pub fn set_progress(&mut self, progress: f32) {
self.progress = progress.clamp(0.0, 1.0);
}
fn get_chars(&self) -> (char, char) {
match self.style {
ProgressStyle::Block => ('█', '░'),
ProgressStyle::Line => ('━', '─'),
ProgressStyle::Ascii => ('#', '-'),
ProgressStyle::Braille => ('⣿', '⡀'),
}
}
}
impl Default for Progress {
fn default() -> Self {
Self::new(0.0)
}
}
impl View for Progress {
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width == 0 || area.height == 0 {
return;
}
let (filled_char, empty_char) = self.get_chars();
let bar_width = if self.show_percentage {
area.width.saturating_sub(5) } else {
area.width
};
let filled_width = (bar_width as f32 * self.progress).round() as u16;
for x in 0..filled_width {
if x >= bar_width {
break;
}
let mut cell = Cell::new(filled_char);
cell.fg = self.filled_fg;
cell.bg = self.filled_bg;
ctx.set(x, 0, cell);
}
for x in filled_width..bar_width {
let mut cell = Cell::new(empty_char);
cell.fg = self.empty_fg;
cell.bg = self.empty_bg;
ctx.set(x, 0, cell);
}
if self.show_percentage {
let pct = format!("{:3.0}%", self.progress * 100.0);
let start_x = bar_width + 1;
let max_width = area.width.saturating_sub(start_x);
let pct_fg = self.filled_fg.unwrap_or(Color::WHITE);
ctx.draw_text_clipped(start_x, 0, &pct, pct_fg, max_width);
}
}
crate::impl_view_meta!("Progress");
}
pub fn progress(value: f32) -> Progress {
Progress::new(value)
}
impl_styled_view!(Progress);
impl_props_builders!(Progress);
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Rect;
use crate::render::Buffer;
#[test]
fn test_progress_new() {
let p = Progress::new(0.5);
assert_eq!(p.value(), 0.5);
}
#[test]
fn test_progress_clamped() {
let p = Progress::new(1.5);
assert_eq!(p.value(), 1.0);
let p = Progress::new(-0.5);
assert_eq!(p.value(), 0.0);
}
#[test]
fn test_progress_set() {
let mut p = Progress::new(0.0);
p.set_progress(0.75);
assert_eq!(p.value(), 0.75);
}
#[test]
fn test_progress_builder() {
let p = Progress::new(0.5)
.filled_color(Color::GREEN)
.empty_color(Color::rgb(50, 50, 50))
.show_percentage(true);
assert_eq!(p.value(), 0.5);
}
#[test]
fn test_progress_render_no_panic() {
let mut buf = Buffer::new(20, 1);
let area = Rect::new(0, 0, 20, 1);
let mut ctx = RenderContext::new(&mut buf, area);
let p = Progress::new(0.5);
p.render(&mut ctx);
}
#[test]
fn test_progress_helper() {
let p = progress(0.3);
assert_eq!(p.value(), 0.3);
}
}