irox_egui_extras/
progresswindow.rs1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
6use std::sync::{Arc, Mutex, RwLock};
7use std::thread::JoinHandle;
8use std::time::Duration;
9
10use crate::progressbar::ProgressBar;
11use egui::collapsing_header::CollapsingState;
12use egui::{Align, Context, CursorIcon, Layout, Ui};
13use irox_progress::{get_human, ProgressPrinter, Task};
14use irox_time::format::iso8601::ISO8601Duration;
15use irox_time::format::Format;
16
17#[derive(Clone)]
18pub struct EguiProgressWindow {
19 completed: Arc<AtomicU64>,
20 tasks: Arc<RwLock<Vec<Task>>>,
21 running: Arc<AtomicBool>,
22 any_tasks_active: Arc<AtomicBool>,
23 handle: Arc<Mutex<Option<JoinHandle<()>>>>,
24}
25
26impl EguiProgressWindow {
27 pub fn new(context: Context) -> EguiProgressWindow {
28 let r2 = Arc::new(AtomicBool::new(true));
29 let running = r2.clone();
30 let active = Arc::new(AtomicBool::new(false));
31 let a2 = active.clone();
32 let handle = std::thread::spawn(move || {
33 while r2.load(Ordering::Relaxed) {
34 let millis = if a2.load(Ordering::Relaxed) { 50 } else { 1000 };
35 std::thread::sleep(Duration::from_millis(millis));
36 context.request_repaint();
37 }
38 });
39 EguiProgressWindow {
40 completed: Arc::new(AtomicU64::new(0)),
41 tasks: Arc::new(RwLock::new(Vec::new())),
42 handle: Arc::new(Mutex::new(Some(handle))),
43 any_tasks_active: active,
44 running,
45 }
46 }
47}
48
49impl Drop for EguiProgressWindow {
50 fn drop(&mut self) {
51 self.running.store(false, Ordering::Relaxed);
52 if let Ok(mut handle) = self.handle.lock() {
53 if let Some(handle) = handle.take() {
54 let _ok = handle.join();
55 }
56 }
57 }
58}
59
60impl EguiProgressWindow {
61 pub fn ui(&self, ui: &mut Ui) {
62 let tasks = self.tasks.clone();
63 let Ok(mut tasks) = tasks.write() else {
64 return;
65 };
66 ui.allocate_ui_with_layout(
67 ui.available_size(),
68 Layout::top_down_justified(Align::Min),
69 |ui| {
70 ui.horizontal(|ui| {
71 ui.label(format!(
72 "{} tasks completed, {} tasks pending",
73 self.completed.load(Ordering::Relaxed),
74 tasks.len()
75 ));
76 ui.allocate_ui_with_layout(
77 ui.available_size_before_wrap(),
78 Layout::right_to_left(Align::Center),
79 |ui| {
80 if ui
81 .button("\u{1F5D9}*")
82 .on_hover_text("Cancel all tasks")
83 .clicked()
84 {
85 tasks.iter().for_each(Task::cancel);
86 }
87 },
88 );
89 });
90
91 let mut any_tasks_active = false;
92 tasks.retain(|task| {
93 let active = self.paint_task(ui, task);
94 any_tasks_active |= active;
95 active
96 });
97 self.any_tasks_active
98 .store(any_tasks_active, Ordering::Relaxed);
99 },
100 );
101 }
102
103 fn get_speed_text(task: &Task) -> String {
104 if let Some(started) = task.get_started() {
105 let elapsed = started.elapsed().as_seconds_f64();
106 let avg_per_sec = task.current_progress_count() as f64 / elapsed;
107 let (avg_per_sec, avg_unit) = get_human!(avg_per_sec);
108 return format!("{avg_per_sec:.02}{avg_unit}/s");
109 }
110 String::new()
111 }
112
113 fn paint_finite_header(ui: &mut Ui, task: &Task) {
114 let frac = task.current_progress_frac() as f32;
115 let current = task.current_progress_count();
116 let max = task.max_elements();
117 let name = task.get_name();
118 let current = current as f64;
119 let (current, unit) = get_human!(current);
120
121 let speed = Self::get_speed_text(task);
122
123 let max = max as f64;
124 let (max, maxunit) = get_human!(max);
125 let status = task
126 .current_status()
127 .map(|v| format!(" {v}"))
128 .unwrap_or_default();
129
130 let rem_str = ISO8601Duration.format(&task.get_remaining_time());
131 let left_text = format!("{:<3.0}% {name}{status}", frac * 100.);
132 let right_text = format!("({current:.02}{unit}/{max:.02}{maxunit}) {rem_str} {speed} ");
133 ProgressBar::new(frac)
134 .text_left(left_text)
135 .text_right(right_text)
136 .ui(ui);
137 }
138
139 fn paint_infinite_header(ui: &mut Ui, task: &Task) {
140 let current = task.current_progress_count();
141 let name = task.get_name();
142
143 let current = current as f64;
144 let (current, unit) = get_human!(current);
145 let speed = Self::get_speed_text(task);
146 let status = task
147 .current_status()
148 .map(|v| format!(": {v}"))
149 .unwrap_or_default();
150 let left_text = format!("{name}{status}");
151 let right_text = format!("{current:.02}{unit} {speed}");
152
153 ProgressBar::indeterminate()
154 .text_left(left_text)
156 .text_right(right_text)
157 .ui(ui);
158 }
159
160 fn paint_task(&self, ui: &mut Ui, task: &Task) -> bool {
161 let is_infinite = task.max_elements() == u64::MAX;
162
163 let id = ui.make_persistent_id(task.get_id());
164 CollapsingState::load_with_default_open(ui.ctx(), id, true)
165 .show_header(ui, |ui| {
166 ui.allocate_ui_with_layout(
167 ui.available_size_before_wrap(),
168 Layout::right_to_left(Align::Center),
169 |ui| {
170 if task.is_cancelled() {
171 ui.label("\u{1F6AB}")
172 .on_hover_cursor(CursorIcon::Wait)
173 .on_hover_text("Task cancelled");
174 } else if ui
175 .button("\u{1F5D9}")
176 .on_hover_text("Request Task Cancel")
177 .clicked()
178 {
179 task.cancel();
180 };
181 if is_infinite {
182 Self::paint_infinite_header(ui, task);
183 } else {
184 Self::paint_finite_header(ui, task);
185 }
186 },
187 );
188 })
189 .body(|ui| {
190 task.each_child(|t| {
191 if !t.is_complete() {
192 self.paint_task(ui, t);
193 }
194 });
195 });
196
197 if task.is_complete() {
198 self.completed.fetch_add(1, Ordering::Relaxed);
199 }
200 !task.is_complete()
201 }
202}
203
204impl ProgressPrinter for EguiProgressWindow {
205 fn track_task_progress(&self, task: &Task) {
206 if let Ok(mut tasks) = self.tasks.clone().write() {
207 tasks.push(task.clone())
208 }
209 }
210}