use egui::{ComboBox, Id, Modal, ProgressBar, Ui, Widget as _, Window}; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Modals { user_modal_open: bool, save_modal_open: bool, save_progress: Option, role: &'static str, name: String, } impl Default for Modals { fn default() -> Self { Self { user_modal_open: false, save_modal_open: false, save_progress: None, role: Self::ROLES[0], name: "John Doe".to_owned(), } } } impl Modals { const ROLES: [&'static str; 2] = ["user", "admin"]; } impl crate::Demo for Modals { fn name(&self) -> &'static str { "🗖 Modals" } fn show(&mut self, ui: &mut egui::Ui, open: &mut bool) { use crate::View as _; Window::new(self.name()) .open(open) .vscroll(false) .resizable(false) .constrain_to(ui.available_rect_before_wrap()) .show(ui, |ui| self.ui(ui)); } } impl crate::View for Modals { fn ui(&mut self, ui: &mut Ui) { let Self { user_modal_open, save_modal_open, save_progress, role, name, } = self; ui.horizontal(|ui| { if ui.button("Open User Modal").clicked() { *user_modal_open = true; } if ui.button("Open Save Modal").clicked() { *save_modal_open = true; } }); ui.label("Click one of the buttons to open a modal."); ui.label("Modals have a backdrop and prevent interaction with the rest of the UI."); ui.label( "You can show modals on top of each other and close the topmost modal with \ escape or by clicking outside the modal.", ); if *user_modal_open { let modal = Modal::new(Id::new("Modal A")).show(ui.ctx(), |ui| { ui.set_width(250.0); ui.heading("Edit User"); ui.label("Name:"); ui.text_edit_singleline(name); ComboBox::new("role", "Role") .selected_text(*role) .show_ui(ui, |ui| { for r in Self::ROLES { ui.selectable_value(role, r, r); } }); ui.separator(); egui::Sides::new().show( ui, |_ui| {}, |ui| { if ui.button("Save").clicked() { *save_modal_open = true; } if ui.button("Cancel").clicked() { // You can call `ui.close()` to close the modal. // (This causes the current modals `should_close` to return true) ui.close(); } }, ); }); if modal.should_close() { *user_modal_open = false; } } if *save_modal_open { let modal = Modal::new(Id::new("Modal B")).show(ui.ctx(), |ui| { ui.set_width(200.0); ui.heading("Save? Are you sure?"); ui.add_space(32.0); egui::Sides::new().show( ui, |_ui| {}, |ui| { if ui.button("Yes Please").clicked() { *save_progress = Some(0.0); } if ui.button("No Thanks").clicked() { ui.close(); } }, ); }); if modal.should_close() { *save_modal_open = false; } } if let Some(progress) = *save_progress { Modal::new(Id::new("Modal C")).show(ui.ctx(), |ui| { ui.set_width(70.0); ui.heading("Saving…"); ProgressBar::new(progress).ui(ui); if progress >= 1.0 { *save_progress = None; *save_modal_open = false; *user_modal_open = false; } else { *save_progress = Some(progress + 0.003); ui.request_repaint(); } }); } ui.vertical_centered(|ui| { ui.add(crate::egui_github_link_file!()); }); } } #[cfg(test)] mod tests { use crate::Demo as _; use crate::demo::modals::Modals; use egui::accesskit::Role; use egui::{Key, Popup}; use egui_kittest::kittest::Queryable as _; use egui_kittest::{Harness, SnapshotResults}; #[test] fn clicking_escape_when_popup_open_should_not_close_modal() { let initial_state = Modals { user_modal_open: true, ..Modals::default() }; let mut harness = Harness::new_ui_state( |ui, modals| { modals.show(ui, &mut true); }, initial_state, ); harness.get_by_role(Role::ComboBox).click(); // Harness::run would fail because we keep requesting repaints to simulate progress. harness.run_ok(); assert!(Popup::is_any_open(&harness.ctx)); assert!(harness.state().user_modal_open); harness.key_press(Key::Escape); harness.run_ok(); assert!(!Popup::is_any_open(&harness.ctx)); assert!(harness.state().user_modal_open); } #[test] fn escape_should_close_top_modal() { let initial_state = Modals { user_modal_open: true, save_modal_open: true, ..Modals::default() }; let mut harness = Harness::new_ui_state( |ui, modals| { modals.show(ui, &mut true); }, initial_state, ); assert!(harness.state().user_modal_open); assert!(harness.state().save_modal_open); harness.key_press(Key::Escape); harness.run(); assert!(harness.state().user_modal_open); assert!(!harness.state().save_modal_open); } #[test] fn should_match_snapshot() { let initial_state = Modals { user_modal_open: true, ..Modals::default() }; let mut harness = Harness::new_ui_state( |ui, modals| { modals.show(ui, &mut true); }, initial_state, ); let mut results = SnapshotResults::new(); harness.run(); results.add(harness.try_snapshot("modals_1")); harness.get_by_label("Save").click(); harness.run_ok(); results.add(harness.try_snapshot("modals_2")); harness.get_by_label("Yes Please").click(); harness.run_ok(); results.add(harness.try_snapshot("modals_3")); } // This tests whether the backdrop actually prevents interaction with lower layers. #[test] fn backdrop_should_prevent_focusing_lower_area() { let initial_state = Modals { save_modal_open: true, save_progress: Some(0.0), ..Modals::default() }; let mut harness = Harness::new_ui_state( |ui, modals| { modals.show(ui, &mut true); }, initial_state, ); harness.run_ok(); harness.get_by_label("Yes Please").click(); harness.run_ok(); // This snapshots should show the progress bar modal on top of the save modal. harness.snapshot("modals_backdrop_should_prevent_focusing_lower_area"); } }