Skip to main content

ratatui_interact/components/
progress.rs

1//! Progress bar widget
2//!
3//! A styled progress bar with label and step counter support.
4//!
5//! # Example
6//!
7//! ```rust
8//! use ratatui_interact::components::{Progress, ProgressStyle};
9//! use ratatui::layout::Rect;
10//! use ratatui::buffer::Buffer;
11//! use ratatui::widgets::Widget;
12//!
13//! // Simple progress bar
14//! let progress = Progress::new(0.75)
15//!     .label("Processing");
16//!
17//! // With step counter
18//! let progress = Progress::new(0.5)
19//!     .label("Building")
20//!     .steps(5, 10);
21//!
22//! // Custom style
23//! let progress = Progress::new(0.25)
24//!     .style(ProgressStyle::warning());
25//! ```
26
27use ratatui::{
28    buffer::Buffer,
29    layout::Rect,
30    style::{Color, Modifier, Style},
31    text::Span,
32    widgets::{Block, Borders, Gauge, Widget},
33};
34
35/// Style configuration for progress bars
36#[derive(Debug, Clone)]
37pub struct ProgressStyle {
38    /// Foreground color of the filled portion
39    pub filled_color: Color,
40    /// Background color of the unfilled portion
41    pub unfilled_color: Color,
42    /// Style for the label text
43    pub label_style: Style,
44    /// Whether to show borders
45    pub bordered: bool,
46}
47
48impl Default for ProgressStyle {
49    fn default() -> Self {
50        Self {
51            filled_color: Color::Green,
52            unfilled_color: Color::DarkGray,
53            label_style: Style::default()
54                .fg(Color::White)
55                .add_modifier(Modifier::BOLD),
56            bordered: true,
57        }
58    }
59}
60
61impl ProgressStyle {
62    /// Create a new progress style with custom colors
63    pub fn new(filled: Color, unfilled: Color) -> Self {
64        Self {
65            filled_color: filled,
66            unfilled_color: unfilled,
67            ..Default::default()
68        }
69    }
70
71    /// Success style (green)
72    pub fn success() -> Self {
73        Self::default()
74    }
75
76    /// Warning style (yellow)
77    pub fn warning() -> Self {
78        Self {
79            filled_color: Color::Yellow,
80            ..Default::default()
81        }
82    }
83
84    /// Error style (red)
85    pub fn error() -> Self {
86        Self {
87            filled_color: Color::Red,
88            ..Default::default()
89        }
90    }
91
92    /// Info style (cyan)
93    pub fn info() -> Self {
94        Self {
95            filled_color: Color::Cyan,
96            ..Default::default()
97        }
98    }
99
100    /// Set whether to show borders
101    pub fn bordered(mut self, bordered: bool) -> Self {
102        self.bordered = bordered;
103        self
104    }
105}
106
107/// A progress bar widget with label and step counter support.
108///
109/// The progress value should be between 0.0 and 1.0.
110#[derive(Debug, Clone)]
111pub struct Progress<'a> {
112    /// Progress value (0.0 to 1.0)
113    ratio: f64,
114    /// Optional label text
115    label: Option<&'a str>,
116    /// Optional step counter (current, total)
117    steps: Option<(usize, usize)>,
118    /// Style configuration
119    style: ProgressStyle,
120}
121
122impl<'a> Progress<'a> {
123    /// Create a new progress bar with the given ratio (0.0 to 1.0)
124    pub fn new(ratio: f64) -> Self {
125        Self {
126            ratio: ratio.clamp(0.0, 1.0),
127            label: None,
128            steps: None,
129            style: ProgressStyle::default(),
130        }
131    }
132
133    /// Create a progress bar from current/total values
134    pub fn from_steps(current: usize, total: usize) -> Self {
135        let ratio = if total > 0 {
136            current as f64 / total as f64
137        } else {
138            0.0
139        };
140        Self::new(ratio).steps(current, total)
141    }
142
143    /// Set the label text
144    pub fn label(mut self, label: &'a str) -> Self {
145        self.label = Some(label);
146        self
147    }
148
149    /// Set the step counter (current step, total steps)
150    pub fn steps(mut self, current: usize, total: usize) -> Self {
151        self.steps = Some((current, total));
152        self
153    }
154
155    /// Set the style
156    pub fn style(mut self, style: ProgressStyle) -> Self {
157        self.style = style;
158        self
159    }
160
161    /// Build the label string
162    fn build_label(&self) -> String {
163        let percent = (self.ratio * 100.0) as u16;
164
165        match (&self.label, &self.steps) {
166            (Some(label), Some((current, total))) => {
167                format!("{} - {}/{} steps ({}%)", label, current, total, percent)
168            }
169            (Some(label), None) => {
170                format!("{} ({}%)", label, percent)
171            }
172            (None, Some((current, total))) => {
173                format!("{}/{} ({}%)", current, total, percent)
174            }
175            (None, None) => {
176                format!("{}%", percent)
177            }
178        }
179    }
180}
181
182impl Widget for Progress<'_> {
183    fn render(self, area: Rect, buf: &mut Buffer) {
184        let label = self.build_label();
185        let label_span = Span::styled(label, self.style.label_style);
186
187        let mut gauge = Gauge::default()
188            .gauge_style(
189                Style::default()
190                    .fg(self.style.filled_color)
191                    .bg(self.style.unfilled_color),
192            )
193            .percent((self.ratio * 100.0) as u16)
194            .label(label_span);
195
196        if self.style.bordered {
197            gauge = gauge.block(Block::default().borders(Borders::ALL));
198        }
199
200        gauge.render(area, buf);
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_progress_new() {
210        let p = Progress::new(0.5);
211        assert!((p.ratio - 0.5).abs() < 0.001);
212    }
213
214    #[test]
215    fn test_progress_clamp() {
216        let p = Progress::new(1.5);
217        assert!((p.ratio - 1.0).abs() < 0.001);
218
219        let p = Progress::new(-0.5);
220        assert!((p.ratio - 0.0).abs() < 0.001);
221    }
222
223    #[test]
224    fn test_progress_from_steps() {
225        let p = Progress::from_steps(5, 10);
226        assert!((p.ratio - 0.5).abs() < 0.001);
227        assert_eq!(p.steps, Some((5, 10)));
228    }
229
230    #[test]
231    fn test_progress_label() {
232        let p = Progress::new(0.75).label("Building");
233        assert_eq!(p.build_label(), "Building (75%)");
234    }
235
236    #[test]
237    fn test_progress_label_with_steps() {
238        let p = Progress::new(0.5).label("Processing").steps(5, 10);
239        assert_eq!(p.build_label(), "Processing - 5/10 steps (50%)");
240    }
241
242    #[test]
243    fn test_progress_render() {
244        let mut buf = Buffer::empty(Rect::new(0, 0, 40, 3));
245        let progress = Progress::new(0.5).label("Test");
246        progress.render(Rect::new(0, 0, 40, 3), &mut buf);
247        // Just verify it doesn't panic
248    }
249}