egui_async/
egui.rs

1//! Extension traits for `egui` to add common functionality
2//! Only available when the `egui` feature is enabled.
3//!
4//! `loop_handle()` must be called every frame to update the internal time
5//! and drive the polling mechanism on egui.
6
7use std::fmt::Debug;
8
9use super::bind::{self, Bind, MaybeSend, State};
10
11/// Extension traits for `egui::Context`
12pub trait ContextExt {
13    /// This must be called every frame to update the internal time
14    /// and drive the polling mechanism.
15    fn loop_handle(&self);
16}
17
18impl ContextExt for egui::Context {
19    fn loop_handle(&self) {
20        bind::CTX.get_or_init(|| self.clone());
21        let time = self.input(|i| i.time);
22
23        let last_frame = bind::CURR_FRAME.swap(time, std::sync::atomic::Ordering::Relaxed);
24        bind::LAST_FRAME.store(last_frame, std::sync::atomic::Ordering::Relaxed);
25    }
26}
27
28impl<T: 'static, E: Debug + 'static> Bind<T, E> {
29    /// Reads the data if available, otherwise shows an error popup if there was an error.
30    /// If there was an error, the popup will have a "Retry" button that will trigger the given future.
31    /// If the data is not available, returns None.
32    /// This does NOT automatically request the data if it is not available.
33    pub fn read_or_error<Fut>(&mut self, f: impl FnOnce() -> Fut, ui: &mut egui::Ui) -> Option<&T>
34    where
35        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
36        T: MaybeSend,
37        E: MaybeSend,
38    {
39        self.poll();
40
41        if let Some(Err(e)) = &self.data {
42            let error_string = format!("{e:?}");
43            if ui.popup_error(&error_string) {
44                self.request(f());
45            }
46            None
47        } else if let Some(Ok(data)) = self.data.as_ref() {
48            Some(data)
49        } else {
50            None
51        }
52    }
53
54    /// Reads the data mutably if available, otherwise shows an error popup if there was an error.
55    /// If there was an error, the popup will have a "Retry" button that will
56    /// trigger the given future.
57    /// If the data is not available, returns None.
58    /// This does NOT automatically request the data if it is not available.
59    pub fn read_mut_or_error<Fut>(
60        &mut self,
61        f: impl FnOnce() -> Fut,
62        ui: &mut egui::Ui,
63    ) -> Option<&mut T>
64    where
65        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
66        T: MaybeSend,
67        E: MaybeSend,
68    {
69        self.poll();
70
71        if let Some(Err(e)) = &self.data {
72            let error_string = format!("{e:?}");
73            if ui.popup_error(&error_string) {
74                self.request(f());
75            }
76            None
77        } else if let Some(Ok(data)) = self.data.as_mut() {
78            Some(data)
79        } else {
80            None
81        }
82    }
83
84    /// Reads the data if available, otherwise requests it using the given future.
85    /// If there was an error, the popup will have a "Retry" button that will
86    /// trigger the given future.
87    /// If the data is not available, returns None.
88    /// This automatically requests the data if it is not available.
89    pub fn read_or_request_or_error<Fut>(
90        &mut self,
91        f: impl FnOnce() -> Fut,
92        ui: &mut egui::Ui,
93    ) -> Option<&T>
94    where
95        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
96        T: MaybeSend,
97        E: MaybeSend,
98    {
99        self.poll();
100
101        if matches!(self.state, State::Idle) {
102            self.request(f());
103            None
104        } else if let Some(Err(e)) = &self.data {
105            let error_string = format!("{e:?}");
106            if ui.popup_error(&error_string) {
107                self.request(f());
108            }
109            None
110        } else if let Some(Ok(data)) = self.data.as_ref() {
111            Some(data)
112        } else {
113            None
114        }
115    }
116
117    /// Reads the data mutably if available, otherwise requests it using the given future.
118    /// If there was an error, the popup will have a "Retry" button that will
119    /// trigger the given future.
120    /// If the data is not available, returns None.
121    /// This automatically requests the data if it is not available.
122    pub fn read_mut_or_request_or_error<Fut>(
123        &mut self,
124        f: impl FnOnce() -> Fut,
125        ui: &mut egui::Ui,
126    ) -> Option<&mut T>
127    where
128        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
129        T: MaybeSend,
130        E: MaybeSend,
131    {
132        self.poll();
133
134        if matches!(self.state, State::Idle) {
135            self.request(f());
136            None
137        } else if let Some(Err(e)) = &self.data {
138            let error_string = format!("{e:?}");
139            if ui.popup_error(&error_string) {
140                self.request(f());
141            }
142            None
143        } else if let Some(Ok(data)) = self.data.as_mut() {
144            Some(data)
145        } else {
146            None
147        }
148    }
149}
150
151// After this, it's just some common egui helpers
152// Also serves as more examples.
153
154/// Extension traits for `egui::Ui`
155pub trait UiExt {
156    /// Pops up an error window with the given error message.
157    /// Returns true if the "Retry" button was clicked.
158    fn popup_error(&self, error: &str) -> bool;
159    /// Pops up a notification window with the given info message.
160    /// Returns true if the "Ok" button was clicked.
161    fn popup_notify(&self, info: &str) -> bool;
162
163    /// Adds a refresh button that triggers the given future when clicked.
164    /// Also sets up automatic refreshing every `secs` seconds.
165    /// The button shows a tooltip with the time remaining until the next automatic refresh.
166    /// If the button is clicked, it triggers an immediate refresh, unless the last refresh
167    /// was less than `secs / 4` seconds ago, in which case it does nothing.
168    /// This is to prevent spamming the refresh button.
169    fn refresh_button<T, E, Fut>(
170        &mut self,
171        bind: &mut bind::Bind<T, E>,
172        f: impl FnOnce() -> Fut,
173        secs: f64,
174    ) where
175        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
176        T: MaybeSend + 'static,
177        E: MaybeSend + 'static;
178}
179
180const REFRESH_DEBOUNCE_FACTOR: f64 = 4.0;
181
182impl UiExt for egui::Ui {
183    fn popup_error(&self, error: &str) -> bool {
184        let screen_rect = self.ctx().screen_rect();
185        let total_width = screen_rect.width();
186        let total_height = screen_rect.height();
187
188        let id = egui::Id::new("error_window");
189        egui::Window::new("Error")
190            .id(id)
191            .collapsible(false)
192            .default_width(total_width * 0.25)
193            .default_height(total_height * 0.20)
194            .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
195            .show(self.ctx(), |ui| {
196                ui.vertical_centered(|ui| {
197                    ui.label(egui::RichText::new(error).color(egui::Color32::RED));
198
199                    ui.add_space(10.0);
200
201                    ui.label("Please retry the request, or contact support if the error persists.");
202
203                    ui.add_space(10.0);
204
205                    ui.button("Retry").clicked()
206                })
207                .inner
208            })
209            .is_some_and(|r| r.inner.is_some_and(|r| r))
210    }
211    fn popup_notify(&self, info: &str) -> bool {
212        let screen_rect = self.ctx().screen_rect();
213        let total_width = screen_rect.width();
214        let total_height = screen_rect.height();
215
216        let id = egui::Id::new("notify_window");
217        egui::Window::new("Info")
218            .id(id)
219            .collapsible(false)
220            .default_width(total_width * 0.25)
221            .default_height(total_height * 0.20)
222            .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
223            .show(self.ctx(), |ui| {
224                ui.vertical_centered(|ui| {
225                    ui.label(info);
226
227                    ui.add_space(10.0);
228
229                    ui.button("Ok").clicked()
230                })
231                .inner
232            })
233            .is_some_and(|r| r.inner.is_some_and(|r| r))
234    }
235
236    fn refresh_button<T, E, Fut>(
237        &mut self,
238        bind: &mut bind::Bind<T, E>,
239        f: impl FnOnce() -> Fut,
240        secs: f64,
241    ) where
242        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
243        T: MaybeSend + 'static,
244        E: MaybeSend + 'static,
245    {
246        let resp = self.button("🔄");
247
248        // Only actually refresh when clicked if the last completion was more than 1/4 of the interval ago
249        let diff = if bind.since_completed() > secs / REFRESH_DEBOUNCE_FACTOR && resp.clicked() {
250            bind.refresh(f());
251            -1.0
252        } else {
253            bind.request_every_sec(f, secs)
254        };
255
256        resp.on_hover_text(if diff < 0.0 {
257            "Refreshing now!".to_string()
258        } else {
259            format!("Refreshing automatically in {diff:.0}s...")
260        });
261    }
262}