1use crate::color::Color;
8use crate::console::{Console, ConsoleOptions};
9use crate::measure::Measurement;
10use crate::protocol::Renderable;
11use crate::segment::Segment;
12use crate::style::Style;
13
14const FULL_BLOCK: &str = "\u{2588}"; const BEGIN_BLOCK_ELEMENTS: [&str; 8] = [
17 "\u{2588}", "\u{2588}", "\u{2588}", "\u{2590}", "\u{2590}", "\u{2590}", "\u{2595}", "\u{2595}",
18];
19const END_BLOCK_ELEMENTS: [&str; 8] = [
21 " ", "\u{258f}", "\u{258e}", "\u{258d}", "\u{258c}", "\u{258b}", "\u{258a}", "\u{2589}",
22];
23
24pub struct Bar {
26 size: f64,
27 begin: f64,
28 end: f64,
29 width: Option<usize>,
30 style: Style,
31}
32
33impl Bar {
34 pub fn new(size: f64, begin: f64, end: f64) -> Self {
36 Bar {
37 size,
38 begin,
39 end,
40 width: None,
41 style: Style::new()
43 .with_color(Color::default_color())
44 .with_bgcolor(Color::default_color()),
45 }
46 }
47
48 pub fn width(mut self, width: usize) -> Self {
50 self.width = Some(width);
51 self
52 }
53}
54
55impl Renderable for Bar {
56 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
59 match self.width {
60 Some(width) => Measurement::new(width, width),
61 None => Measurement::new(4, options.max_width),
62 }
63 }
64
65 fn rich_render(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
66 let width = self
67 .width
68 .unwrap_or(options.max_width)
69 .min(options.max_width);
70 let style = Some(self.style.clone());
71
72 if self.begin >= self.end {
73 return vec![Segment::new(" ".repeat(width), style)];
74 }
75
76 let prefix_complete_eighths = (width as f64 * 8.0 * self.begin / self.size) as usize;
77 let prefix_bar_count = prefix_complete_eighths / 8;
78 let prefix_eighths = prefix_complete_eighths % 8;
79
80 let body_complete_eighths = (width as f64 * 8.0 * self.end / self.size) as usize;
81 let body_bar_count = body_complete_eighths / 8;
82 let body_eighths = body_complete_eighths % 8;
83
84 let mut prefix = " ".repeat(prefix_bar_count);
85 if prefix_eighths != 0 {
86 prefix.push_str(BEGIN_BLOCK_ELEMENTS[prefix_eighths]);
87 }
88
89 let mut body = FULL_BLOCK.repeat(body_bar_count);
90 if body_eighths != 0 {
91 body.push_str(END_BLOCK_ELEMENTS[body_eighths]);
92 }
93
94 let body_len = body.chars().count();
95 let suffix = " ".repeat(width.saturating_sub(body_len));
96
97 let prefix_len = prefix.chars().count();
99 let body_tail: String = body.chars().skip(prefix_len).collect();
100 let line = format!("{prefix}{body_tail}{suffix}");
101
102 vec![Segment::new(line, style)]
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::color::ColorSystem;
110
111 fn render(begin: f64, end: f64) -> String {
112 let console = Console::builder()
113 .force_terminal(true)
114 .color_system(Some(ColorSystem::Truecolor))
115 .width(20)
116 .build();
117 console.render_to_string(&Bar::new(100.0, begin, end).width(20))
118 }
119
120 #[test]
121 fn full_bar() {
122 assert_eq!(
123 render(0.0, 100.0),
124 format!("\x1b[39;49m{}\x1b[0m", FULL_BLOCK.repeat(20))
125 );
126 }
127
128 #[test]
129 fn half_bar() {
130 assert_eq!(
131 render(0.0, 50.0),
132 format!("\x1b[39;49m{} \x1b[0m", FULL_BLOCK.repeat(10))
133 );
134 }
135
136 #[test]
137 fn partial_edge_uses_eighth_block() {
138 assert_eq!(render(0.0, 33.0), "\x1b[39;49m██████▌ \x1b[0m");
140 }
141}