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