1use egui::{
2 emath::{Align, Align2},
3 epaint::{Color32, Pos2, Rounding},
4 Area, Button, Context, Id, Layout, Response, RichText, Sense, Ui, WidgetText, Window,
5};
6
7const ERROR_ICON_COLOR: Color32 = Color32::from_rgb(200, 90, 90);
8const INFO_ICON_COLOR: Color32 = Color32::from_rgb(150, 200, 210);
9const WARNING_ICON_COLOR: Color32 = Color32::from_rgb(230, 220, 140);
10const SUCCESS_ICON_COLOR: Color32 = Color32::from_rgb(140, 230, 140);
11
12const CAUTION_BUTTON_FILL: Color32 = Color32::from_rgb(87, 38, 34);
13const SUGGESTED_BUTTON_FILL: Color32 = Color32::from_rgb(33, 54, 84);
14const CAUTION_BUTTON_TEXT_COLOR: Color32 = Color32::from_rgb(242, 148, 148);
15const SUGGESTED_BUTTON_TEXT_COLOR: Color32 = Color32::from_rgb(141, 182, 242);
16
17const OVERLAY_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 200);
18
19pub enum ModalButtonStyle {
21 None,
23 Suggested,
25 Caution,
27}
28
29#[derive(Clone, Default, PartialEq)]
32pub enum Icon {
33 #[default]
34 Info,
36 Warning,
38 Success,
40 Error,
42 Custom((String, Color32)),
46}
47
48impl std::fmt::Display for Icon {
49 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
50 match self {
51 Icon::Info => write!(f, "ℹ"),
52 Icon::Warning => write!(f, "⚠"),
53 Icon::Success => write!(f, "✔"),
54 Icon::Error => write!(f, "❗"),
55 Icon::Custom((icon_text, _)) => write!(f, "{icon_text}"),
56 }
57 }
58}
59
60#[derive(Clone, Default)]
61struct DialogData {
62 title: Option<String>,
63 body: Option<String>,
64 icon: Option<Icon>,
65}
66
67#[must_use = "use `DialogBuilder::open`"]
72pub struct DialogBuilder {
73 data: DialogData,
74 modal_id: Id,
75 ctx: Context,
76}
77
78#[derive(Clone)]
79enum ModalType {
80 Modal,
81 Dialog(DialogData),
82}
83
84#[derive(Clone)]
85struct ModalState {
88 is_open: bool,
89 was_outside_clicked: bool,
90 modal_type: ModalType,
91 last_frame_height: Option<f32>,
92}
93
94#[derive(Clone, Debug)]
95pub struct ModalStyle {
98 pub body_margin: f32,
101 pub frame_margin: f32,
104 pub icon_margin: f32,
107 pub icon_size: f32,
109 pub overlay_color: Color32,
111
112 pub caution_button_fill: Color32,
114 pub suggested_button_fill: Color32,
116
117 pub caution_button_text_color: Color32,
119 pub suggested_button_text_color: Color32,
121
122 pub dialog_ok_text: String,
124
125 pub info_icon_color: Color32,
127 pub warning_icon_color: Color32,
129 pub success_icon_color: Color32,
131 pub error_icon_color: Color32,
133
134 pub default_width: Option<f32>,
136 pub default_height: Option<f32>,
138
139 pub body_alignment: Align,
141}
142
143impl ModalState {
144 fn load(ctx: &Context, id: Id) -> Self {
145 ctx.data_mut(|d| d.get_temp(id).unwrap_or_default())
146 }
147 fn save(self, ctx: &Context, id: Id) {
148 ctx.data_mut(|d| d.insert_temp(id, self))
149 }
150}
151
152impl Default for ModalState {
153 fn default() -> Self {
154 Self {
155 was_outside_clicked: false,
156 is_open: false,
157 modal_type: ModalType::Modal,
158 last_frame_height: None,
159 }
160 }
161}
162
163impl Default for ModalStyle {
164 fn default() -> Self {
165 Self {
166 body_margin: 5.,
167 icon_margin: 7.,
168 frame_margin: 2.,
169 icon_size: 30.,
170 overlay_color: OVERLAY_COLOR,
171
172 caution_button_fill: CAUTION_BUTTON_FILL,
173 suggested_button_fill: SUGGESTED_BUTTON_FILL,
174
175 caution_button_text_color: CAUTION_BUTTON_TEXT_COLOR,
176 suggested_button_text_color: SUGGESTED_BUTTON_TEXT_COLOR,
177
178 dialog_ok_text: "ok".to_string(),
179
180 info_icon_color: INFO_ICON_COLOR,
181 warning_icon_color: WARNING_ICON_COLOR,
182 success_icon_color: SUCCESS_ICON_COLOR,
183 error_icon_color: ERROR_ICON_COLOR,
184
185 default_height: None,
186 default_width: None,
187
188 body_alignment: Align::Min,
189 }
190 }
191}
192pub struct Modal {
220 close_on_outside_click: bool,
221 style: ModalStyle,
222 ctx: Context,
223 id: Id,
224 window_id: Id,
225}
226
227fn ui_with_margin<R>(ui: &mut Ui, margin: f32, add_contents: impl FnOnce(&mut Ui) -> R) {
228 egui::Frame::none()
229 .inner_margin(margin)
230 .show(ui, |ui| add_contents(ui));
231}
232
233impl Modal {
234 pub fn new(ctx: &Context, id_source: impl std::fmt::Display) -> Self {
237 let self_id = Id::new(id_source.to_string());
238 Self {
239 window_id: self_id.with("window"),
240 id: self_id,
241 style: ModalStyle::default(),
242 ctx: ctx.clone(),
243 close_on_outside_click: false,
244 }
245 }
246
247 fn set_open_state(&self, is_open: bool) {
248 let mut modal_state = ModalState::load(&self.ctx, self.id);
249 modal_state.is_open = is_open;
250 modal_state.save(&self.ctx, self.id)
251 }
252
253 fn set_outside_clicked(&self, was_clicked: bool) {
254 let mut modal_state = ModalState::load(&self.ctx, self.id);
255 modal_state.was_outside_clicked = was_clicked;
256 modal_state.save(&self.ctx, self.id)
257 }
258
259 pub fn was_outside_clicked(&self) -> bool {
261 let modal_state = ModalState::load(&self.ctx, self.id);
262 modal_state.was_outside_clicked
263 }
264
265 pub fn is_open(&self) -> bool {
267 let modal_state = ModalState::load(&self.ctx, self.id);
268 modal_state.is_open
269 }
270
271 pub fn open(&self) {
277 self.set_open_state(true)
278 }
279
280 pub fn close(&self) {
286 self.set_open_state(false)
287 }
288
289 pub fn with_close_on_outside_click(mut self, do_close_on_click_ouside: bool) -> Self {
292 self.close_on_outside_click = do_close_on_click_ouside;
293 self
294 }
295
296 pub fn with_style(mut self, style: &ModalStyle) -> Self {
298 self.style = style.clone();
299 self
300 }
301
302 pub fn title(&self, ui: &mut Ui, text: impl Into<RichText>) {
310 let text: RichText = text.into();
311 ui.vertical_centered(|ui| {
312 ui.heading(text);
313 });
314 ui.separator();
315 }
316
317 pub fn icon(&self, ui: &mut Ui, icon: Icon) {
327 let color = match icon {
328 Icon::Info => self.style.info_icon_color,
329 Icon::Warning => self.style.warning_icon_color,
330 Icon::Success => self.style.success_icon_color,
331 Icon::Error => self.style.error_icon_color,
332 Icon::Custom((_, color)) => color,
333 };
334 let text = RichText::new(icon.to_string())
335 .color(color)
336 .size(self.style.icon_size);
337 ui_with_margin(ui, self.style.icon_margin, |ui| {
338 ui.add(egui::Label::new(text));
339 });
340 }
341
342 pub fn frame<R>(&self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) {
356 let last_frame_height = ModalState::load(&self.ctx, self.id)
357 .last_frame_height
358 .unwrap_or_default();
359 let default_height = self.style.default_height.unwrap_or_default();
360 let space_height = ((default_height - last_frame_height) * 0.5).max(0.);
361 ui.with_layout(
362 Layout::top_down(Align::Center).with_cross_align(Align::Center),
363 |ui| {
364 ui_with_margin(ui, self.style.frame_margin, |ui| {
365 if space_height > 0. {
366 ui.add_space(space_height);
367 add_contents(ui);
368 ui.add_space(space_height);
369 } else {
370 add_contents(ui);
371 }
372 })
373 },
374 );
375 }
376
377 pub fn body_and_icon(&self, ui: &mut Ui, text: impl Into<WidgetText>, icon: Icon) {
387 egui::Grid::new(self.id).num_columns(2).show(ui, |ui| {
388 self.icon(ui, icon);
389 self.body(ui, text);
390 });
391 }
392
393 pub fn body(&self, ui: &mut Ui, text: impl Into<WidgetText>) {
403 let text: WidgetText = text.into();
404 ui.with_layout(Layout::top_down(self.style.body_alignment), |ui| {
405 ui_with_margin(ui, self.style.body_margin, |ui| {
406 ui.label(text);
407 })
408 });
409 }
410
411 pub fn buttons<R>(&self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) {
421 ui.separator();
422 ui.with_layout(Layout::right_to_left(Align::Min), add_contents);
423 }
424
425 pub fn button(&self, ui: &mut Ui, text: impl Into<WidgetText>) -> Response {
428 self.styled_button(ui, text, ModalButtonStyle::None)
429 }
430
431 pub fn caution_button(&self, ui: &mut Ui, text: impl Into<WidgetText>) -> Response {
434 self.styled_button(ui, text, ModalButtonStyle::Caution)
435 }
436
437 pub fn suggested_button(&self, ui: &mut Ui, text: impl Into<WidgetText>) -> Response {
440 self.styled_button(ui, text, ModalButtonStyle::Suggested)
441 }
442
443 fn styled_button(
444 &self,
445 ui: &mut Ui,
446 text: impl Into<WidgetText>,
447 button_style: ModalButtonStyle,
448 ) -> Response {
449 let button = match button_style {
450 ModalButtonStyle::Suggested => {
451 let text: WidgetText = text.into().color(self.style.suggested_button_text_color);
452 Button::new(text).fill(self.style.suggested_button_fill)
453 }
454 ModalButtonStyle::Caution => {
455 let text: WidgetText = text.into().color(self.style.caution_button_text_color);
456 Button::new(text).fill(self.style.caution_button_fill)
457 }
458 ModalButtonStyle::None => Button::new(text.into()),
459 };
460
461 let response = ui.add(button);
462 if response.clicked() {
463 self.close()
464 }
465 response
466 }
467
468 pub fn show<R>(&self, add_contents: impl FnOnce(&mut Ui) -> R) {
471 let mut modal_state = ModalState::load(&self.ctx, self.id);
472 self.set_outside_clicked(false);
473 if modal_state.is_open {
474 let ctx_clone = self.ctx.clone();
475 let area_resp = Area::new(self.id)
476 .interactable(true)
477 .fixed_pos(Pos2::ZERO)
478 .show(&self.ctx, |ui: &mut Ui| {
479 let screen_rect = ui.ctx().input(|i| i.screen_rect);
480 let area_response = ui.allocate_response(screen_rect.size(), Sense::click());
481 if area_response.clicked() {
487 self.set_outside_clicked(true);
488 if self.close_on_outside_click {
489 self.close();
490 }
491 }
492 ui.painter()
493 .rect_filled(screen_rect, Rounding::ZERO, self.style.overlay_color);
494 });
495
496 ctx_clone.move_to_top(area_resp.response.layer_id);
497
498 let mut window_id = self
501 .style
502 .default_width
503 .map_or(self.window_id, |w| self.window_id.with(w.to_string()));
504
505 window_id = self
506 .style
507 .default_height
508 .map_or(window_id, |h| window_id.with(h.to_string()));
509
510 let mut window = Window::new("")
511 .id(window_id)
512 .open(&mut modal_state.is_open)
513 .title_bar(false)
514 .anchor(Align2::CENTER_CENTER, [0., 0.])
515 .resizable(false);
516
517 let recalculating_height =
518 self.style.default_height.is_some() && modal_state.last_frame_height.is_none();
519
520 if let Some(default_height) = self.style.default_height {
521 window = window.default_height(default_height);
522 }
523
524 if let Some(default_width) = self.style.default_width {
525 window = window.default_width(default_width);
526 }
527
528 let response = window.show(&ctx_clone, add_contents);
529
530 if let Some(inner_response) = response {
531 ctx_clone.move_to_top(inner_response.response.layer_id);
532 if recalculating_height {
533 let mut modal_state = ModalState::load(&self.ctx, self.id);
534 modal_state.last_frame_height = Some(inner_response.response.rect.height());
535 modal_state.save(&self.ctx, self.id);
536 }
537 }
538 }
539 }
540
541 #[deprecated(since = "0.3.0", note = "use `Modal::dialog`")]
545 pub fn open_dialog(
546 &self,
547 title: Option<impl std::fmt::Display>,
548 body: Option<impl std::fmt::Display>,
549 icon: Option<Icon>,
550 ) {
551 let modal_data = DialogData {
552 title: title.map(|s| s.to_string()),
553 body: body.map(|s| s.to_string()),
554 icon,
555 };
556 let mut modal_state = ModalState::load(&self.ctx, self.id);
557 modal_state.modal_type = ModalType::Dialog(modal_data);
558 modal_state.is_open = true;
559 modal_state.save(&self.ctx, self.id);
560 }
561
562 pub fn dialog(&self) -> DialogBuilder {
565 DialogBuilder {
566 data: DialogData::default(),
567 modal_id: self.id.clone(),
568 ctx: self.ctx.clone(),
569 }
570 }
571
572 pub fn show_dialog(&mut self) {
575 let modal_state = ModalState::load(&self.ctx, self.id);
576 if let ModalType::Dialog(modal_data) = modal_state.modal_type {
577 self.close_on_outside_click = true;
578 self.show(|ui| {
579 if let Some(title) = modal_data.title {
580 self.title(ui, title)
581 }
582 self.frame(ui, |ui| {
583 if modal_data.body.is_none() {
584 if let Some(icon) = modal_data.icon {
585 self.icon(ui, icon)
586 }
587 } else if modal_data.icon.is_none() {
588 if let Some(body) = modal_data.body {
589 self.body(ui, body)
590 }
591 } else if modal_data.icon.is_some() && modal_data.icon.is_some() {
592 self.body_and_icon(ui, modal_data.body.unwrap(), modal_data.icon.unwrap())
593 }
594 });
595 self.buttons(ui, |ui| {
596 ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
597 self.button(ui, &self.style.dialog_ok_text)
598 })
599 })
600 });
601 }
602 }
603}
604
605impl DialogBuilder {
606 pub fn with_title(mut self, title: impl std::fmt::Display) -> Self {
608 self.data.title = Some(title.to_string());
609 self
610 }
611 pub fn with_body(mut self, body: impl std::fmt::Display) -> Self {
613 self.data.body = Some(body.to_string());
614 self
615 }
616 pub fn with_icon(mut self, icon: Icon) -> Self {
618 self.data.icon = Some(icon);
619 self
620 }
621 pub fn open(self) {
626 let mut modal_state = ModalState::load(&self.ctx, self.modal_id);
627 modal_state.modal_type = ModalType::Dialog(self.data);
628 modal_state.is_open = true;
629 modal_state.save(&self.ctx, self.modal_id);
630 }
631}