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 && 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 let filled_width = (self.progress * bar_area.width as f32) as u16;
125 let _empty_width = bar_area.width - filled_width;
126
127 for y in bar_area.top()..bar_area.bottom() {
129 for x in bar_area.left()..(bar_area.left() + filled_width) {
130 if x < bar_area.right() {
131 buf[(x, y)]
132 .set_symbol("█")
133 .set_style(self.filled_style);
134 }
135 }
136
137 for x in (bar_area.left() + filled_width)..bar_area.right() {
139 buf[(x, y)]
140 .set_symbol("░")
141 .set_style(self.empty_style);
142 }
143 }
144
145 if let Some(info_area) = info_area {
147 let mut info_text = String::new();
148
149 if self.show_label && !self.label.is_empty() {
150 info_text.push_str(&self.label);
151 }
152
153 if self.show_percentage {
154 if !info_text.is_empty() {
155 info_text.push(' ');
156 }
157 info_text.push_str(&format!("{:.1}%", self.progress * 100.0));
158 }
159
160 let info_chars: Vec<char> = info_text.chars().collect();
162 let start_x = if info_area.width > info_chars.len() as u16 {
163 info_area.left() + (info_area.width - info_chars.len() as u16) / 2
164 } else {
165 info_area.left()
166 };
167
168 for (i, ch) in info_chars.iter().enumerate() {
169 let x = start_x + i as u16;
170 if x < info_area.right() {
171 buf[(x, info_area.top())]
172 .set_symbol(&ch.to_string())
173 .set_style(self.style);
174 }
175 }
176 }
177 }
178}
179
180impl Default for ProgressBar {
181 fn default() -> Self {
182 Self::new()
183 }
184}
185
186impl Widget for ProgressBar {
187 fn render(self, area: Rect, buf: &mut Buffer) {
188 self.render_widget(area, buf);
189 }
190}
191
192impl Widget for &ProgressBar {
193 fn render(self, area: Rect, buf: &mut Buffer) {
194 self.render_widget(area, buf);
195 }
196}
197
198#[derive(Debug, Clone)]
200pub struct ProgressBars {
201 bars: Vec<(String, ProgressBar)>,
202 style: Style,
203 block: Option<Block<'static>>,
204}
205
206impl ProgressBars {
207 pub fn new() -> Self {
209 Self {
210 bars: Vec::new(),
211 style: Style::default(),
212 block: None,
213 }
214 }
215
216 pub fn add_bar<T>(&mut self, name: T, progress: f32)
218 where
219 T: Into<String>,
220 {
221 let name_string = name.into();
222
223 for (existing_name, bar) in &mut self.bars {
225 if *existing_name == name_string {
226 *bar = ProgressBar::new()
227 .label(&name_string)
228 .progress(progress);
229 return;
230 }
231 }
232
233 let bar = ProgressBar::new()
235 .label(&name_string)
236 .progress(progress);
237 self.bars.push((name_string, bar));
238 }
239
240 pub fn style(mut self, style: Style) -> Self {
242 self.style = style;
243 self
244 }
245
246 pub fn block(mut self, block: Block<'static>) -> Self {
248 self.block = Some(block);
249 self
250 }
251
252 pub fn clear(&mut self) {
254 self.bars.clear();
255 }
256
257 pub fn len(&self) -> usize {
259 self.bars.len()
260 }
261
262 pub fn is_empty(&self) -> bool {
264 self.bars.is_empty()
265 }
266
267 pub fn render_widget(&self, area: Rect, buf: &mut Buffer) {
269 if area.width < 3 || area.height < 3 {
270 return;
271 }
272
273 let inner_area = if let Some(ref block) = self.block {
274 let inner = block.inner(area);
275 block.render(area, buf);
276 inner
277 } else {
278 area
279 };
280
281 if self.bars.is_empty() || inner_area.height == 0 {
282 return;
283 }
284
285 let bar_height = if inner_area.height >= self.bars.len() as u16 * 2 {
286 2 } else {
288 1 };
290
291 let total_height_needed = self.bars.len() as u16 * bar_height;
292 let start_y = if total_height_needed <= inner_area.height {
293 inner_area.top()
294 } else {
295 inner_area.top()
296 };
297
298 for (i, (_name, bar)) in self.bars.iter().enumerate() {
299 let y = start_y + (i as u16 * bar_height);
300
301 if y >= inner_area.bottom() {
302 break;
303 }
304
305 let bar_area = Rect {
306 x: inner_area.x,
307 y,
308 width: inner_area.width,
309 height: bar_height.min(inner_area.bottom() - y),
310 };
311
312 bar.render_widget(bar_area, buf);
313 }
314 }
315}
316
317impl Default for ProgressBars {
318 fn default() -> Self {
319 Self::new()
320 }
321}
322
323impl Widget for ProgressBars {
324 fn render(self, area: Rect, buf: &mut Buffer) {
325 self.render_widget(area, buf);
326 }
327}
328
329impl Widget for &ProgressBars {
330 fn render(self, area: Rect, buf: &mut Buffer) {
331 self.render_widget(area, buf);
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use ratatui::{
339 buffer::Buffer,
340 layout::Rect,
341 };
342
343 #[test]
344 fn test_progress_bar_creation() {
345 let bar = ProgressBar::new()
346 .progress(0.5)
347 .label("Test")
348 .show_percentage(true);
349
350 assert!((bar.progress - 0.5).abs() < f32::EPSILON);
351 assert_eq!(bar.label, "Test");
352 assert!(bar.show_percentage);
353 }
354
355 #[test]
356 fn test_progress_clamping() {
357 let bar1 = ProgressBar::new().progress(-0.5);
358 assert!((bar1.progress - 0.0).abs() < f32::EPSILON);
359
360 let bar2 = ProgressBar::new().progress(1.5);
361 assert!((bar2.progress - 1.0).abs() < f32::EPSILON);
362 }
363
364 #[test]
365 fn test_progress_bars_collection() {
366 let mut bars = ProgressBars::new();
367 bars.add_bar("Algorithm1", 0.3);
368 bars.add_bar("Algorithm2", 0.7);
369
370 assert_eq!(bars.len(), 2);
371 assert!(!bars.is_empty());
372
373 bars.add_bar("Algorithm1", 0.5);
375 assert_eq!(bars.len(), 2); }
377
378 #[test]
379 fn test_render_widget() {
380 let bar = ProgressBar::new()
381 .progress(0.5)
382 .label("Test Bar")
383 .show_percentage(true);
384
385 let area = Rect::new(0, 0, 20, 3);
386 let mut buffer = Buffer::empty(area);
387
388 bar.render_widget(area, &mut buffer);
389
390 let content = buffer.content();
392 assert!(!content.is_empty());
393 }
394
395 #[test]
396 fn test_render_progress_bars_collection() {
397 let mut bars = ProgressBars::new();
398 bars.add_bar("Quick Sort", 0.8);
399 bars.add_bar("Merge Sort", 0.4);
400 bars.add_bar("Bubble Sort", 0.2);
401
402 let area = Rect::new(0, 0, 30, 10);
403 let mut buffer = Buffer::empty(area);
404
405 bars.render_widget(area, &mut buffer);
406
407 let content = buffer.content();
409 assert!(!content.is_empty());
410 }
411
412 #[test]
413 fn test_clear_bars() {
414 let mut bars = ProgressBars::new();
415 bars.add_bar("Test", 0.5);
416 assert_eq!(bars.len(), 1);
417
418 bars.clear();
419 assert_eq!(bars.len(), 0);
420 assert!(bars.is_empty());
421 }
422
423 #[test]
424 fn test_small_area_handling() {
425 let bar = ProgressBar::new().progress(0.5);
426
427 let tiny_area = Rect::new(0, 0, 1, 1);
429 let mut buffer = Buffer::empty(tiny_area);
430 bar.render_widget(tiny_area, &mut buffer);
431
432 }
434}