use crate::get_global_color;
use egui::{self, Color32, Context, Id, Modal, Response, Sense, Stroke, Ui, Vec2};
#[derive(Clone, Copy, PartialEq)]
pub enum DialogType {
Standard,
Alert,
Confirm,
Form,
}
pub struct MaterialDialog<'a> {
id: Id,
title: String,
open: &'a mut bool,
dialog_type: DialogType,
icon: Option<String>,
content: Box<dyn FnOnce(&mut Ui) + 'a>,
actions: Vec<DialogAction<'a>>,
quick: bool,
no_focus_trap: bool,
max_width: Option<f32>,
min_width: Option<f32>,
max_height: Option<f32>,
title_padding: Option<[f32; 4]>,
content_padding: Option<[f32; 4]>,
actions_padding: Option<[f32; 4]>,
button_padding: Option<[f32; 2]>,
scrollable: bool,
actions_spacing: f32,
}
pub struct DialogAction<'a> {
text: String,
action_type: ActionType,
_enabled: bool,
action: Box<dyn FnOnce() + 'a>,
}
#[derive(Clone, Copy, PartialEq)]
pub enum ActionType {
Text,
FilledTonal,
Filled,
}
impl<'a> MaterialDialog<'a> {
pub fn new(id: impl Into<Id>, title: impl Into<String>, open: &'a mut bool) -> Self {
Self {
id: id.into(),
title: title.into(),
open,
dialog_type: DialogType::Standard,
icon: None,
content: Box::new(|_| {}),
actions: Vec::new(),
quick: false,
no_focus_trap: false,
max_width: None,
min_width: Some(280.0),
max_height: None,
title_padding: None,
content_padding: None,
actions_padding: None,
button_padding: None,
scrollable: false,
actions_spacing: 8.0,
}
}
pub fn dialog_type(mut self, dialog_type: DialogType) -> Self {
self.dialog_type = dialog_type;
self
}
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn content<F>(mut self, content: F) -> Self
where
F: FnOnce(&mut Ui) + 'a,
{
self.content = Box::new(content);
self
}
pub fn quick(mut self, quick: bool) -> Self {
self.quick = quick;
self
}
pub fn no_focus_trap(mut self, no_focus_trap: bool) -> Self {
self.no_focus_trap = no_focus_trap;
self
}
pub fn max_width(mut self, width: f32) -> Self {
self.max_width = Some(width);
self
}
pub fn min_width(mut self, width: f32) -> Self {
self.min_width = Some(width);
self
}
pub fn max_height(mut self, height: f32) -> Self {
self.max_height = Some(height);
self
}
pub fn title_padding(mut self, padding: [f32; 4]) -> Self {
self.title_padding = Some(padding);
self
}
pub fn content_padding(mut self, padding: [f32; 4]) -> Self {
self.content_padding = Some(padding);
self
}
pub fn actions_padding(mut self, padding: [f32; 4]) -> Self {
self.actions_padding = Some(padding);
self
}
pub fn button_padding(mut self, padding: [f32; 2]) -> Self {
self.button_padding = Some(padding);
self
}
pub fn scrollable(mut self, scrollable: bool) -> Self {
self.scrollable = scrollable;
self
}
pub fn actions_spacing(mut self, spacing: f32) -> Self {
self.actions_spacing = spacing;
self
}
pub fn text_action<F>(mut self, text: impl Into<String>, action: F) -> Self
where
F: FnOnce() + 'a,
{
self.actions.push(DialogAction {
text: text.into(),
action_type: ActionType::Text,
_enabled: true,
action: Box::new(action),
});
self
}
pub fn filled_tonal_action<F>(mut self, text: impl Into<String>, action: F) -> Self
where
F: FnOnce() + 'a,
{
self.actions.push(DialogAction {
text: text.into(),
action_type: ActionType::FilledTonal,
_enabled: true,
action: Box::new(action),
});
self
}
pub fn filled_action<F>(mut self, text: impl Into<String>, action: F) -> Self
where
F: FnOnce() + 'a,
{
self.actions.push(DialogAction {
text: text.into(),
action_type: ActionType::Filled,
_enabled: true,
action: Box::new(action),
});
self
}
pub fn action<F>(self, text: impl Into<String>, action: F) -> Self
where
F: FnOnce() + 'a,
{
self.text_action(text, action)
}
pub fn primary_action<F>(self, text: impl Into<String>, action: F) -> Self
where
F: FnOnce() + 'a,
{
self.filled_action(text, action)
}
pub fn show(mut self, ctx: &Context) {
if !*self.open {
return;
}
let mut should_close = false;
let mut pending_actions = Vec::new();
let default_width: f32 = match self.dialog_type {
DialogType::Alert => 280.0,
DialogType::Confirm => 320.0,
DialogType::Form => 560.0,
DialogType::Standard => 400.0,
};
let dialog_min_width = self.min_width.unwrap_or(280.0);
let dialog_max_width = self.max_width.unwrap_or(default_width.max(560.0));
let dialog_max_height = self.max_height;
let screen_height = ctx.content_rect().height();
let effective_max_height = dialog_max_height.unwrap_or((screen_height * 0.9).min(800.0));
let title = self.title.clone();
let icon = self.icon.clone();
let actions = std::mem::take(&mut self.actions);
let open_ref = self.open as *mut bool;
let title_padding = self.title_padding;
let content_padding = self.content_padding;
let actions_padding = self.actions_padding;
let button_padding = self.button_padding;
let scrollable = self.scrollable;
let actions_spacing = self.actions_spacing;
let modal_frame = egui::Frame::default()
.inner_margin(egui::vec2(0.0, 24.0))
.fill(get_global_color("surfaceContainerHigh"))
.corner_radius(egui::CornerRadius::same(28))
.stroke(Stroke::NONE);
let modal = Modal::new(self.id)
.frame(modal_frame)
.show(ctx, |ui| {
ui.set_min_width(dialog_min_width);
ui.set_max_width(dialog_max_width);
if scrollable {
ui.set_max_height(effective_max_height);
}
let surface_container_high = get_global_color("surfaceContainerHigh");
let on_surface = get_global_color("onSurface");
let on_surface_variant = get_global_color("onSurfaceVariant");
ui.style_mut().visuals.window_fill = surface_container_high;
ui.style_mut().visuals.panel_fill = surface_container_high;
ui.style_mut().visuals.window_stroke = Stroke::NONE;
ui.vertical(|ui| {
if let Some(ref icon) = icon {
ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| {
ui.add_space(0.0);
let icon_widget = crate::icon::MaterialIcon::new(crate::material_symbol::material_symbol_text(icon))
.size(24.0)
.color(on_surface_variant);
ui.add(icon_widget);
ui.add_space(16.0);
});
}
let [title_left, title_right, _title_top, title_bottom] =
title_padding.unwrap_or([24.0, 24.0, 0.0, 0.0]);
ui.horizontal(|ui| {
ui.add_space(title_left);
let layout = if icon.is_some() {
egui::Layout::centered_and_justified(egui::Direction::LeftToRight)
} else {
egui::Layout::left_to_right(egui::Align::TOP)
};
ui.with_layout(layout, |ui| {
ui.label(
egui::RichText::new(&title)
.size(24.0)
.color(on_surface)
.family(egui::FontFamily::Proportional),
);
});
ui.add_space(title_right);
});
ui.add_space(if title_bottom > 0.0 { title_bottom } else { 16.0 });
let [content_left, content_right, content_top, content_bottom] =
content_padding.unwrap_or([24.0, 24.0, 0.0, 24.0]);
if scrollable {
let scroll_width = ui.available_width() - content_left - content_right;
let scroll_height = ui.available_height() - content_bottom;
ui.horizontal(|ui| {
ui.add_space(content_left);
ui.allocate_ui_with_layout(
egui::vec2(scroll_width, scroll_height),
egui::Layout::top_down(egui::Align::LEFT),
|ui| {
egui::ScrollArea::vertical()
.id_salt("dialog_content_scroll")
.auto_shrink([false, false])
.show(ui, |ui| {
ui.set_width(scroll_width - 20.0); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
if content_top > 0.0 {
ui.add_space(content_top);
}
(self.content)(ui);
});
},
);
ui.add_space(content_right);
});
} else {
let content_width = ui.available_width() - content_left - content_right;
ui.horizontal(|ui| {
ui.add_space(content_left);
ui.vertical(|ui| {
ui.set_max_width(content_width);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
if content_top > 0.0 {
ui.add_space(content_top);
}
(self.content)(ui);
});
ui.add_space(content_right);
});
}
if !actions.is_empty() {
let [actions_left, actions_right, actions_top, _actions_bottom] =
actions_padding.unwrap_or([24.0, 24.0, 0.0, 0.0]);
let spacing_before_actions = if actions_top > 0.0 {
actions_top
} else if content_bottom > 0.0 {
content_bottom.min(16.0)
} else {
16.0
};
ui.add_space(spacing_before_actions);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.add_space(actions_right);
for (index, action) in actions.into_iter().enumerate().rev() {
let button_response = Self::draw_action_button_static(ui, &action, button_padding);
if button_response.clicked() {
pending_actions.push((index, action.action));
}
if index > 0 {
ui.add_space(actions_spacing);
}
}
ui.add_space(actions_left);
});
}
});
});
for (_index, action) in pending_actions {
action();
should_close = true;
}
if modal.should_close() || should_close {
unsafe {
*open_ref = false;
}
}
}
fn draw_action_button_static(ui: &mut Ui, action: &DialogAction, button_padding: Option<[f32; 2]>) -> Response {
let primary = get_global_color("primary");
let on_primary = get_global_color("onPrimary");
let secondary_container = get_global_color("secondaryContainer");
let on_secondary_container = get_global_color("onSecondaryContainer");
let _on_surface_variant = get_global_color("onSurfaceVariant");
let [btn_h_padding, btn_v_padding] = button_padding.unwrap_or([12.0, 8.0]);
let text_width = ui.painter().layout_no_wrap(
action.text.clone(),
egui::FontId::default(),
Color32::WHITE,
).rect.width();
let button_width = (text_width + btn_h_padding * 2.0).max(64.0);
let button_height = (20.0 + btn_v_padding * 2.0).max(40.0);
let desired_size = Vec2::new(button_width, button_height);
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
let (bg_color, text_color, _border_color) = match action.action_type {
ActionType::Text => {
if response.hovered() {
(
Color32::from_rgba_premultiplied(primary.r(), primary.g(), primary.b(), 20), primary,
Color32::TRANSPARENT,
)
} else {
(Color32::TRANSPARENT, primary, Color32::TRANSPARENT)
}
}
ActionType::FilledTonal => {
(
secondary_container,
on_secondary_container,
Color32::TRANSPARENT,
)
}
ActionType::Filled => {
(primary, on_primary, Color32::TRANSPARENT)
}
};
ui.painter().rect_filled(
rect, 20.0, bg_color,
);
if response.is_pointer_button_down_on() {
let pressed_overlay = Color32::from_rgba_premultiplied(
text_color.r(),
text_color.g(),
text_color.b(),
31,
); ui.painter().rect_filled(rect, 20.0, pressed_overlay);
}
ui.painter().text(
rect.center(),
egui::Align2::CENTER_CENTER,
&action.text,
egui::FontId::proportional(14.0),
text_color,
);
response
}
fn _draw_action_button(&self, ui: &mut Ui, action: &DialogAction) -> Response {
Self::draw_action_button_static(ui, action, self.button_padding)
}
}
pub fn dialog(
id: impl Into<egui::Id>,
title: impl Into<String>,
open: &mut bool,
) -> MaterialDialog<'_> {
MaterialDialog::new(id, title, open)
}
pub fn alert_dialog(
id: impl Into<egui::Id>,
title: impl Into<String>,
open: &mut bool,
) -> MaterialDialog<'_> {
MaterialDialog::new(id, title, open).dialog_type(DialogType::Alert)
}
pub fn confirm_dialog(
id: impl Into<egui::Id>,
title: impl Into<String>,
open: &mut bool,
) -> MaterialDialog<'_> {
MaterialDialog::new(id, title, open).dialog_type(DialogType::Confirm)
}
pub fn form_dialog(
id: impl Into<egui::Id>,
title: impl Into<String>,
open: &mut bool,
) -> MaterialDialog<'_> {
MaterialDialog::new(id, title, open).dialog_type(DialogType::Form)
}