1use std::fmt::Debug;
8
9use super::bind::{self, Bind, MaybeSend, State};
10
11pub trait ContextExt {
13 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 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 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 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 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
151pub trait UiExt {
156 fn popup_error(&self, error: &str) -> bool;
159 fn popup_notify(&self, info: &str) -> bool;
162
163 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 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}