1use std::fmt::Debug;
11
12use super::bind::{self, Bind, MaybeSend, State};
13
14#[derive(Default)]
24pub struct EguiAsyncPlugin;
25
26impl egui::Plugin for EguiAsyncPlugin {
27 fn debug_name(&self) -> &'static str {
28 "egui_async"
29 }
30
31 fn on_begin_pass(&mut self, ctx: &egui::Context) {
32 bind::CTX.get_or_init(|| ctx.clone());
36 let time = ctx.input(|i| i.time);
37
38 let last_frame = bind::CURR_FRAME.swap(time, std::sync::atomic::Ordering::Relaxed);
39 bind::LAST_FRAME.store(last_frame, std::sync::atomic::Ordering::Relaxed);
40 }
41}
42
43impl<T: 'static, E: Debug + 'static> Bind<T, E> {
44 pub fn read_or_error<Fut>(&mut self, f: impl FnOnce() -> Fut, ui: &mut egui::Ui) -> Option<&T>
49 where
50 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
51 T: MaybeSend,
52 E: MaybeSend,
53 {
54 self.poll();
55
56 if let Some(Err(e)) = &self.data {
57 let error_string = format!("{e:?}");
58 if ui.popup_error(&error_string) {
59 self.request(f());
60 }
61 None
62 } else if let Some(Ok(data)) = self.data.as_ref() {
63 Some(data)
64 } else {
65 None
66 }
67 }
68
69 pub fn read_mut_or_error<Fut>(
75 &mut self,
76 f: impl FnOnce() -> Fut,
77 ui: &mut egui::Ui,
78 ) -> Option<&mut T>
79 where
80 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
81 T: MaybeSend,
82 E: MaybeSend,
83 {
84 self.poll();
85
86 if let Some(Err(e)) = &self.data {
87 let error_string = format!("{e:?}");
88 if ui.popup_error(&error_string) {
89 self.request(f());
90 }
91 None
92 } else if let Some(Ok(data)) = self.data.as_mut() {
93 Some(data)
94 } else {
95 None
96 }
97 }
98
99 pub fn read_or_request_or_error<Fut>(
105 &mut self,
106 f: impl FnOnce() -> Fut,
107 ui: &mut egui::Ui,
108 ) -> Option<&T>
109 where
110 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
111 T: MaybeSend,
112 E: MaybeSend,
113 {
114 self.poll();
115
116 if matches!(self.state, State::Idle) {
117 self.request(f());
118 None
119 } else if let Some(Err(e)) = &self.data {
120 let error_string = format!("{e:?}");
121 if ui.popup_error(&error_string) {
122 self.request(f());
123 }
124 None
125 } else if let Some(Ok(data)) = self.data.as_ref() {
126 Some(data)
127 } else {
128 None
129 }
130 }
131
132 pub fn read_mut_or_request_or_error<Fut>(
138 &mut self,
139 f: impl FnOnce() -> Fut,
140 ui: &mut egui::Ui,
141 ) -> Option<&mut T>
142 where
143 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
144 T: MaybeSend,
145 E: MaybeSend,
146 {
147 self.poll();
148
149 if matches!(self.state, State::Idle) {
150 self.request(f());
151 None
152 } else if let Some(Err(e)) = &self.data {
153 let error_string = format!("{e:?}");
154 if ui.popup_error(&error_string) {
155 self.request(f());
156 }
157 None
158 } else if let Some(Ok(data)) = self.data.as_mut() {
159 Some(data)
160 } else {
161 None
162 }
163 }
164}
165
166pub trait UiExt {
168 fn popup_error(&self, error: &str) -> bool;
173 fn popup_notify(&self, info: &str) -> bool;
178
179 fn refresh_button<T, E, Fut>(
187 &mut self,
188 bind: &mut bind::Bind<T, E>,
189 f: impl FnOnce() -> Fut,
190 secs: f64,
191 ) where
192 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
193 T: MaybeSend + 'static,
194 E: MaybeSend + 'static;
195}
196
197const REFRESH_DEBOUNCE_FACTOR: f64 = 4.0;
198
199impl UiExt for egui::Ui {
200 fn popup_error(&self, error: &str) -> bool {
201 let screen_rect = self.ctx().content_rect();
202 let total_width = screen_rect.width();
203 let total_height = screen_rect.height();
204
205 let id = egui::Id::new("error_window");
206 egui::Window::new("Error")
207 .id(id)
208 .collapsible(false)
209 .default_width(total_width * 0.25)
210 .default_height(total_height * 0.20)
211 .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
212 .show(self.ctx(), |ui| {
213 ui.vertical_centered(|ui| {
214 ui.label(egui::RichText::new(error).color(egui::Color32::RED));
215
216 ui.add_space(10.0);
217
218 ui.label("Please retry the request, or contact support if the error persists.");
219
220 ui.add_space(10.0);
221
222 ui.button("Retry").clicked()
223 })
224 .inner
225 })
226 .is_some_and(|r| r.inner.is_some_and(|r| r))
227 }
228 fn popup_notify(&self, info: &str) -> bool {
229 let screen_rect = self.ctx().content_rect();
230 let total_width = screen_rect.width();
231 let total_height = screen_rect.height();
232
233 let id = egui::Id::new("notify_window");
234 egui::Window::new("Info")
235 .id(id)
236 .collapsible(false)
237 .default_width(total_width * 0.25)
238 .default_height(total_height * 0.20)
239 .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
240 .show(self.ctx(), |ui| {
241 ui.vertical_centered(|ui| {
242 ui.label(info);
243
244 ui.add_space(10.0);
245
246 ui.button("Ok").clicked()
247 })
248 .inner
249 })
250 .is_some_and(|r| r.inner.is_some_and(|r| r))
251 }
252
253 fn refresh_button<T, E, Fut>(
254 &mut self,
255 bind: &mut bind::Bind<T, E>,
256 f: impl FnOnce() -> Fut,
257 secs: f64,
258 ) where
259 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
260 T: MaybeSend + 'static,
261 E: MaybeSend + 'static,
262 {
263 let resp = self.button("🔄");
264
265 let diff = if bind.since_completed() > secs / REFRESH_DEBOUNCE_FACTOR && resp.clicked() {
267 bind.refresh(f());
268 -1.0
269 } else {
270 bind.request_every_sec(f, secs)
271 };
272
273 resp.on_hover_text(if diff < 0.0 {
274 "Refreshing now!".to_string()
275 } else {
276 format!("Refreshing automatically in {diff:.0}s...")
277 });
278 }
279}