use crate::color::SimpleColor;
use crate::measure::Measurement;
use crate::segment::{Segment, Segments};
use crate::style::Style;
use crate::{Console, ConsoleOptions, Renderable};
const BEGIN_BLOCK_ELEMENTS: [char; 8] = ['█', '█', '█', '▐', '▐', '▐', '▕', '▕'];
const END_BLOCK_ELEMENTS: [char; 8] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉'];
const FULL_BLOCK: char = '█';
#[derive(Debug, Clone)]
pub struct Bar {
pub size: f64,
begin: f64,
end: f64,
pub width: Option<usize>,
pub style: Style,
}
impl Bar {
pub fn new(size: f64, begin: f64, end: f64) -> Self {
Bar {
size,
begin: begin.max(0.0),
end: end.min(size),
width: None,
style: Style::default(),
}
}
pub fn with_width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
pub fn with_color(mut self, color: SimpleColor) -> Self {
self.style = self.style.with_color(color);
self
}
pub fn with_bgcolor(mut self, bgcolor: SimpleColor) -> Self {
self.style = self.style.with_bgcolor(bgcolor);
self
}
pub fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
impl Renderable for Bar {
fn render(&self, _console: &Console, options: &ConsoleOptions) -> Segments {
let width = match self.width {
Some(w) => w.min(options.max_width),
None => options.max_width,
};
let mut segments = Segments::new();
if self.begin >= self.end || self.size <= 0.0 {
let text = " ".repeat(width);
segments.push(Segment::styled(text, self.style));
segments.push(Segment::line());
return segments;
}
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 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 prefix = " ".repeat(prefix_bar_count);
if prefix_eights_count > 0 {
prefix.push(BEGIN_BLOCK_ELEMENTS[prefix_eights_count]);
}
let mut body = FULL_BLOCK.to_string().repeat(body_bar_count);
if body_eights_count > 0 {
body.push(END_BLOCK_ELEMENTS[body_eights_count]);
}
let suffix = " ".repeat(width.saturating_sub(body.chars().count()));
let prefix_len = prefix.chars().count();
let body_remainder: String = body.chars().skip(prefix_len).collect();
let bar_text = format!("{}{}{}", prefix, body_remainder, suffix);
segments.push(Segment::styled(bar_text, self.style));
segments.push(Segment::line());
segments
}
fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
match self.width {
Some(w) => Measurement::exact(w),
None => Measurement::new(4, options.max_width),
}
}
}
impl std::fmt::Display for Bar {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Bar({}, {}, {})", self.size, self.begin, self.end)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bar_new() {
let bar = Bar::new(1.0, 0.25, 0.75);
assert_eq!(bar.size, 1.0);
assert_eq!(bar.begin, 0.25);
assert_eq!(bar.end, 0.75);
assert!(bar.width.is_none());
}
#[test]
fn test_bar_clamps_begin() {
let bar = Bar::new(1.0, -0.5, 0.5);
assert_eq!(bar.begin, 0.0);
}
#[test]
fn test_bar_clamps_end() {
let bar = Bar::new(1.0, 0.5, 1.5);
assert_eq!(bar.end, 1.0);
}
#[test]
fn test_bar_with_width() {
let bar = Bar::new(1.0, 0.0, 1.0).with_width(40);
assert_eq!(bar.width, Some(40));
}
#[test]
fn test_bar_with_color() {
let bar = Bar::new(1.0, 0.0, 1.0).with_color(SimpleColor::Standard(1));
assert_eq!(bar.style.color, Some(SimpleColor::Standard(1)));
}
#[test]
fn test_bar_display() {
let bar = Bar::new(2.0, 0.5, 1.5);
assert_eq!(format!("{}", bar), "Bar(2, 0.5, 1.5)");
}
#[test]
fn test_bar_render_full() {
let console = Console::new();
let mut options = ConsoleOptions::default();
options.max_width = 10;
let bar = Bar::new(1.0, 0.0, 1.0).with_width(10);
let segments = bar.render(&console, &options);
assert_eq!(segments.len(), 2);
let first = segments.iter().next().unwrap();
assert!(first.text.contains('█'));
}
#[test]
fn test_bar_render_empty() {
let console = Console::new();
let mut options = ConsoleOptions::default();
options.max_width = 10;
let bar = Bar::new(1.0, 0.5, 0.5).with_width(10); let segments = bar.render(&console, &options);
let first = segments.iter().next().unwrap();
assert_eq!(first.text.trim(), ""); }
#[test]
fn test_bar_render_partial() {
let console = Console::new();
let mut options = ConsoleOptions::default();
options.max_width = 20;
let bar = Bar::new(1.0, 0.25, 0.75).with_width(20);
let segments = bar.render(&console, &options);
let first = segments.iter().next().unwrap();
assert!(!first.text.is_empty());
}
#[test]
fn test_bar_measure_with_width() {
let console = Console::new();
let mut options = ConsoleOptions::default();
options.max_width = 100;
let bar = Bar::new(1.0, 0.0, 1.0).with_width(40);
let measurement = bar.measure(&console, &options);
assert_eq!(measurement.minimum, 40);
assert_eq!(measurement.maximum, 40);
}
#[test]
fn test_bar_measure_without_width() {
let console = Console::new();
let mut options = ConsoleOptions::default();
options.max_width = 80;
let bar = Bar::new(1.0, 0.0, 1.0);
let measurement = bar.measure(&console, &options);
assert_eq!(measurement.minimum, 4);
assert_eq!(measurement.maximum, 80);
}
#[test]
fn test_bar_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Bar>();
assert_sync::<Bar>();
}
}