use crate::element::{Component, Element};
use crate::style::{Color, Modifier, Style};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProgressStyle {
#[default]
Block,
Ascii,
Thin,
Thick,
Dots,
Braille,
}
impl ProgressStyle {
pub fn chars(&self) -> ProgressChars {
match self {
ProgressStyle::Block => ProgressChars {
filled: '█',
empty: '░',
head: None,
},
ProgressStyle::Ascii => ProgressChars {
filled: '=',
empty: '-',
head: Some('>'),
},
ProgressStyle::Thin => ProgressChars {
filled: '─',
empty: '─',
head: Some('○'),
},
ProgressStyle::Thick => ProgressChars {
filled: '▓',
empty: '░',
head: None,
},
ProgressStyle::Dots => ProgressChars {
filled: '●',
empty: '○',
head: None,
},
ProgressStyle::Braille => ProgressChars {
filled: '⣿',
empty: '⣀',
head: None,
},
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ProgressChars {
pub filled: char,
pub empty: char,
pub head: Option<char>,
}
impl Default for ProgressChars {
fn default() -> Self {
ProgressStyle::Block.chars()
}
}
#[derive(Debug, Clone)]
pub struct ProgressProps {
pub progress: f32,
pub width: usize,
pub style: ProgressStyle,
pub custom_chars: Option<ProgressChars>,
pub show_percentage: bool,
pub label: Option<String>,
pub filled_color: Option<Color>,
pub empty_color: Option<Color>,
pub text_color: Option<Color>,
pub bg_color: Option<Color>,
pub brackets: bool,
pub bold: bool,
pub dim_empty: bool,
}
impl Default for ProgressProps {
fn default() -> Self {
Self {
progress: 0.0,
width: 20,
style: ProgressStyle::Block,
custom_chars: None,
show_percentage: false,
label: None,
filled_color: None,
empty_color: None,
text_color: None,
bg_color: None,
brackets: false,
bold: false,
dim_empty: true,
}
}
}
impl ProgressProps {
pub fn new(progress: f32) -> Self {
Self {
progress: progress.clamp(0.0, 1.0),
..Default::default()
}
}
#[must_use]
pub fn progress(mut self, progress: f32) -> Self {
self.progress = progress.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn percent(mut self, percent: u32) -> Self {
self.progress = (percent.min(100) as f32) / 100.0;
self
}
#[must_use]
pub fn width(mut self, width: usize) -> Self {
self.width = width.max(1);
self
}
#[must_use]
pub fn style(mut self, style: ProgressStyle) -> Self {
self.style = style;
self
}
#[must_use]
pub fn custom_chars(mut self, chars: ProgressChars) -> Self {
self.custom_chars = Some(chars);
self
}
#[must_use]
pub fn show_percentage(mut self) -> Self {
self.show_percentage = true;
self
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn filled_color(mut self, color: Color) -> Self {
self.filled_color = Some(color);
self
}
#[must_use]
pub fn empty_color(mut self, color: Color) -> Self {
self.empty_color = Some(color);
self
}
#[must_use]
pub fn text_color(mut self, color: Color) -> Self {
self.text_color = Some(color);
self
}
#[must_use]
pub fn color(mut self, color: Color) -> Self {
self.filled_color = Some(color);
self.empty_color = Some(color);
self.text_color = Some(color);
self
}
#[must_use]
pub fn brackets(mut self) -> Self {
self.brackets = true;
self
}
#[must_use]
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
#[must_use]
pub fn dim_empty(mut self, dim: bool) -> Self {
self.dim_empty = dim;
self
}
#[must_use]
pub fn bg_color(mut self, color: Color) -> Self {
self.bg_color = Some(color);
self
}
fn chars(&self) -> ProgressChars {
self.custom_chars.unwrap_or_else(|| self.style.chars())
}
pub fn percentage(&self) -> u32 {
(self.progress * 100.0).round() as u32
}
pub fn render_string(&self) -> String {
let chars = self.chars();
let filled_count = ((self.progress * self.width as f32).round() as usize).min(self.width);
let empty_count = self.width - filled_count;
let mut result = String::new();
if let Some(ref label) = self.label {
result.push_str(label);
result.push(' ');
}
if self.brackets {
result.push('[');
}
if let Some(head) = chars.head {
if filled_count > 0 {
result.extend(std::iter::repeat_n(chars.filled, filled_count - 1));
if filled_count < self.width {
result.push(head);
} else {
result.push(chars.filled);
}
}
} else {
result.extend(std::iter::repeat_n(chars.filled, filled_count));
}
result.extend(std::iter::repeat_n(chars.empty, empty_count));
if self.brackets {
result.push(']');
}
if self.show_percentage {
result.push_str(&format!(" {:>3}%", self.percentage()));
}
result
}
}
pub struct Progress;
impl Component for Progress {
type Props = ProgressProps;
fn render(props: &Self::Props) -> Element {
let content = props.render_string();
let mut style = Style::new();
if let Some(color) = props.filled_color {
style = style.fg(color);
}
if let Some(bg) = props.bg_color {
style = style.bg(bg);
}
if props.bold {
style = style.add_modifier(Modifier::BOLD);
}
Element::styled_text(&content, style)
}
}
pub fn progress_bar(percent: u32, width: usize) -> String {
ProgressProps::new(percent as f32 / 100.0)
.width(width)
.render_string()
}
pub fn progress_bar_bracketed(percent: u32, width: usize) -> String {
ProgressProps::new(percent as f32 / 100.0)
.width(width)
.brackets()
.render_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_progress_props_default() {
let props = ProgressProps::default();
assert_eq!(props.progress, 0.0);
assert_eq!(props.width, 20);
assert_eq!(props.style, ProgressStyle::Block);
}
#[test]
fn test_progress_props_new() {
let props = ProgressProps::new(0.5);
assert_eq!(props.progress, 0.5);
}
#[test]
fn test_progress_props_clamp() {
let props = ProgressProps::new(1.5);
assert_eq!(props.progress, 1.0);
let props = ProgressProps::new(-0.5);
assert_eq!(props.progress, 0.0);
}
#[test]
fn test_progress_props_percent() {
let props = ProgressProps::default().percent(75);
assert_eq!(props.progress, 0.75);
}
#[test]
fn test_progress_props_builder() {
let props = ProgressProps::new(0.5)
.width(30)
.style(ProgressStyle::Ascii)
.filled_color(Color::Green)
.show_percentage()
.brackets();
assert_eq!(props.progress, 0.5);
assert_eq!(props.width, 30);
assert_eq!(props.style, ProgressStyle::Ascii);
assert_eq!(props.filled_color, Some(Color::Green));
assert!(props.show_percentage);
assert!(props.brackets);
}
#[test]
fn test_progress_percentage() {
let props = ProgressProps::new(0.756);
assert_eq!(props.percentage(), 76);
}
#[test]
fn test_progress_render_string_empty() {
let props = ProgressProps::new(0.0).width(10);
let bar = props.render_string();
assert_eq!(bar, "░░░░░░░░░░");
}
#[test]
fn test_progress_render_string_full() {
let props = ProgressProps::new(1.0).width(10);
let bar = props.render_string();
assert_eq!(bar, "██████████");
}
#[test]
fn test_progress_render_string_half() {
let props = ProgressProps::new(0.5).width(10);
let bar = props.render_string();
assert_eq!(bar, "█████░░░░░");
}
#[test]
fn test_progress_render_string_with_brackets() {
let props = ProgressProps::new(0.5).width(10).brackets();
let bar = props.render_string();
assert_eq!(bar, "[█████░░░░░]");
}
#[test]
fn test_progress_render_string_with_percentage() {
let props = ProgressProps::new(0.75).width(10).show_percentage();
let bar = props.render_string();
assert!(bar.ends_with(" 75%"));
}
#[test]
fn test_progress_render_string_with_label() {
let props = ProgressProps::new(0.5).width(10).label("Loading");
let bar = props.render_string();
assert!(bar.starts_with("Loading "));
}
#[test]
fn test_progress_render_string_ascii_style() {
let props = ProgressProps::new(0.5)
.width(10)
.style(ProgressStyle::Ascii);
let bar = props.render_string();
assert!(bar.contains('='));
assert!(bar.contains('>'));
assert!(bar.contains('-'));
}
#[test]
fn test_progress_style_block_chars() {
let chars = ProgressStyle::Block.chars();
assert_eq!(chars.filled, '█');
assert_eq!(chars.empty, '░');
assert!(chars.head.is_none());
}
#[test]
fn test_progress_style_ascii_chars() {
let chars = ProgressStyle::Ascii.chars();
assert_eq!(chars.filled, '=');
assert_eq!(chars.empty, '-');
assert_eq!(chars.head, Some('>'));
}
#[test]
fn test_progress_helper_function() {
let bar = progress_bar(50, 10);
assert_eq!(bar, "█████░░░░░");
}
#[test]
fn test_progress_helper_bracketed() {
let bar = progress_bar_bracketed(50, 10);
assert_eq!(bar, "[█████░░░░░]");
}
#[test]
fn test_progress_render_component() {
let props = ProgressProps::new(0.5).width(10);
let elem = Progress::render(&props);
match elem {
Element::Text { content, .. } => {
assert_eq!(content, "█████░░░░░");
}
_ => panic!("Expected Text element"),
}
}
#[test]
fn test_all_progress_styles() {
let styles = [
ProgressStyle::Block,
ProgressStyle::Ascii,
ProgressStyle::Thin,
ProgressStyle::Thick,
ProgressStyle::Dots,
ProgressStyle::Braille,
];
for style in &styles {
let props = ProgressProps::new(0.5).width(10).style(*style);
let bar = props.render_string();
assert_eq!(
bar.chars().count(),
10,
"Style {:?} has wrong length",
style
);
}
}
}