ratatui_interact/components/
progress.rs1use ratatui::{
28 buffer::Buffer,
29 layout::Rect,
30 style::{Color, Modifier, Style},
31 text::Span,
32 widgets::{Block, Borders, Gauge, Widget},
33};
34
35#[derive(Debug, Clone)]
37pub struct ProgressStyle {
38 pub filled_color: Color,
40 pub unfilled_color: Color,
42 pub label_style: Style,
44 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 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 pub fn success() -> Self {
73 Self::default()
74 }
75
76 pub fn warning() -> Self {
78 Self {
79 filled_color: Color::Yellow,
80 ..Default::default()
81 }
82 }
83
84 pub fn error() -> Self {
86 Self {
87 filled_color: Color::Red,
88 ..Default::default()
89 }
90 }
91
92 pub fn info() -> Self {
94 Self {
95 filled_color: Color::Cyan,
96 ..Default::default()
97 }
98 }
99
100 pub fn bordered(mut self, bordered: bool) -> Self {
102 self.bordered = bordered;
103 self
104 }
105}
106
107#[derive(Debug, Clone)]
111pub struct Progress<'a> {
112 ratio: f64,
114 label: Option<&'a str>,
116 steps: Option<(usize, usize)>,
118 style: ProgressStyle,
120}
121
122impl<'a> Progress<'a> {
123 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 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 pub fn label(mut self, label: &'a str) -> Self {
145 self.label = Some(label);
146 self
147 }
148
149 pub fn steps(mut self, current: usize, total: usize) -> Self {
151 self.steps = Some((current, total));
152 self
153 }
154
155 pub fn style(mut self, style: ProgressStyle) -> Self {
157 self.style = style;
158 self
159 }
160
161 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 }
249}