use std::fmt;
use crate::color::Color;
use crate::console::{Console, ConsoleOptions, Renderable};
use crate::measure::Measurement;
use crate::segment::Segment;
use crate::style::Style;
const BEGIN_BLOCK_ELEMENTS: [&str; 8] = [
"\u{2588}", "\u{2588}", "\u{2588}", "\u{2590}", "\u{2590}", "\u{2590}", "\u{2595}", "\u{2595}", ];
const END_BLOCK_ELEMENTS: [&str; 8] = [
" ", "\u{258F}", "\u{258E}", "\u{258D}", "\u{258C}", "\u{258B}", "\u{258A}", "\u{2589}", ];
const FULL_BLOCK: &str = "\u{2588}";
#[derive(Debug, Clone)]
pub struct Bar {
pub size: f64,
pub begin: f64,
pub end: f64,
pub width: Option<usize>,
pub style: Style,
}
impl Bar {
pub fn new(size: f64, begin: f64, end: f64) -> Self {
Self {
size,
begin: begin.max(0.0),
end: end.min(size),
width: None,
style: Style::null(),
}
}
#[must_use]
pub fn with_width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
#[must_use]
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
pub fn with_color(mut self, color: Color) -> Self {
self.style = Style::from_color(Some(color), self.style.bgcolor().cloned());
self
}
#[must_use]
pub fn with_bgcolor(mut self, bgcolor: Color) -> Self {
self.style = Style::from_color(self.style.color().cloned(), Some(bgcolor));
self
}
pub fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
match self.width {
Some(w) => Measurement::new(w, w),
None => Measurement::new(4, options.max_width),
}
}
}
impl fmt::Display for Bar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Bar({}, {}, {})", self.size, self.begin, self.end)
}
}
impl Renderable for Bar {
fn rich_console(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let width = match self.width {
Some(w) => w.min(options.max_width),
None => options.max_width,
};
if self.begin >= self.end {
return vec![
Segment::styled(&" ".repeat(width), self.style.clone()),
Segment::line(),
];
}
let prefix_complete_eights = (width as f64 * 8.0 * self.begin / self.size) as usize;
let prefix_bar_count = prefix_complete_eights / 8;
let prefix_eights_count = prefix_complete_eights % 8;
let mut prefix = " ".repeat(prefix_bar_count);
if prefix_eights_count != 0 {
prefix.push_str(BEGIN_BLOCK_ELEMENTS[prefix_eights_count]);
}
let body_complete_eights = (width as f64 * 8.0 * self.end / self.size) as usize;
let body_bar_count = body_complete_eights / 8;
let body_eights_count = body_complete_eights % 8;
let mut body = FULL_BLOCK.repeat(body_bar_count);
if body_eights_count != 0 {
body.push_str(END_BLOCK_ELEMENTS[body_eights_count]);
}
let body_char_len = body.chars().count();
let suffix = " ".repeat(width.saturating_sub(body_char_len));
let prefix_char_len = prefix.chars().count();
let body_tail: String = body.chars().skip(prefix_char_len).collect();
let bar_text = format!("{prefix}{body_tail}{suffix}");
vec![
Segment::styled(&bar_text, self.style.clone()),
Segment::line(),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::console::{Console, ConsoleDimensions, ConsoleOptions};
fn make_options(max_width: usize) -> ConsoleOptions {
ConsoleOptions {
size: ConsoleDimensions {
width: max_width,
height: 25,
},
legacy_windows: false,
min_width: 1,
max_width,
is_terminal: false,
encoding: "utf-8".to_string(),
max_height: 25,
justify: None,
overflow: None,
no_wrap: false,
highlight: None,
markup: None,
height: None,
}
}
fn render_bar_text(bar: &Bar, max_width: usize) -> String {
let console = Console::builder().width(max_width).build();
let opts = make_options(max_width);
let segments = bar.rich_console(&console, &opts);
segments.iter().map(|s| s.text.as_str()).collect()
}
#[test]
fn test_new_defaults() {
let bar = Bar::new(100.0, 0.0, 50.0);
assert_eq!(bar.size, 100.0);
assert_eq!(bar.begin, 0.0);
assert_eq!(bar.end, 50.0);
assert!(bar.width.is_none());
assert!(bar.style.is_null());
}
#[test]
fn test_new_clamps_begin() {
let bar = Bar::new(100.0, -10.0, 50.0);
assert_eq!(bar.begin, 0.0);
}
#[test]
fn test_new_clamps_end() {
let bar = Bar::new(100.0, 0.0, 200.0);
assert_eq!(bar.end, 100.0);
}
#[test]
fn test_with_width() {
let bar = Bar::new(100.0, 0.0, 50.0).with_width(40);
assert_eq!(bar.width, Some(40));
}
#[test]
fn test_with_style() {
let style = Style::parse("bold red on blue").unwrap();
let bar = Bar::new(100.0, 0.0, 50.0).with_style(style.clone());
assert_eq!(bar.style, style);
}
#[test]
fn test_with_color() {
let bar = Bar::new(100.0, 0.0, 50.0).with_color(Color::parse("red").unwrap());
assert_eq!(bar.style.color().unwrap().name, "red");
}
#[test]
fn test_with_bgcolor() {
let bar = Bar::new(100.0, 0.0, 50.0).with_bgcolor(Color::parse("blue").unwrap());
assert_eq!(bar.style.bgcolor().unwrap().name, "blue");
}
#[test]
fn test_with_color_and_bgcolor_chained() {
let bar = Bar::new(100.0, 0.0, 50.0)
.with_color(Color::parse("red").unwrap())
.with_bgcolor(Color::parse("blue").unwrap());
assert_eq!(bar.style.color().unwrap().name, "red");
assert_eq!(bar.style.bgcolor().unwrap().name, "blue");
}
#[test]
fn test_display() {
let bar = Bar::new(100.0, 10.0, 90.0);
assert_eq!(format!("{bar}"), "Bar(100, 10, 90)");
}
#[test]
fn test_empty_bar_begin_equals_end() {
let bar = Bar::new(100.0, 50.0, 50.0).with_width(10);
let text = render_bar_text(&bar, 10);
assert_eq!(text, format!("{}\n", " ".repeat(10)));
}
#[test]
fn test_empty_bar_begin_greater_than_end() {
let bar = Bar::new(100.0, 80.0, 20.0).with_width(10);
let text = render_bar_text(&bar, 10);
assert_eq!(text, format!("{}\n", " ".repeat(10)));
}
#[test]
fn test_full_bar() {
let bar = Bar::new(100.0, 0.0, 100.0).with_width(10);
let text = render_bar_text(&bar, 10);
assert_eq!(text, format!("{}\n", FULL_BLOCK.repeat(10)));
}
#[test]
fn test_half_bar() {
let bar = Bar::new(100.0, 0.0, 50.0).with_width(10);
let text = render_bar_text(&bar, 10);
assert!(text.contains(FULL_BLOCK));
assert!(text.ends_with('\n'));
let line = text.trim_end_matches('\n');
assert_eq!(line.chars().count(), 10);
}
#[test]
fn test_bar_with_specific_width() {
let bar = Bar::new(100.0, 0.0, 100.0).with_width(20);
let text = render_bar_text(&bar, 80);
let line = text.trim_end_matches('\n');
assert_eq!(line.chars().count(), 20);
}
#[test]
fn test_bar_width_capped_by_max_width() {
let bar = Bar::new(100.0, 0.0, 100.0).with_width(100);
let text = render_bar_text(&bar, 40);
let line = text.trim_end_matches('\n');
assert_eq!(line.chars().count(), 40);
}
#[test]
fn test_bar_default_width_uses_max_width() {
let bar = Bar::new(100.0, 0.0, 100.0);
let text = render_bar_text(&bar, 30);
let line = text.trim_end_matches('\n');
assert_eq!(line.chars().count(), 30);
}
#[test]
fn test_measure_with_fixed_width() {
let bar = Bar::new(100.0, 0.0, 50.0).with_width(25);
let console = Console::new();
let opts = make_options(80);
let m = bar.measure(&console, &opts);
assert_eq!(m, Measurement::new(25, 25));
}
#[test]
fn test_measure_without_fixed_width() {
let bar = Bar::new(100.0, 0.0, 50.0);
let console = Console::new();
let opts = make_options(80);
let m = bar.measure(&console, &opts);
assert_eq!(m, Measurement::new(4, 80));
}
#[test]
fn test_bar_offset_position() {
let bar = Bar::new(100.0, 25.0, 75.0).with_width(20);
let text = render_bar_text(&bar, 20);
let line = text.trim_end_matches('\n');
assert_eq!(line.chars().count(), 20);
let first_char = line.chars().next().unwrap();
assert_eq!(first_char, ' ');
}
#[test]
fn test_bar_near_end() {
let bar = Bar::new(100.0, 80.0, 100.0).with_width(20);
let text = render_bar_text(&bar, 20);
let line = text.trim_end_matches('\n');
assert_eq!(line.chars().count(), 20);
let chars: Vec<char> = line.chars().collect();
for ch in &chars[..16] {
assert_eq!(*ch, ' ');
}
}
#[test]
fn test_size_zero() {
let bar = Bar::new(0.0, 0.0, 0.0).with_width(10);
let text = render_bar_text(&bar, 10);
assert_eq!(text, format!("{}\n", " ".repeat(10)));
}
#[test]
fn test_begin_equals_end_at_zero() {
let bar = Bar::new(100.0, 0.0, 0.0).with_width(10);
let text = render_bar_text(&bar, 10);
assert_eq!(text, format!("{}\n", " ".repeat(10)));
}
#[test]
fn test_very_small_bar() {
let bar = Bar::new(100.0, 0.0, 1.0).with_width(10);
let text = render_bar_text(&bar, 10);
let line = text.trim_end_matches('\n');
assert_eq!(line.chars().count(), 10);
}
#[test]
fn test_segments_have_style() {
let style = Style::parse("red on blue").unwrap();
let bar = Bar::new(100.0, 0.0, 50.0)
.with_width(10)
.with_style(style.clone());
let console = Console::builder().width(10).build();
let opts = make_options(10);
let segments = bar.rich_console(&console, &opts);
assert_eq!(segments.len(), 2);
assert_eq!(segments[0].style.as_ref(), Some(&style));
assert_eq!(segments[1].text, "\n");
}
#[test]
fn test_segments_end_with_newline() {
let bar = Bar::new(100.0, 0.0, 50.0).with_width(10);
let console = Console::builder().width(10).build();
let opts = make_options(10);
let segments = bar.rich_console(&console, &opts);
let last = segments.last().unwrap();
assert_eq!(last.text, "\n");
}
#[test]
fn test_width_consistency() {
for pct in (0..=100).step_by(5) {
let bar = Bar::new(100.0, 0.0, pct as f64).with_width(20);
let text = render_bar_text(&bar, 20);
let line = text.trim_end_matches('\n');
assert_eq!(line.chars().count(), 20, "width mismatch at {}%", pct);
}
}
#[test]
fn test_width_consistency_with_offset() {
for begin in (0..=90).step_by(10) {
let bar = Bar::new(100.0, begin as f64, 100.0).with_width(20);
let text = render_bar_text(&bar, 20);
let line = text.trim_end_matches('\n');
assert_eq!(
line.chars().count(),
20,
"width mismatch with begin={}",
begin
);
}
}
#[test]
fn test_renderable_trait() {
let bar = Bar::new(100.0, 0.0, 50.0).with_width(10);
let console = Console::builder().width(80).build();
let opts = make_options(80);
let renderable: &dyn Renderable = &bar;
let segments = renderable.rich_console(&console, &opts);
assert!(!segments.is_empty());
}
#[test]
fn test_clone() {
let bar = Bar::new(100.0, 10.0, 90.0).with_width(40);
let cloned = bar.clone();
assert_eq!(cloned.size, bar.size);
assert_eq!(cloned.begin, bar.begin);
assert_eq!(cloned.end, bar.end);
assert_eq!(cloned.width, bar.width);
}
#[test]
fn test_debug() {
let bar = Bar::new(100.0, 0.0, 50.0);
let debug = format!("{bar:?}");
assert!(debug.contains("Bar"));
assert!(debug.contains("100"));
assert!(debug.contains("50"));
}
}