1use ratatui::{
4 buffer::Buffer,
5 layout::Rect,
6 style::{Color, Style},
7 widgets::{Block, Widget},
8};
9
10#[derive(Debug, Clone)]
12pub struct ProgressBar {
13 progress: f32,
14 label: String,
15 style: Style,
16 filled_style: Style,
17 empty_style: Style,
18 block: Option<Block<'static>>,
19 show_percentage: bool,
20 show_label: bool,
21}
22
23impl ProgressBar {
24 pub fn new() -> Self {
26 Self {
27 progress: 0.0,
28 label: String::new(),
29 style: Style::default(),
30 filled_style: Style::default().fg(Color::Green),
31 empty_style: Style::default().fg(Color::Gray),
32 block: None,
33 show_percentage: true,
34 show_label: true,
35 }
36 }
37
38 pub fn progress(mut self, progress: f32) -> Self {
40 self.progress = progress.clamp(0.0, 1.0);
41 self
42 }
43
44 pub fn label<T>(mut self, label: T) -> Self
46 where
47 T: Into<String>,
48 {
49 self.label = label.into();
50 self
51 }
52
53 pub fn style(mut self, style: Style) -> Self {
55 self.style = style;
56 self
57 }
58
59 pub fn filled_style(mut self, style: Style) -> Self {
61 self.filled_style = style;
62 self
63 }
64
65 pub fn empty_style(mut self, style: Style) -> Self {
67 self.empty_style = style;
68 self
69 }
70
71 pub fn block(mut self, block: Block<'static>) -> Self {
73 self.block = Some(block);
74 self
75 }
76
77 pub fn show_percentage(mut self, show: bool) -> Self {
79 self.show_percentage = show;
80 self
81 }
82
83 pub fn show_label(mut self, show: bool) -> Self {
85 self.show_label = show;
86 self
87 }
88
89 pub fn render_widget(&self, area: Rect, buf: &mut Buffer) {
91 if area.width < 3 || area.height < 1 {
92 return;
93 }
94
95 let inner_area = if let Some(ref block) = self.block {
96 let inner = block.inner(area);
97 block.render(area, buf);
98 inner
99 } else {
100 area
101 };
102
103 if inner_area.width == 0 || inner_area.height == 0 {
104 return;
105 }
106
107 let mut bar_area = inner_area;
109 let mut info_area = None;
110
111 if (self.show_label && !self.label.is_empty()) || self.show_percentage {
113 if inner_area.height >= 2 {
114 bar_area.height = inner_area.height - 1;
115 info_area = Some(Rect {
116 x: inner_area.x,
117 y: inner_area.y + bar_area.height,
118 width: inner_area.width,
119 height: 1,
120 });
121 }
122 }
123
124 let filled_width = (self.progress * bar_area.width as f32) as u16;
126 let _empty_width = bar_area.width - filled_width;
127
128 for y in bar_area.top()..bar_area.bottom() {
130 for x in bar_area.left()..(bar_area.left() + filled_width) {
131 if x < bar_area.right() {
132 buf[(x, y)]
133 .set_symbol("█")
134 .set_style(self.filled_style);
135 }
136 }
137
138 for x in (bar_area.left() + filled_width)..bar_area.right() {
140 buf[(x, y)]
141 .set_symbol("░")
142 .set_style(self.empty_style);
143 }
144 }
145
146 if let Some(info_area) = info_area {
148 let mut info_text = String::new();
149
150 if self.show_label && !self.label.is_empty() {
151 info_text.push_str(&self.label);
152 }
153
154 if self.show_percentage {
155 if !info_text.is_empty() {
156 info_text.push_str(" ");
157 }
158 info_text.push_str(&format!("{:.1}%", self.progress * 100.0));
159 }
160
161 let info_chars: Vec<char> = info_text.chars().collect();
163 let start_x = if info_area.width > info_chars.len() as u16 {
164 info_area.left() + (info_area.width - info_chars.len() as u16) / 2
165 } else {
166 info_area.left()
167 };
168
169 for (i, ch) in info_chars.iter().enumerate() {
170 let x = start_x + i as u16;
171 if x < info_area.right() {
172 buf[(x, info_area.top())]
173 .set_symbol(&ch.to_string())
174 .set_style(self.style);
175 }
176 }
177 }
178 }
179}
180
181impl Default for ProgressBar {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187impl Widget for ProgressBar {
188 fn render(self, area: Rect, buf: &mut Buffer) {
189 self.render_widget(area, buf);
190 }
191}
192
193impl Widget for &ProgressBar {
194 fn render(self, area: Rect, buf: &mut Buffer) {
195 self.render_widget(area, buf);
196 }
197}
198
199#[derive(Debug, Clone)]
201pub struct ProgressBars {
202 bars: Vec<(String, ProgressBar)>,
203 style: Style,
204 block: Option<Block<'static>>,
205}
206
207impl ProgressBars {
208 pub fn new() -> Self {
210 Self {
211 bars: Vec::new(),
212 style: Style::default(),
213 block: None,
214 }
215 }
216
217 pub fn add_bar<T>(&mut self, name: T, progress: f32)
219 where
220 T: Into<String>,
221 {
222 let name_string = name.into();
223
224 for (existing_name, bar) in &mut self.bars {
226 if *existing_name == name_string {
227 *bar = ProgressBar::new()
228 .label(&name_string)
229 .progress(progress);
230 return;
231 }
232 }
233
234 let bar = ProgressBar::new()
236 .label(&name_string)
237 .progress(progress);
238 self.bars.push((name_string, bar));
239 }
240
241 pub fn style(mut self, style: Style) -> Self {
243 self.style = style;
244 self
245 }
246
247 pub fn block(mut self, block: Block<'static>) -> Self {
249 self.block = Some(block);
250 self
251 }
252
253 pub fn clear(&mut self) {
255 self.bars.clear();
256 }
257
258 pub fn len(&self) -> usize {
260 self.bars.len()
261 }
262
263 pub fn is_empty(&self) -> bool {
265 self.bars.is_empty()
266 }
267
268 pub fn render_widget(&self, area: Rect, buf: &mut Buffer) {
270 if area.width < 3 || area.height < 3 {
271 return;
272 }
273
274 let inner_area = if let Some(ref block) = self.block {
275 let inner = block.inner(area);
276 block.render(area, buf);
277 inner
278 } else {
279 area
280 };
281
282 if self.bars.is_empty() || inner_area.height == 0 {
283 return;
284 }
285
286 let bar_height = if inner_area.height >= self.bars.len() as u16 * 2 {
287 2 } else {
289 1 };
291
292 let total_height_needed = self.bars.len() as u16 * bar_height;
293 let start_y = if total_height_needed <= inner_area.height {
294 inner_area.top()
295 } else {
296 inner_area.top()
297 };
298
299 for (i, (_name, bar)) in self.bars.iter().enumerate() {
300 let y = start_y + (i as u16 * bar_height);
301
302 if y >= inner_area.bottom() {
303 break;
304 }
305
306 let bar_area = Rect {
307 x: inner_area.x,
308 y,
309 width: inner_area.width,
310 height: bar_height.min(inner_area.bottom() - y),
311 };
312
313 bar.render_widget(bar_area, buf);
314 }
315 }
316}
317
318impl Default for ProgressBars {
319 fn default() -> Self {
320 Self::new()
321 }
322}
323
324impl Widget for ProgressBars {
325 fn render(self, area: Rect, buf: &mut Buffer) {
326 self.render_widget(area, buf);
327 }
328}
329
330impl Widget for &ProgressBars {
331 fn render(self, area: Rect, buf: &mut Buffer) {
332 self.render_widget(area, buf);
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 use ratatui::{
340 buffer::Buffer,
341 layout::Rect,
342 };
343
344 #[test]
345 fn test_progress_bar_creation() {
346 let bar = ProgressBar::new()
347 .progress(0.5)
348 .label("Test")
349 .show_percentage(true);
350
351 assert!((bar.progress - 0.5).abs() < f32::EPSILON);
352 assert_eq!(bar.label, "Test");
353 assert!(bar.show_percentage);
354 }
355
356 #[test]
357 fn test_progress_clamping() {
358 let bar1 = ProgressBar::new().progress(-0.5);
359 assert!((bar1.progress - 0.0).abs() < f32::EPSILON);
360
361 let bar2 = ProgressBar::new().progress(1.5);
362 assert!((bar2.progress - 1.0).abs() < f32::EPSILON);
363 }
364
365 #[test]
366 fn test_progress_bars_collection() {
367 let mut bars = ProgressBars::new();
368 bars.add_bar("Algorithm1", 0.3);
369 bars.add_bar("Algorithm2", 0.7);
370
371 assert_eq!(bars.len(), 2);
372 assert!(!bars.is_empty());
373
374 bars.add_bar("Algorithm1", 0.5);
376 assert_eq!(bars.len(), 2); }
378
379 #[test]
380 fn test_render_widget() {
381 let bar = ProgressBar::new()
382 .progress(0.5)
383 .label("Test Bar")
384 .show_percentage(true);
385
386 let area = Rect::new(0, 0, 20, 3);
387 let mut buffer = Buffer::empty(area);
388
389 bar.render_widget(area, &mut buffer);
390
391 let content = buffer.content();
393 assert!(!content.is_empty());
394 }
395
396 #[test]
397 fn test_render_progress_bars_collection() {
398 let mut bars = ProgressBars::new();
399 bars.add_bar("Quick Sort", 0.8);
400 bars.add_bar("Merge Sort", 0.4);
401 bars.add_bar("Bubble Sort", 0.2);
402
403 let area = Rect::new(0, 0, 30, 10);
404 let mut buffer = Buffer::empty(area);
405
406 bars.render_widget(area, &mut buffer);
407
408 let content = buffer.content();
410 assert!(!content.is_empty());
411 }
412
413 #[test]
414 fn test_clear_bars() {
415 let mut bars = ProgressBars::new();
416 bars.add_bar("Test", 0.5);
417 assert_eq!(bars.len(), 1);
418
419 bars.clear();
420 assert_eq!(bars.len(), 0);
421 assert!(bars.is_empty());
422 }
423
424 #[test]
425 fn test_small_area_handling() {
426 let bar = ProgressBar::new().progress(0.5);
427
428 let tiny_area = Rect::new(0, 0, 1, 1);
430 let mut buffer = Buffer::empty(tiny_area);
431 bar.render_widget(tiny_area, &mut buffer);
432
433 }
435}