1use crate::buffer::ScreenBuffer;
7use crate::cell::Cell;
8use crate::geometry::Rect;
9use crate::style::Style;
10
11use super::{BorderStyle, Widget};
12
13#[derive(Clone, Debug, PartialEq)]
15pub enum ProgressMode {
16 Determinate(f32),
18 Indeterminate {
20 phase: usize,
22 },
23}
24
25pub struct ProgressBar {
31 mode: ProgressMode,
33 filled_style: Style,
35 empty_style: Style,
37 label_style: Style,
39 show_percentage: bool,
41 border: BorderStyle,
43}
44
45const WAVE_CHARS: &[&str] = &["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
47
48impl ProgressBar {
49 pub fn new(progress: f32) -> Self {
53 Self {
54 mode: ProgressMode::Determinate(progress.clamp(0.0, 1.0)),
55 filled_style: Style::default().reverse(true),
56 empty_style: Style::default(),
57 label_style: Style::default(),
58 show_percentage: true,
59 border: BorderStyle::None,
60 }
61 }
62
63 pub fn indeterminate() -> Self {
65 Self {
66 mode: ProgressMode::Indeterminate { phase: 0 },
67 filled_style: Style::default().reverse(true),
68 empty_style: Style::default(),
69 label_style: Style::default(),
70 show_percentage: false,
71 border: BorderStyle::None,
72 }
73 }
74
75 #[must_use]
77 pub fn with_filled_style(mut self, style: Style) -> Self {
78 self.filled_style = style;
79 self
80 }
81
82 #[must_use]
84 pub fn with_empty_style(mut self, style: Style) -> Self {
85 self.empty_style = style;
86 self
87 }
88
89 #[must_use]
91 pub fn with_label_style(mut self, style: Style) -> Self {
92 self.label_style = style;
93 self
94 }
95
96 #[must_use]
98 pub fn with_show_percentage(mut self, show: bool) -> Self {
99 self.show_percentage = show;
100 self
101 }
102
103 #[must_use]
105 pub fn with_border(mut self, border: BorderStyle) -> Self {
106 self.border = border;
107 self
108 }
109
110 pub fn set_progress(&mut self, progress: f32) {
114 self.mode = ProgressMode::Determinate(progress.clamp(0.0, 1.0));
115 }
116
117 pub fn progress(&self) -> Option<f32> {
121 match self.mode {
122 ProgressMode::Determinate(p) => Some(p),
123 ProgressMode::Indeterminate { .. } => None,
124 }
125 }
126
127 pub fn tick(&mut self) {
131 if let ProgressMode::Indeterminate { ref mut phase } = self.mode {
132 *phase = phase.wrapping_add(1);
133 }
134 }
135
136 pub fn mode(&self) -> &ProgressMode {
138 &self.mode
139 }
140}
141
142impl Widget for ProgressBar {
143 fn render(&self, area: Rect, buf: &mut ScreenBuffer) {
144 if area.size.width == 0 || area.size.height == 0 {
145 return;
146 }
147
148 super::border::render_border(area, self.border, self.empty_style.clone(), buf);
149 let inner = super::border::inner_area(area, self.border);
150 if inner.size.width == 0 || inner.size.height == 0 {
151 return;
152 }
153
154 let w = inner.size.width as usize;
155 let y = inner.position.y;
156 let x0 = inner.position.x;
157
158 match &self.mode {
159 ProgressMode::Determinate(progress) => {
160 let filled_count = ((progress * w as f32).round() as usize).min(w);
161
162 for i in 0..filled_count {
164 buf.set(x0 + i as u16, y, Cell::new("█", self.filled_style.clone()));
165 }
166
167 for i in filled_count..w {
169 buf.set(x0 + i as u16, y, Cell::new("░", self.empty_style.clone()));
170 }
171
172 if self.show_percentage {
174 let pct = (progress * 100.0).round() as u32;
175 let label = format!("{pct}%");
176 let label_len = label.len();
177 let start = w.saturating_sub(label_len) / 2;
178
179 for (i, ch) in label.chars().enumerate() {
180 let col = start + i;
181 if col < w {
182 buf.set(
183 x0 + col as u16,
184 y,
185 Cell::new(ch.to_string(), self.label_style.clone()),
186 );
187 }
188 }
189 }
190 }
191 ProgressMode::Indeterminate { phase } => {
192 let wave_len = WAVE_CHARS.len();
193 for i in 0..w {
194 let char_idx = (i + phase) % (wave_len * 2);
195 let ch = if char_idx < wave_len {
196 WAVE_CHARS[char_idx]
197 } else {
198 WAVE_CHARS[wave_len * 2 - 1 - char_idx]
199 };
200 buf.set(x0 + i as u16, y, Cell::new(ch, self.filled_style.clone()));
201 }
202 }
203 }
204 }
205}
206
207#[cfg(test)]
208#[allow(clippy::unwrap_used)]
209mod tests {
210 use super::*;
211 use crate::geometry::Size;
212
213 #[test]
214 fn create_determinate_zero() {
215 let bar = ProgressBar::new(0.0);
216 assert_eq!(bar.progress(), Some(0.0));
217 }
218
219 #[test]
220 fn create_determinate_half() {
221 let bar = ProgressBar::new(0.5);
222 assert_eq!(bar.progress(), Some(0.5));
223 }
224
225 #[test]
226 fn create_determinate_full() {
227 let bar = ProgressBar::new(1.0);
228 assert_eq!(bar.progress(), Some(1.0));
229 }
230
231 #[test]
232 fn progress_clamped() {
233 let bar = ProgressBar::new(2.0);
234 assert_eq!(bar.progress(), Some(1.0));
235
236 let bar2 = ProgressBar::new(-0.5);
237 assert_eq!(bar2.progress(), Some(0.0));
238 }
239
240 #[test]
241 fn render_determinate_half() {
242 let bar = ProgressBar::new(0.5).with_show_percentage(false);
243 let mut buf = ScreenBuffer::new(Size::new(10, 1));
244 bar.render(Rect::new(0, 0, 10, 1), &mut buf);
245
246 assert_eq!(buf.get(0, 0).unwrap().grapheme, "█");
248 assert_eq!(buf.get(4, 0).unwrap().grapheme, "█");
249 assert_eq!(buf.get(5, 0).unwrap().grapheme, "░");
250 assert_eq!(buf.get(9, 0).unwrap().grapheme, "░");
251 }
252
253 #[test]
254 fn render_determinate_full() {
255 let bar = ProgressBar::new(1.0).with_show_percentage(false);
256 let mut buf = ScreenBuffer::new(Size::new(10, 1));
257 bar.render(Rect::new(0, 0, 10, 1), &mut buf);
258
259 for i in 0..10 {
260 assert_eq!(buf.get(i, 0).unwrap().grapheme, "█");
261 }
262 }
263
264 #[test]
265 fn percentage_label_shown() {
266 let bar = ProgressBar::new(0.5).with_show_percentage(true);
267 let mut buf = ScreenBuffer::new(Size::new(20, 1));
268 bar.render(Rect::new(0, 0, 20, 1), &mut buf);
269
270 let row: String = (0..20)
272 .map(|x| buf.get(x, 0).map(|c| c.grapheme.as_str()).unwrap_or(" "))
273 .collect();
274 assert!(row.contains("50%"));
275 }
276
277 #[test]
278 fn set_progress_updates() {
279 let mut bar = ProgressBar::new(0.0);
280 bar.set_progress(0.75);
281 assert_eq!(bar.progress(), Some(0.75));
282 }
283
284 #[test]
285 fn indeterminate_mode() {
286 let bar = ProgressBar::indeterminate();
287 assert!(bar.progress().is_none());
288 assert!(matches!(
289 bar.mode(),
290 ProgressMode::Indeterminate { phase: 0 }
291 ));
292 }
293
294 #[test]
295 fn tick_advances_indeterminate() {
296 let mut bar = ProgressBar::indeterminate();
297 bar.tick();
298 assert!(matches!(
299 bar.mode(),
300 ProgressMode::Indeterminate { phase: 1 }
301 ));
302 bar.tick();
303 assert!(matches!(
304 bar.mode(),
305 ProgressMode::Indeterminate { phase: 2 }
306 ));
307 }
308
309 #[test]
310 fn indeterminate_renders() {
311 let bar = ProgressBar::indeterminate();
312 let mut buf = ScreenBuffer::new(Size::new(10, 1));
313 bar.render(Rect::new(0, 0, 10, 1), &mut buf);
314
315 let first = buf.get(0, 0).unwrap().grapheme.clone();
317 assert!(WAVE_CHARS.contains(&first.as_str()) || first == "█" || first == "▏");
318 }
319
320 #[test]
321 fn border_rendering() {
322 let bar = ProgressBar::new(0.5)
323 .with_border(BorderStyle::Single)
324 .with_show_percentage(false);
325 let mut buf = ScreenBuffer::new(Size::new(12, 3));
326 bar.render(Rect::new(0, 0, 12, 3), &mut buf);
327
328 assert_eq!(buf.get(0, 0).unwrap().grapheme, "┌");
329 assert_eq!(buf.get(11, 0).unwrap().grapheme, "┐");
330 assert_eq!(buf.get(1, 1).unwrap().grapheme, "█");
332 }
333}