1use crate::config::{Config, ProgressBarPosition, ProgressBarStyle};
7pub use par_term_emu_core_rust::terminal::NamedProgressBar;
8use par_term_emu_core_rust::terminal::{ProgressBar, ProgressState};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
16pub struct ProgressBarSnapshot {
17 pub simple: ProgressBar,
19 pub named: HashMap<String, NamedProgressBar>,
21}
22
23impl ProgressBarSnapshot {
24 pub fn has_active(&self) -> bool {
26 self.simple.is_active() || self.named.values().any(|b| b.state.is_active())
27 }
28}
29
30pub fn render_progress_bars(
32 ctx: &egui::Context,
33 snapshot: &ProgressBarSnapshot,
34 config: &Config,
35 window_width: f32,
36 window_height: f32,
37) {
38 if !config.progress_bar_enabled || !snapshot.has_active() {
39 return;
40 }
41
42 let bar_height = config.progress_bar_height;
43 let alpha = (config.progress_bar_opacity * 255.0) as u8;
44
45 let base_y = match config.progress_bar_position {
47 ProgressBarPosition::Top => 0.0,
48 ProgressBarPosition::Bottom => window_height - bar_height,
49 };
50
51 let mut bars: Vec<BarRenderInfo> = Vec::new();
53
54 if snapshot.simple.is_active() {
55 bars.push(BarRenderInfo {
56 state: snapshot.simple.state,
57 percent: snapshot.simple.progress,
58 label: None,
59 });
60 }
61
62 let mut named_sorted: Vec<_> = snapshot
63 .named
64 .values()
65 .filter(|b| b.state.is_active())
66 .collect();
67 named_sorted.sort_by(|a, b| a.id.cmp(&b.id));
68 for bar in named_sorted {
69 bars.push(BarRenderInfo {
70 state: bar.state,
71 percent: bar.percent,
72 label: bar.label.as_deref(),
73 });
74 }
75
76 if bars.is_empty() {
77 return;
78 }
79
80 let total_height = bar_height * bars.len() as f32;
82 let stacked_y = match config.progress_bar_position {
83 ProgressBarPosition::Top => base_y,
84 ProgressBarPosition::Bottom => window_height - total_height,
85 };
86
87 egui::Area::new(egui::Id::new("progress_bar_overlay"))
88 .fixed_pos(egui::pos2(0.0, stacked_y))
89 .order(egui::Order::Foreground)
90 .interactable(false)
91 .show(ctx, |ui| {
92 let painter = ui.painter();
93
94 for (i, bar) in bars.iter().enumerate() {
95 let y_offset = i as f32 * bar_height;
96 let bar_y = stacked_y + y_offset;
97
98 let color = state_color(bar.state, config, alpha);
99 let bg_color = egui::Color32::from_rgba_unmultiplied(0, 0, 0, alpha / 2);
100
101 painter.rect_filled(
103 egui::Rect::from_min_size(
104 egui::pos2(0.0, bar_y),
105 egui::vec2(window_width, bar_height),
106 ),
107 0.0,
108 bg_color,
109 );
110
111 if bar.state == ProgressState::Indeterminate {
112 let time = ctx.input(|i| i.time) as f32;
114 let segments = (window_width / 2.0).max(64.0) as usize;
115 let seg_width = window_width / segments as f32;
116
117 for s in 0..segments {
118 let t = s as f32 / segments as f32;
119 let phase = (t * std::f32::consts::TAU * 2.0) - (time * 3.0);
121 let brightness = phase.sin() * 0.5 + 0.5; let seg_alpha = (alpha as f32 * (0.25 + 0.75 * brightness)) as u8;
123 let seg_color = egui::Color32::from_rgba_unmultiplied(
124 color.r(),
125 color.g(),
126 color.b(),
127 seg_alpha,
128 );
129 painter.rect_filled(
130 egui::Rect::from_min_size(
131 egui::pos2(s as f32 * seg_width, bar_y),
132 egui::vec2(seg_width + 1.0, bar_height),
133 ),
134 0.0,
135 seg_color,
136 );
137 }
138 ctx.request_repaint();
139 } else {
140 let fill_width = window_width * (bar.percent as f32 / 100.0);
142 painter.rect_filled(
143 egui::Rect::from_min_size(
144 egui::pos2(0.0, bar_y),
145 egui::vec2(fill_width, bar_height),
146 ),
147 0.0,
148 color,
149 );
150 }
151
152 if config.progress_bar_style == ProgressBarStyle::BarWithText && bar_height >= 10.0
154 {
155 let text = if let Some(label) = bar.label {
156 if bar.state == ProgressState::Indeterminate {
157 label.to_string()
158 } else {
159 format!("{} {}%", label, bar.percent)
160 }
161 } else if bar.state == ProgressState::Indeterminate {
162 String::new()
163 } else {
164 format!("{}%", bar.percent)
165 };
166
167 if !text.is_empty() {
168 let font_size = (bar_height - 2.0).clamp(8.0, 12.0);
169 let font_id = egui::FontId::new(font_size, egui::FontFamily::Proportional);
170 let text_color = egui::Color32::WHITE;
171 painter.text(
172 egui::pos2(6.0, bar_y + bar_height / 2.0),
173 egui::Align2::LEFT_CENTER,
174 &text,
175 font_id,
176 text_color,
177 );
178 }
179 }
180 }
181 });
182}
183
184struct BarRenderInfo<'a> {
186 state: ProgressState,
187 percent: u8,
188 label: Option<&'a str>,
189}
190
191fn state_color(state: ProgressState, config: &Config, alpha: u8) -> egui::Color32 {
193 let rgb = match state {
194 ProgressState::Normal => config.progress_bar_normal_color,
195 ProgressState::Warning => config.progress_bar_warning_color,
196 ProgressState::Error => config.progress_bar_error_color,
197 ProgressState::Indeterminate => config.progress_bar_indeterminate_color,
198 ProgressState::Hidden => [0, 0, 0],
199 };
200 egui::Color32::from_rgba_unmultiplied(rgb[0], rgb[1], rgb[2], alpha)
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_snapshot_has_active_empty() {
209 let snap = ProgressBarSnapshot {
210 simple: ProgressBar::hidden(),
211 named: HashMap::new(),
212 };
213 assert!(!snap.has_active());
214 }
215
216 #[test]
217 fn test_snapshot_has_active_simple() {
218 let snap = ProgressBarSnapshot {
219 simple: ProgressBar::normal(50),
220 named: HashMap::new(),
221 };
222 assert!(snap.has_active());
223 }
224
225 #[test]
226 fn test_snapshot_has_active_named() {
227 let mut named = HashMap::new();
228 named.insert(
229 "test".to_string(),
230 NamedProgressBar {
231 id: "test".to_string(),
232 state: ProgressState::Normal,
233 percent: 50,
234 label: Some("Testing".to_string()),
235 },
236 );
237 let snap = ProgressBarSnapshot {
238 simple: ProgressBar::hidden(),
239 named,
240 };
241 assert!(snap.has_active());
242 }
243
244 #[test]
245 fn test_state_color_normal() {
246 let config = Config::default();
247 let color = state_color(ProgressState::Normal, &config, 255);
248 assert_eq!(
249 color,
250 egui::Color32::from_rgba_unmultiplied(
251 config.progress_bar_normal_color[0],
252 config.progress_bar_normal_color[1],
253 config.progress_bar_normal_color[2],
254 255,
255 )
256 );
257 }
258
259 #[test]
260 fn test_state_color_warning() {
261 let config = Config::default();
262 let color = state_color(ProgressState::Warning, &config, 200);
263 assert_eq!(
264 color,
265 egui::Color32::from_rgba_unmultiplied(
266 config.progress_bar_warning_color[0],
267 config.progress_bar_warning_color[1],
268 config.progress_bar_warning_color[2],
269 200,
270 )
271 );
272 }
273
274 #[test]
275 fn test_state_color_error() {
276 let config = Config::default();
277 let color = state_color(ProgressState::Error, &config, 128);
278 assert_eq!(
279 color,
280 egui::Color32::from_rgba_unmultiplied(
281 config.progress_bar_error_color[0],
282 config.progress_bar_error_color[1],
283 config.progress_bar_error_color[2],
284 128,
285 )
286 );
287 }
288}