sorting_race/lib/
progress.rs

1//! Progress bar widgets for visualizing algorithm completion
2
3use ratatui::{
4    buffer::Buffer,
5    layout::Rect,
6    style::{Color, Style},
7    widgets::{Block, Widget},
8};
9
10/// A single progress bar
11#[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    /// Create a new progress bar
25    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    /// Set the progress (0.0 to 1.0)
39    pub fn progress(mut self, progress: f32) -> Self {
40        self.progress = progress.clamp(0.0, 1.0);
41        self
42    }
43
44    /// Set the label
45    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    /// Set the base style
54    pub fn style(mut self, style: Style) -> Self {
55        self.style = style;
56        self
57    }
58
59    /// Set the style for filled portion
60    pub fn filled_style(mut self, style: Style) -> Self {
61        self.filled_style = style;
62        self
63    }
64
65    /// Set the style for empty portion
66    pub fn empty_style(mut self, style: Style) -> Self {
67        self.empty_style = style;
68        self
69    }
70
71    /// Set the block
72    pub fn block(mut self, block: Block<'static>) -> Self {
73        self.block = Some(block);
74        self
75    }
76
77    /// Set whether to show percentage
78    pub fn show_percentage(mut self, show: bool) -> Self {
79        self.show_percentage = show;
80        self
81    }
82
83    /// Set whether to show label
84    pub fn show_label(mut self, show: bool) -> Self {
85        self.show_label = show;
86        self
87    }
88
89    /// Render the progress bar to a buffer
90    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        // Calculate space for progress bar
108        let mut bar_area = inner_area;
109        let mut info_area = None;
110
111        // Reserve space for label and percentage if needed
112        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        // Render the progress bar
124        let filled_width = (self.progress * bar_area.width as f32) as u16;
125        let _empty_width = bar_area.width - filled_width;
126
127        // Fill the filled portion
128        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            // Fill the empty portion
138            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        // Render label and percentage
146        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            // Center the info text
161            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/// Collection of progress bars for multiple algorithms
199#[derive(Debug, Clone)]
200pub struct ProgressBars {
201    bars: Vec<(String, ProgressBar)>,
202    style: Style,
203    block: Option<Block<'static>>,
204}
205
206impl ProgressBars {
207    /// Create a new progress bars collection
208    pub fn new() -> Self {
209        Self {
210            bars: Vec::new(),
211            style: Style::default(),
212            block: None,
213        }
214    }
215
216    /// Add or update a progress bar
217    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        // Check if bar already exists
224        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        // Add new bar
234        let bar = ProgressBar::new()
235            .label(&name_string)
236            .progress(progress);
237        self.bars.push((name_string, bar));
238    }
239
240    /// Set the base style
241    pub fn style(mut self, style: Style) -> Self {
242        self.style = style;
243        self
244    }
245
246    /// Set the block
247    pub fn block(mut self, block: Block<'static>) -> Self {
248        self.block = Some(block);
249        self
250    }
251
252    /// Clear all bars
253    pub fn clear(&mut self) {
254        self.bars.clear();
255    }
256
257    /// Get number of bars
258    pub fn len(&self) -> usize {
259        self.bars.len()
260    }
261
262    /// Check if empty
263    pub fn is_empty(&self) -> bool {
264        self.bars.is_empty()
265    }
266
267    /// Render the progress bars to a buffer
268    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 // Space for bar and info
287        } else {
288            1 // Just the bar
289        };
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        // Update existing bar
374        bars.add_bar("Algorithm1", 0.5);
375        assert_eq!(bars.len(), 2); // Should still be 2
376    }
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        // Should not panic and should have some content
391        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        // Should not panic and should have some content
408        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        // Test very small area
428        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        // Should handle gracefully without panicking
433    }
434}