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