egui_async/egui/widgets/
async_button.rs1use std::sync::Arc;
4
5use crate::bind::{Bind, MaybeSend};
6
7struct ButtonLayout {
9 normal_galley: Arc<egui::Galley>,
10 pending_galley: Arc<egui::Galley>,
11 desired_size: egui::Vec2,
12 spinner_size: f32,
13 item_spacing: egui::Vec2,
14}
15
16#[must_use = "You should call .show() on this widget to render it"]
21pub struct AsyncButton<'a, T, E> {
22 bind: &'a mut Bind<T, E>,
23 text: egui::WidgetText,
24 pending_text: Option<egui::WidgetText>,
25 frame: bool,
26 clear_on_click: bool,
27}
28
29impl<'a, T, E> AsyncButton<'a, T, E> {
30 pub fn new(bind: &'a mut Bind<T, E>, text: impl Into<egui::WidgetText>) -> Self {
32 Self {
33 bind,
34 text: text.into(),
35 pending_text: None,
36 frame: true,
37 clear_on_click: true,
38 }
39 }
40
41 pub fn pending_text(mut self, text: impl Into<egui::WidgetText>) -> Self {
43 self.pending_text = Some(text.into());
44 self
45 }
46
47 pub const fn frame(mut self, frame: bool) -> Self {
50 self.frame = frame;
51 self
52 }
53
54 pub const fn clear_on_click(mut self, clear: bool) -> Self {
58 self.clear_on_click = clear;
59 self
60 }
61
62 pub fn show<Fut>(self, ui: &mut egui::Ui, f: impl FnOnce() -> Fut) -> egui::Response
64 where
65 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
66 T: MaybeSend + 'static,
67 E: MaybeSend + 'static,
68 {
69 let is_pending = self.bind.is_pending();
70 let layout = self.calculate_layout(ui);
71
72 let sense = if is_pending {
73 egui::Sense::hover()
74 } else {
75 egui::Sense::click()
76 };
77
78 let (rect, resp) = ui.allocate_exact_size(layout.desired_size, sense);
79
80 if ui.is_rect_visible(rect) {
81 self.paint_visuals(ui, &rect, &resp, &layout, is_pending);
82 }
83
84 if resp.clicked() && !is_pending {
85 if self.clear_on_click {
86 self.bind.refresh(f());
87 } else {
88 self.bind.request(f());
89 }
90 }
91
92 if is_pending {
93 resp.on_hover_cursor(egui::CursorIcon::Wait)
94 } else {
95 resp.on_hover_cursor(egui::CursorIcon::PointingHand)
96 }
97 }
98
99 fn calculate_layout(&self, ui: &egui::Ui) -> ButtonLayout {
101 let pending_display = self
102 .pending_text
103 .clone()
104 .unwrap_or_else(|| self.text.clone());
105
106 let normal_galley =
107 self.text
108 .clone()
109 .into_galley(ui, None, f32::INFINITY, egui::FontSelection::Default);
110 let pending_galley =
111 pending_display.into_galley(ui, None, f32::INFINITY, egui::FontSelection::Default);
112
113 let button_padding = if self.frame {
114 ui.spacing().button_padding
115 } else {
116 egui::vec2(2.0, 2.0)
117 };
118
119 let item_spacing = ui.spacing().item_spacing;
120 let spinner_size = ui.text_style_height(&egui::TextStyle::Button);
121
122 let normal_size = normal_galley.size() + 2.0 * button_padding;
123 let mut pending_size = pending_galley.size() + 2.0 * button_padding;
124
125 if pending_galley.size().x > 0.0 {
126 pending_size.x += item_spacing.x;
127 }
128
129 pending_size.x += spinner_size;
130 pending_size.y = pending_size
131 .y
132 .max(2.0f32.mul_add(button_padding.y, spinner_size));
133
134 let desired_size = egui::vec2(
135 normal_size.x.max(pending_size.x),
136 normal_size.y.max(pending_size.y),
137 );
138
139 ButtonLayout {
140 normal_galley,
141 pending_galley,
142 desired_size,
143 spinner_size,
144 item_spacing,
145 }
146 }
147
148 fn paint_visuals(
150 &self,
151 ui: &mut egui::Ui,
152 rect: &egui::Rect,
153 resp: &egui::Response,
154 layout: &ButtonLayout,
155 is_pending: bool,
156 ) {
157 let visuals = if is_pending {
158 ui.style().visuals.widgets.noninteractive
159 } else {
160 *ui.style().interact(resp)
161 };
162
163 if self.frame || (resp.hovered() && !is_pending) {
165 let (fill, stroke) = if is_pending && self.frame {
166 (
167 ui.style().visuals.widgets.inactive.bg_fill,
168 ui.style().visuals.widgets.inactive.bg_stroke,
169 )
170 } else if self.frame || resp.hovered() {
171 (visuals.bg_fill, visuals.bg_stroke)
172 } else {
173 (egui::Color32::TRANSPARENT, egui::Stroke::NONE)
174 };
175
176 let expansion = if resp.hovered() && !is_pending && self.frame {
177 visuals.expansion
178 } else if !self.frame && resp.hovered() && !is_pending {
179 ui.spacing().item_spacing.x * 0.5
180 } else {
181 0.0
182 };
183
184 ui.painter().rect(
185 rect.expand(expansion),
186 visuals.corner_radius,
187 fill,
188 stroke,
189 egui::StrokeKind::Middle,
190 );
191 }
192
193 let current_galley = if is_pending {
195 &layout.pending_galley
196 } else {
197 &layout.normal_galley
198 };
199
200 let content_width = if is_pending {
201 if current_galley.size().x > 0.0 {
202 current_galley.size().x + layout.item_spacing.x + layout.spinner_size
203 } else {
204 layout.spinner_size
205 }
206 } else {
207 current_galley.size().x
208 };
209
210 let mut cursor_x = rect.center().x - content_width / 2.0;
211
212 if is_pending {
214 let spinner_rect = egui::Rect::from_min_size(
215 egui::pos2(cursor_x, rect.center().y - layout.spinner_size / 2.0),
216 egui::vec2(layout.spinner_size, layout.spinner_size),
217 );
218
219 ui.put(
220 spinner_rect,
221 egui::Spinner::new()
222 .size(layout.spinner_size)
223 .color(visuals.text_color()),
224 );
225
226 cursor_x += layout.spinner_size;
227 if current_galley.size().x > 0.0 {
228 cursor_x += layout.item_spacing.x;
229 }
230 }
231
232 if current_galley.size().x > 0.0 {
234 let text_color = visuals.text_color();
235 let text_pos = egui::pos2(cursor_x, rect.center().y - current_galley.size().y / 2.0);
236
237 ui.painter()
238 .galley(text_pos, current_galley.clone(), text_color);
239 }
240 }
241}