use egui::{
accesskit, Align2, Area, Color32, Context, CornerRadius, Frame, Id, Key, Margin, Order,
Response, Sense, Stroke, Ui, Vec2, WidgetInfo, WidgetText, WidgetType,
};
use crate::{theme::Theme, Button, ButtonSize};
#[must_use = "Call `.show(ctx, |ui| { ... })` to render the modal."]
pub struct Modal<'a> {
id_salt: Id,
heading: Option<WidgetText>,
open: &'a mut bool,
max_width: f32,
close_on_backdrop: bool,
close_on_escape: bool,
alert: bool,
}
impl<'a> std::fmt::Debug for Modal<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Modal")
.field("id_salt", &self.id_salt)
.field("heading", &self.heading.as_ref().map(|h| h.text()))
.field("open", &*self.open)
.field("max_width", &self.max_width)
.field("close_on_backdrop", &self.close_on_backdrop)
.field("close_on_escape", &self.close_on_escape)
.field("alert", &self.alert)
.finish()
}
}
impl<'a> Modal<'a> {
pub fn new(id_salt: impl std::hash::Hash, open: &'a mut bool) -> Self {
Self {
id_salt: Id::new(id_salt),
heading: None,
open,
max_width: 440.0,
close_on_backdrop: true,
close_on_escape: true,
alert: false,
}
}
pub fn heading(mut self, heading: impl Into<WidgetText>) -> Self {
self.heading = Some(heading.into());
self
}
pub fn max_width(mut self, max_width: f32) -> Self {
self.max_width = max_width;
self
}
pub fn close_on_backdrop(mut self, close: bool) -> Self {
self.close_on_backdrop = close;
self
}
pub fn close_on_escape(mut self, close: bool) -> Self {
self.close_on_escape = close;
self
}
pub fn alert(mut self, alert: bool) -> Self {
self.alert = alert;
self
}
pub fn show<R>(self, ctx: &Context, add_contents: impl FnOnce(&mut Ui) -> R) -> Option<R> {
let focus_storage = Id::new(("elegance_modal_focus", self.id_salt));
let mut focus_state: ModalFocusState =
ctx.data(|d| d.get_temp(focus_storage).unwrap_or_default());
let is_open = *self.open;
if focus_state.was_open && !is_open {
if let Some(prev) = focus_state.prev_focus {
ctx.memory_mut(|m| m.request_focus(prev));
}
ctx.data_mut(|d| d.insert_temp(focus_storage, ModalFocusState::default()));
return None;
}
if !is_open {
return None;
}
let just_opened = !focus_state.was_open;
if just_opened {
focus_state.prev_focus = ctx.memory(|m| m.focused());
focus_state.was_open = true;
ctx.data_mut(|d| d.insert_temp(focus_storage, focus_state));
}
let theme = Theme::current(ctx);
let p = &theme.palette;
let mut should_close = false;
let mut close_btn_id: Option<Id> = None;
let screen = ctx.content_rect();
let backdrop_id = Id::new("elegance_modal_backdrop").with(self.id_salt);
let backdrop = Area::new(backdrop_id)
.fixed_pos(screen.min)
.order(Order::Middle)
.show(ctx, |ui| {
ui.painter().rect_filled(
screen,
CornerRadius::ZERO,
Color32::from_rgba_premultiplied(0, 0, 0, 150),
);
ui.allocate_rect(screen, Sense::click())
});
if self.close_on_backdrop && backdrop.inner.clicked() {
should_close = true;
}
let window_id = Id::new("elegance_modal_window").with(self.id_salt);
let alert = self.alert;
let heading_text: Option<String> = self.heading.as_ref().map(|h| h.text().to_string());
let result = Area::new(window_id)
.order(Order::Foreground)
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
.show(ctx, |ui| {
let role = if alert {
accesskit::Role::AlertDialog
} else {
accesskit::Role::Dialog
};
let heading_for_label = heading_text.clone();
ui.ctx().accesskit_node_builder(ui.unique_id(), |node| {
node.set_role(role);
if let Some(label) = heading_for_label {
node.set_label(label);
}
});
ui.set_max_width(self.max_width);
Frame::new()
.fill(p.card)
.stroke(Stroke::new(1.0, p.border))
.corner_radius(CornerRadius::same(theme.card_radius as u8))
.inner_margin(Margin::same(theme.card_padding as i8))
.show(ui, |ui| {
let has_heading = self.heading.is_some();
if has_heading {
ui.horizontal(|ui| {
if let Some(h) = &self.heading {
ui.add(egui::Label::new(theme.heading_text(h.text())));
}
ui.with_layout(
egui::Layout::right_to_left(egui::Align::Center),
|ui| {
let resp = close_button(ui);
if resp.clicked() {
should_close = true;
}
close_btn_id = Some(resp.id);
},
);
});
ui.add_space(6.0);
ui.separator();
ui.add_space(10.0);
}
add_contents(ui)
})
});
if self.close_on_escape && ctx.input(|i| i.key_pressed(Key::Escape)) {
should_close = true;
}
if just_opened {
if let Some(id) = close_btn_id {
ctx.memory_mut(|m| m.request_focus(id));
}
}
if should_close {
*self.open = false;
}
Some(result.inner.inner)
}
}
#[derive(Clone, Copy, Default, Debug)]
struct ModalFocusState {
was_open: bool,
prev_focus: Option<Id>,
}
fn close_button(ui: &mut Ui) -> Response {
let inner = ui
.push_id("elegance_modal_close", |ui| {
ui.add(Button::new("×").outline().size(ButtonSize::Small))
})
.inner;
let enabled = inner.enabled();
inner.widget_info(|| WidgetInfo::labeled(WidgetType::Button, enabled, "Close"));
inner
}