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 From<&crate::theme::Theme> for ProgressStyle {
62 fn from(theme: &crate::theme::Theme) -> Self {
63 let p = &theme.palette;
64 Self {
65 filled_color: p.success,
66 unfilled_color: p.text_disabled,
67 label_style: Style::default().fg(p.text).add_modifier(Modifier::BOLD),
68 bordered: true,
69 }
70 }
71}
72
73impl ProgressStyle {
74 pub fn new(filled: Color, unfilled: Color) -> Self {
76 Self {
77 filled_color: filled,
78 unfilled_color: unfilled,
79 ..Default::default()
80 }
81 }
82
83 pub fn success() -> Self {
85 Self::default()
86 }
87
88 pub fn warning() -> Self {
90 Self {
91 filled_color: Color::Yellow,
92 ..Default::default()
93 }
94 }
95
96 pub fn error() -> Self {
98 Self {
99 filled_color: Color::Red,
100 ..Default::default()
101 }
102 }
103
104 pub fn info() -> Self {
106 Self {
107 filled_color: Color::Cyan,
108 ..Default::default()
109 }
110 }
111
112 pub fn bordered(mut self, bordered: bool) -> Self {
114 self.bordered = bordered;
115 self
116 }
117}
118
119#[derive(Debug, Clone)]
123pub struct Progress<'a> {
124 ratio: f64,
126 label: Option<&'a str>,
128 steps: Option<(usize, usize)>,
130 style: ProgressStyle,
132}
133
134impl<'a> Progress<'a> {
135 pub fn new(ratio: f64) -> Self {
137 Self {
138 ratio: ratio.clamp(0.0, 1.0),
139 label: None,
140 steps: None,
141 style: ProgressStyle::default(),
142 }
143 }
144
145 pub fn from_steps(current: usize, total: usize) -> Self {
147 let ratio = if total > 0 {
148 current as f64 / total as f64
149 } else {
150 0.0
151 };
152 Self::new(ratio).steps(current, total)
153 }
154
155 pub fn label(mut self, label: &'a str) -> Self {
157 self.label = Some(label);
158 self
159 }
160
161 pub fn steps(mut self, current: usize, total: usize) -> Self {
163 self.steps = Some((current, total));
164 self
165 }
166
167 pub fn style(mut self, style: ProgressStyle) -> Self {
169 self.style = style;
170 self
171 }
172
173 pub fn theme(self, theme: &crate::theme::Theme) -> Self {
175 self.style(ProgressStyle::from(theme))
176 }
177
178 fn build_label(&self) -> String {
180 let percent = (self.ratio * 100.0) as u16;
181
182 match (&self.label, &self.steps) {
183 (Some(label), Some((current, total))) => {
184 format!("{} - {}/{} steps ({}%)", label, current, total, percent)
185 }
186 (Some(label), None) => {
187 format!("{} ({}%)", label, percent)
188 }
189 (None, Some((current, total))) => {
190 format!("{}/{} ({}%)", current, total, percent)
191 }
192 (None, None) => {
193 format!("{}%", percent)
194 }
195 }
196 }
197}
198
199impl Widget for Progress<'_> {
200 fn render(self, area: Rect, buf: &mut Buffer) {
201 let label = self.build_label();
202 let label_span = Span::styled(label, self.style.label_style);
203
204 let mut gauge = Gauge::default()
205 .gauge_style(
206 Style::default()
207 .fg(self.style.filled_color)
208 .bg(self.style.unfilled_color),
209 )
210 .percent((self.ratio * 100.0) as u16)
211 .label(label_span);
212
213 if self.style.bordered {
214 gauge = gauge.block(Block::default().borders(Borders::ALL));
215 }
216
217 gauge.render(area, buf);
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn test_progress_new() {
227 let p = Progress::new(0.5);
228 assert!((p.ratio - 0.5).abs() < 0.001);
229 }
230
231 #[test]
232 fn test_progress_clamp() {
233 let p = Progress::new(1.5);
234 assert!((p.ratio - 1.0).abs() < 0.001);
235
236 let p = Progress::new(-0.5);
237 assert!((p.ratio - 0.0).abs() < 0.001);
238 }
239
240 #[test]
241 fn test_progress_from_steps() {
242 let p = Progress::from_steps(5, 10);
243 assert!((p.ratio - 0.5).abs() < 0.001);
244 assert_eq!(p.steps, Some((5, 10)));
245 }
246
247 #[test]
248 fn test_progress_label() {
249 let p = Progress::new(0.75).label("Building");
250 assert_eq!(p.build_label(), "Building (75%)");
251 }
252
253 #[test]
254 fn test_progress_label_with_steps() {
255 let p = Progress::new(0.5).label("Processing").steps(5, 10);
256 assert_eq!(p.build_label(), "Processing - 5/10 steps (50%)");
257 }
258
259 #[test]
260 fn test_progress_render() {
261 let mut buf = Buffer::empty(Rect::new(0, 0, 40, 3));
262 let progress = Progress::new(0.5).label("Test");
263 progress.render(Rect::new(0, 0, 40, 3), &mut buf);
264 }
266}