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            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        // Render the progress bar
125        let filled_width = (self.progress * bar_area.width as f32) as u16;
126        let _empty_width = bar_area.width - filled_width;
127
128        // Fill the filled portion
129        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            // Fill the empty portion
139            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        // Render label and percentage
147        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            // Center the info text
162            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/// Collection of progress bars for multiple algorithms
200#[derive(Debug, Clone)]
201pub struct ProgressBars {
202    bars: Vec<(String, ProgressBar)>,
203    style: Style,
204    block: Option<Block<'static>>,
205}
206
207impl ProgressBars {
208    /// Create a new progress bars collection
209    pub fn new() -> Self {
210        Self {
211            bars: Vec::new(),
212            style: Style::default(),
213            block: None,
214        }
215    }
216
217    /// Add or update a progress bar
218    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        // Check if bar already exists
225        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        // Add new bar
235        let bar = ProgressBar::new()
236            .label(&name_string)
237            .progress(progress);
238        self.bars.push((name_string, bar));
239    }
240
241    /// Set the base style
242    pub fn style(mut self, style: Style) -> Self {
243        self.style = style;
244        self
245    }
246
247    /// Set the block
248    pub fn block(mut self, block: Block<'static>) -> Self {
249        self.block = Some(block);
250        self
251    }
252
253    /// Clear all bars
254    pub fn clear(&mut self) {
255        self.bars.clear();
256    }
257
258    /// Get number of bars
259    pub fn len(&self) -> usize {
260        self.bars.len()
261    }
262
263    /// Check if empty
264    pub fn is_empty(&self) -> bool {
265        self.bars.is_empty()
266    }
267
268    /// Render the progress bars to a buffer
269    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 // Space for bar and info
288        } else {
289            1 // Just the bar
290        };
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        // Update existing bar
375        bars.add_bar("Algorithm1", 0.5);
376        assert_eq!(bars.len(), 2); // Should still be 2
377    }
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        // Should not panic and should have some content
392        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        // Should not panic and should have some content
409        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        // Test very small area
429        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        // Should handle gracefully without panicking
434    }
435}