irox_egui_extras/
progresswindow.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2025 IROX Contributors
3//
4
5use 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            // .desired_width(desired_width)
155            .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}