use std::sync::Arc;
use crate::bind::{Bind, MaybeSend};
struct ButtonLayout {
normal_galley: Arc<egui::Galley>,
pending_galley: Arc<egui::Galley>,
desired_size: egui::Vec2,
spinner_size: f32,
item_spacing: egui::Vec2,
}
#[must_use = "You should call .show() on this widget to render it"]
pub struct AsyncButton<'a, T, E> {
bind: &'a mut Bind<T, E>,
text: egui::WidgetText,
pending_text: Option<egui::WidgetText>,
frame: bool,
clear_on_click: bool,
}
impl<'a, T, E> AsyncButton<'a, T, E> {
pub fn new(bind: &'a mut Bind<T, E>, text: impl Into<egui::WidgetText>) -> Self {
Self {
bind,
text: text.into(),
pending_text: None,
frame: true,
clear_on_click: true,
}
}
pub fn pending_text(mut self, text: impl Into<egui::WidgetText>) -> Self {
self.pending_text = Some(text.into());
self
}
pub const fn frame(mut self, frame: bool) -> Self {
self.frame = frame;
self
}
pub const fn clear_on_click(mut self, clear: bool) -> Self {
self.clear_on_click = clear;
self
}
pub fn show<Fut>(self, ui: &mut egui::Ui, f: impl FnOnce() -> Fut) -> egui::Response
where
Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
T: MaybeSend + 'static,
E: MaybeSend + 'static,
{
let is_pending = self.bind.is_pending();
let layout = self.calculate_layout(ui);
let sense = if is_pending {
egui::Sense::hover()
} else {
egui::Sense::click()
};
let (rect, resp) = ui.allocate_exact_size(layout.desired_size, sense);
if ui.is_rect_visible(rect) {
self.paint_visuals(ui, &rect, &resp, &layout, is_pending);
}
if resp.clicked() && !is_pending {
if self.clear_on_click {
self.bind.refresh(f());
} else {
self.bind.request(f());
}
}
if is_pending {
resp.on_hover_cursor(egui::CursorIcon::Wait)
} else {
resp.on_hover_cursor(egui::CursorIcon::PointingHand)
}
}
fn calculate_layout(&self, ui: &egui::Ui) -> ButtonLayout {
let pending_display = self
.pending_text
.clone()
.unwrap_or_else(|| self.text.clone());
let normal_galley =
self.text
.clone()
.into_galley(ui, None, f32::INFINITY, egui::FontSelection::Default);
let pending_galley =
pending_display.into_galley(ui, None, f32::INFINITY, egui::FontSelection::Default);
let button_padding = if self.frame {
ui.spacing().button_padding
} else {
egui::vec2(2.0, 2.0)
};
let item_spacing = ui.spacing().item_spacing;
let spinner_size = ui.text_style_height(&egui::TextStyle::Button);
let normal_size = normal_galley.size() + 2.0 * button_padding;
let mut pending_size = pending_galley.size() + 2.0 * button_padding;
if pending_galley.size().x > 0.0 {
pending_size.x += item_spacing.x;
}
pending_size.x += spinner_size;
pending_size.y = pending_size
.y
.max(2.0f32.mul_add(button_padding.y, spinner_size));
let desired_size = egui::vec2(
normal_size.x.max(pending_size.x),
normal_size.y.max(pending_size.y),
);
ButtonLayout {
normal_galley,
pending_galley,
desired_size,
spinner_size,
item_spacing,
}
}
fn paint_visuals(
&self,
ui: &mut egui::Ui,
rect: &egui::Rect,
resp: &egui::Response,
layout: &ButtonLayout,
is_pending: bool,
) {
let visuals = if is_pending {
ui.style().visuals.widgets.noninteractive
} else {
*ui.style().interact(resp)
};
if self.frame || (resp.hovered() && !is_pending) {
let (fill, stroke) = if is_pending && self.frame {
(
ui.style().visuals.widgets.inactive.bg_fill,
ui.style().visuals.widgets.inactive.bg_stroke,
)
} else if self.frame || resp.hovered() {
(visuals.bg_fill, visuals.bg_stroke)
} else {
(egui::Color32::TRANSPARENT, egui::Stroke::NONE)
};
let expansion = if resp.hovered() && !is_pending && self.frame {
visuals.expansion
} else if !self.frame && resp.hovered() && !is_pending {
ui.spacing().item_spacing.x * 0.5
} else {
0.0
};
ui.painter().rect(
rect.expand(expansion),
visuals.corner_radius,
fill,
stroke,
egui::StrokeKind::Middle,
);
}
let current_galley = if is_pending {
&layout.pending_galley
} else {
&layout.normal_galley
};
let content_width = if is_pending {
if current_galley.size().x > 0.0 {
current_galley.size().x + layout.item_spacing.x + layout.spinner_size
} else {
layout.spinner_size
}
} else {
current_galley.size().x
};
let mut cursor_x = rect.center().x - content_width / 2.0;
if is_pending {
let spinner_rect = egui::Rect::from_min_size(
egui::pos2(cursor_x, rect.center().y - layout.spinner_size / 2.0),
egui::vec2(layout.spinner_size, layout.spinner_size),
);
ui.put(
spinner_rect,
egui::Spinner::new()
.size(layout.spinner_size)
.color(visuals.text_color()),
);
cursor_x += layout.spinner_size;
if current_galley.size().x > 0.0 {
cursor_x += layout.item_spacing.x;
}
}
if current_galley.size().x > 0.0 {
let text_color = visuals.text_color();
let text_pos = egui::pos2(cursor_x, rect.center().y - current_galley.size().y / 2.0);
ui.painter()
.galley(text_pos, current_galley.clone(), text_color);
}
}
}