use egui::{
emath::{Align, Align2},
epaint::{Color32, Pos2, Rounding},
Area, Button, Context, Id, Layout, Response, RichText, Sense, Ui, WidgetText, Window,
};
const ERROR_ICON_COLOR: Color32 = Color32::from_rgb(200, 90, 90);
const INFO_ICON_COLOR: Color32 = Color32::from_rgb(150, 200, 210);
const WARNING_ICON_COLOR: Color32 = Color32::from_rgb(230, 220, 140);
const SUCCESS_ICON_COLOR: Color32 = Color32::from_rgb(140, 230, 140);
const CAUTION_BUTTON_FILL: Color32 = Color32::from_rgb(87, 38, 34);
const SUGGESTED_BUTTON_FILL: Color32 = Color32::from_rgb(33, 54, 84);
const CAUTION_BUTTON_TEXT_COLOR: Color32 = Color32::from_rgb(242, 148, 148);
const SUGGESTED_BUTTON_TEXT_COLOR: Color32 = Color32::from_rgb(141, 182, 242);
const OVERLAY_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 200);
pub enum ModalButtonStyle {
None,
Suggested,
Caution,
}
#[derive(Clone, Default, PartialEq)]
pub enum Icon {
#[default]
Info,
Warning,
Success,
Error,
Custom((String, Color32)),
}
impl std::fmt::Display for Icon {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Icon::Info => write!(f, "ℹ"),
Icon::Warning => write!(f, "⚠"),
Icon::Success => write!(f, "✔"),
Icon::Error => write!(f, "❗"),
Icon::Custom((icon_text, _)) => write!(f, "{icon_text}"),
}
}
}
#[derive(Clone, Default)]
struct DialogData {
title: Option<String>,
body: Option<String>,
icon: Option<Icon>,
}
#[must_use = "use `DialogBuilder::open`"]
pub struct DialogBuilder {
data: DialogData,
modal_id: Id,
ctx: Context,
}
#[derive(Clone)]
enum ModalType {
Modal,
Dialog(DialogData),
}
#[derive(Clone)]
struct ModalState {
is_open: bool,
was_outside_clicked: bool,
modal_type: ModalType,
last_frame_height: Option<f32>,
}
#[derive(Clone, Debug)]
pub struct ModalStyle {
pub body_margin: f32,
pub frame_margin: f32,
pub icon_margin: f32,
pub icon_size: f32,
pub overlay_color: Color32,
pub caution_button_fill: Color32,
pub suggested_button_fill: Color32,
pub caution_button_text_color: Color32,
pub suggested_button_text_color: Color32,
pub dialog_ok_text: String,
pub info_icon_color: Color32,
pub warning_icon_color: Color32,
pub success_icon_color: Color32,
pub error_icon_color: Color32,
pub default_width: Option<f32>,
pub default_height: Option<f32>,
pub body_alignment: Align,
}
impl ModalState {
fn load(ctx: &Context, id: Id) -> Self {
ctx.data_mut(|d| d.get_temp(id).unwrap_or_default())
}
fn save(self, ctx: &Context, id: Id) {
ctx.data_mut(|d| d.insert_temp(id, self))
}
}
impl Default for ModalState {
fn default() -> Self {
Self {
was_outside_clicked: false,
is_open: false,
modal_type: ModalType::Modal,
last_frame_height: None,
}
}
}
impl Default for ModalStyle {
fn default() -> Self {
Self {
body_margin: 5.,
icon_margin: 7.,
frame_margin: 2.,
icon_size: 30.,
overlay_color: OVERLAY_COLOR,
caution_button_fill: CAUTION_BUTTON_FILL,
suggested_button_fill: SUGGESTED_BUTTON_FILL,
caution_button_text_color: CAUTION_BUTTON_TEXT_COLOR,
suggested_button_text_color: SUGGESTED_BUTTON_TEXT_COLOR,
dialog_ok_text: "ok".to_string(),
info_icon_color: INFO_ICON_COLOR,
warning_icon_color: WARNING_ICON_COLOR,
success_icon_color: SUCCESS_ICON_COLOR,
error_icon_color: ERROR_ICON_COLOR,
default_height: None,
default_width: None,
body_alignment: Align::Min,
}
}
}
pub struct Modal {
close_on_outside_click: bool,
style: ModalStyle,
ctx: Context,
id: Id,
window_id: Id,
}
fn ui_with_margin<R>(ui: &mut Ui, margin: f32, add_contents: impl FnOnce(&mut Ui) -> R) {
egui::Frame::none()
.inner_margin(margin)
.show(ui, |ui| add_contents(ui));
}
impl Modal {
pub fn new(ctx: &Context, id_source: impl std::fmt::Display) -> Self {
let self_id = Id::new(id_source.to_string());
Self {
window_id: self_id.with("window"),
id: self_id,
style: ModalStyle::default(),
ctx: ctx.clone(),
close_on_outside_click: false,
}
}
fn set_open_state(&self, is_open: bool) {
let mut modal_state = ModalState::load(&self.ctx, self.id);
modal_state.is_open = is_open;
modal_state.save(&self.ctx, self.id)
}
fn set_outside_clicked(&self, was_clicked: bool) {
let mut modal_state = ModalState::load(&self.ctx, self.id);
modal_state.was_outside_clicked = was_clicked;
modal_state.save(&self.ctx, self.id)
}
pub fn was_outside_clicked(&self) -> bool {
let modal_state = ModalState::load(&self.ctx, self.id);
modal_state.was_outside_clicked
}
pub fn is_open(&self) -> bool {
let modal_state = ModalState::load(&self.ctx, self.id);
modal_state.is_open
}
pub fn open(&self) {
self.set_open_state(true)
}
pub fn close(&self) {
self.set_open_state(false)
}
pub fn with_close_on_outside_click(mut self, do_close_on_click_ouside: bool) -> Self {
self.close_on_outside_click = do_close_on_click_ouside;
self
}
pub fn with_style(mut self, style: &ModalStyle) -> Self {
self.style = style.clone();
self
}
pub fn title(&self, ui: &mut Ui, text: impl Into<RichText>) {
let text: RichText = text.into();
ui.vertical_centered(|ui| {
ui.heading(text);
});
ui.separator();
}
pub fn icon(&self, ui: &mut Ui, icon: Icon) {
let color = match icon {
Icon::Info => self.style.info_icon_color,
Icon::Warning => self.style.warning_icon_color,
Icon::Success => self.style.success_icon_color,
Icon::Error => self.style.error_icon_color,
Icon::Custom((_, color)) => color,
};
let text = RichText::new(icon.to_string())
.color(color)
.size(self.style.icon_size);
ui_with_margin(ui, self.style.icon_margin, |ui| {
ui.add(egui::Label::new(text));
});
}
pub fn frame<R>(&self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) {
let last_frame_height = ModalState::load(&self.ctx, self.id)
.last_frame_height
.unwrap_or_default();
let default_height = self.style.default_height.unwrap_or_default();
let space_height = ((default_height - last_frame_height) * 0.5).max(0.);
ui.with_layout(
Layout::top_down(Align::Center).with_cross_align(Align::Center),
|ui| {
ui_with_margin(ui, self.style.frame_margin, |ui| {
if space_height > 0. {
ui.add_space(space_height);
add_contents(ui);
ui.add_space(space_height);
} else {
add_contents(ui);
}
})
},
);
}
pub fn body_and_icon(&self, ui: &mut Ui, text: impl Into<WidgetText>, icon: Icon) {
egui::Grid::new(self.id).num_columns(2).show(ui, |ui| {
self.icon(ui, icon);
self.body(ui, text);
});
}
pub fn body(&self, ui: &mut Ui, text: impl Into<WidgetText>) {
let text: WidgetText = text.into();
ui.with_layout(Layout::top_down(self.style.body_alignment), |ui| {
ui_with_margin(ui, self.style.body_margin, |ui| {
ui.label(text);
})
});
}
pub fn buttons<R>(&self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) {
ui.separator();
ui.with_layout(Layout::right_to_left(Align::Min), add_contents);
}
pub fn button(&self, ui: &mut Ui, text: impl Into<WidgetText>) -> Response {
self.styled_button(ui, text, ModalButtonStyle::None)
}
pub fn caution_button(&self, ui: &mut Ui, text: impl Into<WidgetText>) -> Response {
self.styled_button(ui, text, ModalButtonStyle::Caution)
}
pub fn suggested_button(&self, ui: &mut Ui, text: impl Into<WidgetText>) -> Response {
self.styled_button(ui, text, ModalButtonStyle::Suggested)
}
fn styled_button(
&self,
ui: &mut Ui,
text: impl Into<WidgetText>,
button_style: ModalButtonStyle,
) -> Response {
let button = match button_style {
ModalButtonStyle::Suggested => {
let text: WidgetText = text.into().color(self.style.suggested_button_text_color);
Button::new(text).fill(self.style.suggested_button_fill)
}
ModalButtonStyle::Caution => {
let text: WidgetText = text.into().color(self.style.caution_button_text_color);
Button::new(text).fill(self.style.caution_button_fill)
}
ModalButtonStyle::None => Button::new(text.into()),
};
let response = ui.add(button);
if response.clicked() {
self.close()
}
response
}
pub fn show<R>(&self, add_contents: impl FnOnce(&mut Ui) -> R) {
let mut modal_state = ModalState::load(&self.ctx, self.id);
self.set_outside_clicked(false);
if modal_state.is_open {
let ctx_clone = self.ctx.clone();
let area_resp = Area::new(self.id)
.interactable(true)
.fixed_pos(Pos2::ZERO)
.show(&self.ctx, |ui: &mut Ui| {
let screen_rect = ui.ctx().input(|i| i.screen_rect);
let area_response = ui.allocate_response(screen_rect.size(), Sense::click());
if area_response.clicked() {
self.set_outside_clicked(true);
if self.close_on_outside_click {
self.close();
}
}
ui.painter()
.rect_filled(screen_rect, Rounding::ZERO, self.style.overlay_color);
});
ctx_clone.move_to_top(area_resp.response.layer_id);
let mut window_id = self
.style
.default_width
.map_or(self.window_id, |w| self.window_id.with(w.to_string()));
window_id = self
.style
.default_height
.map_or(window_id, |h| window_id.with(h.to_string()));
let mut window = Window::new("")
.id(window_id)
.open(&mut modal_state.is_open)
.title_bar(false)
.anchor(Align2::CENTER_CENTER, [0., 0.])
.resizable(false);
let recalculating_height =
self.style.default_height.is_some() && modal_state.last_frame_height.is_none();
if let Some(default_height) = self.style.default_height {
window = window.default_height(default_height);
}
if let Some(default_width) = self.style.default_width {
window = window.default_width(default_width);
}
let response = window.show(&ctx_clone, add_contents);
if let Some(inner_response) = response {
ctx_clone.move_to_top(inner_response.response.layer_id);
if recalculating_height {
let mut modal_state = ModalState::load(&self.ctx, self.id);
modal_state.last_frame_height = Some(inner_response.response.rect.height());
modal_state.save(&self.ctx, self.id);
}
}
}
}
#[deprecated(since = "0.3.0", note = "use `Modal::dialog`")]
pub fn open_dialog(
&self,
title: Option<impl std::fmt::Display>,
body: Option<impl std::fmt::Display>,
icon: Option<Icon>,
) {
let modal_data = DialogData {
title: title.map(|s| s.to_string()),
body: body.map(|s| s.to_string()),
icon,
};
let mut modal_state = ModalState::load(&self.ctx, self.id);
modal_state.modal_type = ModalType::Dialog(modal_data);
modal_state.is_open = true;
modal_state.save(&self.ctx, self.id);
}
pub fn dialog(&self) -> DialogBuilder {
DialogBuilder {
data: DialogData::default(),
modal_id: self.id.clone(),
ctx: self.ctx.clone(),
}
}
pub fn show_dialog(&mut self) {
let modal_state = ModalState::load(&self.ctx, self.id);
if let ModalType::Dialog(modal_data) = modal_state.modal_type {
self.close_on_outside_click = true;
self.show(|ui| {
if let Some(title) = modal_data.title {
self.title(ui, title)
}
self.frame(ui, |ui| {
if modal_data.body.is_none() {
if let Some(icon) = modal_data.icon {
self.icon(ui, icon)
}
} else if modal_data.icon.is_none() {
if let Some(body) = modal_data.body {
self.body(ui, body)
}
} else if modal_data.icon.is_some() && modal_data.icon.is_some() {
self.body_and_icon(ui, modal_data.body.unwrap(), modal_data.icon.unwrap())
}
});
self.buttons(ui, |ui| {
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
self.button(ui, &self.style.dialog_ok_text)
})
})
});
}
}
}
impl DialogBuilder {
pub fn with_title(mut self, title: impl std::fmt::Display) -> Self {
self.data.title = Some(title.to_string());
self
}
pub fn with_body(mut self, body: impl std::fmt::Display) -> Self {
self.data.body = Some(body.to_string());
self
}
pub fn with_icon(mut self, icon: Icon) -> Self {
self.data.icon = Some(icon);
self
}
pub fn open(self) {
let mut modal_state = ModalState::load(&self.ctx, self.modal_id);
modal_state.modal_type = ModalType::Dialog(self.data);
modal_state.is_open = true;
modal_state.save(&self.ctx, self.modal_id);
}
}