1use crate::actor::InputEvent;
7use crate::buffer::{Buffer, Cell, Rgb};
8use crate::layout::Rect;
9use super::traits::Widget;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[derive(Default)]
14pub enum ProgressStyle {
15 Solid,
17 Ascii,
19 #[default]
21 Block,
22 Line,
24}
25
26
27#[derive(Debug, Clone)]
29pub struct ProgressBarConfig {
30 pub style: ProgressStyle,
32 pub filled_fg: Rgb,
34 pub empty_fg: Rgb,
36 pub bg: Rgb,
38 pub show_percentage: bool,
40 pub percentage_fg: Rgb,
42 pub label: Option<String>,
44 pub label_fg: Rgb,
46}
47
48impl Default for ProgressBarConfig {
49 fn default() -> Self {
50 Self {
51 style: ProgressStyle::Block,
52 filled_fg: Rgb::new(0, 200, 100),
53 empty_fg: Rgb::new(60, 60, 60),
54 bg: Rgb::new(30, 30, 30),
55 show_percentage: true,
56 percentage_fg: Rgb::WHITE,
57 label: None,
58 label_fg: Rgb::new(150, 150, 150),
59 }
60 }
61}
62
63#[derive(Debug)]
65pub struct ProgressBar {
66 progress: f32,
68 bounds: Rect,
70 config: ProgressBarConfig,
72 dirty: bool,
74}
75
76impl ProgressBar {
77 pub fn new(bounds: Rect) -> Self {
79 Self {
80 progress: 0.0,
81 bounds,
82 config: ProgressBarConfig::default(),
83 dirty: true,
84 }
85 }
86
87 pub const fn with_config(bounds: Rect, config: ProgressBarConfig) -> Self {
89 Self {
90 progress: 0.0,
91 bounds,
92 config,
93 dirty: true,
94 }
95 }
96
97 pub const fn set_progress(&mut self, progress: f32) {
99 self.progress = progress.clamp(0.0, 1.0);
100 self.dirty = true;
101 }
102
103 pub const fn progress(&self) -> f32 {
105 self.progress
106 }
107
108 pub fn set_label(&mut self, label: impl Into<String>) {
110 self.config.label = Some(label.into());
111 self.dirty = true;
112 }
113
114 pub fn clear_label(&mut self) {
116 self.config.label = None;
117 self.dirty = true;
118 }
119
120 pub fn increment(&mut self, delta: f32) {
122 self.set_progress(self.progress + delta);
123 }
124
125 pub fn is_complete(&self) -> bool {
127 self.progress >= 1.0
128 }
129
130 const fn style_chars(&self) -> (char, char) {
132 match self.config.style {
133 ProgressStyle::Solid => ('█', '░'),
134 ProgressStyle::Ascii => ('=', ' '),
135 ProgressStyle::Block => ('▓', '░'),
136 ProgressStyle::Line => ('─', '─'),
137 }
138 }
139}
140
141impl Widget for ProgressBar {
142 fn bounds(&self) -> Rect {
143 self.bounds
144 }
145
146 fn set_bounds(&mut self, bounds: Rect) {
147 self.bounds = bounds;
148 self.dirty = true;
149 }
150
151 #[allow(clippy::cast_possible_truncation)]
152 #[allow(clippy::cast_sign_loss)]
153 #[allow(clippy::cast_precision_loss)]
154 fn render(&self, buffer: &mut Buffer) {
155 let x = self.bounds.x;
156 let y = self.bounds.y;
157 let width = self.bounds.width as usize;
158
159 for i in 0..self.bounds.width {
161 buffer.set(x + i, y, Cell::new(' ').with_bg(self.config.bg));
162 }
163
164 let label_len = self.config.label.as_ref().map_or(0, |l| l.chars().count() + 1);
166 let pct_len = if self.config.show_percentage { 5 } else { 0 }; let bar_width = width.saturating_sub(label_len + pct_len);
168
169 if bar_width == 0 {
170 return;
171 }
172
173 let mut offset = x;
174
175 if let Some(ref label) = self.config.label {
177 for c in label.chars().take(width / 3) {
178 buffer.set(offset, y, Cell::new(c)
179 .with_fg(self.config.label_fg)
180 .with_bg(self.config.bg));
181 offset += 1;
182 }
183 buffer.set(offset, y, Cell::new(' ').with_bg(self.config.bg));
184 offset += 1;
185 }
186
187 let (filled_char, empty_char) = self.style_chars();
189 let filled_count = (self.progress * bar_width as f32).round() as usize;
190
191 for i in 0..bar_width {
192 let (c, fg) = if i < filled_count {
193 (filled_char, self.config.filled_fg)
194 } else {
195 (empty_char, self.config.empty_fg)
196 };
197 buffer.set(offset + i as u16, y, Cell::new(c)
198 .with_fg(fg)
199 .with_bg(self.config.bg));
200 }
201 offset += bar_width as u16;
202
203 if self.config.show_percentage {
205 let pct = format!(" {:>3}%", (self.progress * 100.0).round() as u32);
206 for c in pct.chars() {
207 buffer.set(offset, y, Cell::new(c)
208 .with_fg(self.config.percentage_fg)
209 .with_bg(self.config.bg));
210 offset += 1;
211 }
212 }
213 }
214
215 fn handle_input(&mut self, _event: &InputEvent) -> bool {
216 false
218 }
219
220 fn needs_redraw(&self) -> bool {
221 self.dirty
222 }
223
224 fn clear_redraw(&mut self) {
225 self.dirty = false;
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_progress_bar_basic() {
235 let mut bar = ProgressBar::new(Rect::new(0, 0, 80, 1));
236
237 assert_eq!(bar.progress(), 0.0);
238
239 bar.set_progress(0.5);
240 assert_eq!(bar.progress(), 0.5);
241
242 bar.set_progress(1.5); assert_eq!(bar.progress(), 1.0);
244 }
245
246 #[test]
247 fn test_progress_bar_increment() {
248 let mut bar = ProgressBar::new(Rect::new(0, 0, 80, 1));
249
250 bar.increment(0.25);
251 assert!((bar.progress() - 0.25).abs() < f32::EPSILON);
252
253 bar.increment(0.25);
254 assert!((bar.progress() - 0.5).abs() < f32::EPSILON);
255 }
256
257 #[test]
258 fn test_progress_bar_complete() {
259 let mut bar = ProgressBar::new(Rect::new(0, 0, 80, 1));
260
261 assert!(!bar.is_complete());
262
263 bar.set_progress(1.0);
264 assert!(bar.is_complete());
265 }
266}