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 color(mut self, color: Color) -> Self {
50 self.style = Style::from_color(Some(color), self.style.bgcolor().cloned());
51 self
52 }
53
54 pub fn bgcolor(mut self, bgcolor: Color) -> Self {
56 self.style = Style::from_color(self.style.color().cloned(), Some(bgcolor));
57 self
58 }
59
60 pub fn width(mut self, width: usize) -> Self {
62 self.width = Some(width);
63 self
64 }
65}
66
67impl Renderable for Bar {
68 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
71 match self.width {
72 Some(width) => Measurement::new(width, width),
73 None => Measurement::new(4, options.max_width),
74 }
75 }
76
77 fn rich_render(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
78 let width = self
79 .width
80 .unwrap_or(options.max_width)
81 .min(options.max_width);
82 let style = Some(self.style.clone());
83
84 if self.begin >= self.end {
85 return vec![Segment::new(" ".repeat(width), style)];
86 }
87
88 let prefix_complete_eighths = (width as f64 * 8.0 * self.begin / self.size) as usize;
89 let prefix_bar_count = prefix_complete_eighths / 8;
90 let prefix_eighths = prefix_complete_eighths % 8;
91
92 let body_complete_eighths = (width as f64 * 8.0 * self.end / self.size) as usize;
93 let body_bar_count = body_complete_eighths / 8;
94 let body_eighths = body_complete_eighths % 8;
95
96 let mut prefix = " ".repeat(prefix_bar_count);
97 if prefix_eighths != 0 {
98 prefix.push_str(BEGIN_BLOCK_ELEMENTS[prefix_eighths]);
99 }
100
101 let mut body = FULL_BLOCK.repeat(body_bar_count);
102 if body_eighths != 0 {
103 body.push_str(END_BLOCK_ELEMENTS[body_eighths]);
104 }
105
106 let body_len = body.chars().count();
107 let suffix = " ".repeat(width.saturating_sub(body_len));
108
109 let prefix_len = prefix.chars().count();
111 let body_tail: String = body.chars().skip(prefix_len).collect();
112 let line = format!("{prefix}{body_tail}{suffix}");
113
114 vec![Segment::new(line, style)]
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::color::ColorSystem;
122
123 fn render(begin: f64, end: f64) -> String {
124 let console = Console::builder()
125 .force_terminal(true)
126 .color_system(Some(ColorSystem::Truecolor))
127 .width(20)
128 .build();
129 console.render_to_string(&Bar::new(100.0, begin, end).width(20))
130 }
131
132 #[test]
133 fn full_bar() {
134 assert_eq!(
135 render(0.0, 100.0),
136 format!("\x1b[39;49m{}\x1b[0m", FULL_BLOCK.repeat(20))
137 );
138 }
139
140 #[test]
141 fn half_bar() {
142 assert_eq!(
143 render(0.0, 50.0),
144 format!("\x1b[39;49m{} \x1b[0m", FULL_BLOCK.repeat(10))
145 );
146 }
147
148 #[test]
149 fn partial_edge_uses_eighth_block() {
150 assert_eq!(render(0.0, 33.0), "\x1b[39;49m██████▌ \x1b[0m");
152 }
153}