From bf4d7df88421d3d5e3fd9a1df05f1a40453f45c7 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Thu, 14 Nov 2024 14:09:59 +0100 Subject: [PATCH] Add proper example and tests --- crates/egui/src/containers/mod.rs | 1 + crates/egui/src/containers/modal.rs | 22 +- .../src/demo/demo_app_windows.rs | 1 + crates/egui_demo_lib/src/demo/mod.rs | 1 + crates/egui_demo_lib/src/demo/modals.rs | 237 ++++++++++++++++++ .../tests/snapshots/demos/Modals.png | 3 + .../tests/snapshots/modals_1.png | 3 + .../tests/snapshots/modals_2.png | 3 + .../tests/snapshots/modals_3.png | 3 + examples/hello_world_simple/src/main.rs | 110 +------- 10 files changed, 279 insertions(+), 105 deletions(-) create mode 100644 crates/egui_demo_lib/src/demo/modals.rs create mode 100644 crates/egui_demo_lib/tests/snapshots/demos/Modals.png create mode 100644 crates/egui_demo_lib/tests/snapshots/modals_1.png create mode 100644 crates/egui_demo_lib/tests/snapshots/modals_2.png create mode 100644 crates/egui_demo_lib/tests/snapshots/modals_3.png diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index 5b967bafb3c..e68e0def1b0 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -19,6 +19,7 @@ pub use { collapsing_header::{CollapsingHeader, CollapsingResponse}, combo_box::*, frame::Frame, + modal::{Modal, ModalResponse}, panel::{CentralPanel, SidePanel, TopBottomPanel}, popup::*, resize::Resize, diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs index 441eaa68eef..83ff3d0684a 100644 --- a/crates/egui/src/containers/modal.rs +++ b/crates/egui/src/containers/modal.rs @@ -22,9 +22,12 @@ impl Modal { } pub fn show(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse { - let is_top_modal = ctx.memory_mut(|mem| { + let (is_top_modal, any_popup_open) = ctx.memory_mut(|mem| { mem.set_modal_layer(self.area.layer()); - mem.top_modal_layer() == Some(self.area.layer()) + ( + mem.top_modal_layer() == Some(self.area.layer()), + mem.any_popup_open(), + ) }); let InnerResponse { inner: (inner, backdrop_response), @@ -57,6 +60,7 @@ impl Modal { backdrop_response, inner, is_top_modal, + any_popup_open, } } } @@ -82,19 +86,21 @@ pub struct ModalResponse { pub backdrop_response: Response, pub inner: T, pub is_top_modal: bool, + /// Is there any popup open? + /// We need to check this before the modal contents are shown, so we can know if any popup + /// was open when checking if the escape key was clicked. + pub any_popup_open: bool, } impl ModalResponse { /// Should the modal be closed? /// Returns true if: /// - the backdrop was clicked - /// - this is the top most modal and the escape key was pressed + /// - this is the top most modal, no popup is open and the escape key was pressed pub fn should_close(&self) -> bool { + let ctx = &self.response.ctx; + let escape_clicked = ctx.input(|i| i.key_pressed(crate::Key::Escape)); self.backdrop_response.clicked() - || (self.is_top_modal - && self - .response - .ctx - .input(|i| i.key_pressed(crate::Key::Escape))) + || (self.is_top_modal && !self.any_popup_open && escape_clicked) } } diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 790e7289953..cd8d2b1b534 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -33,6 +33,7 @@ impl Default for Demos { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index 828bdd896a3..8c9034868e4 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -17,6 +17,7 @@ pub mod frame_demo; pub mod highlighting; pub mod interactive_container; pub mod misc_demo_window; +pub mod modals; pub mod multi_touch; pub mod paint_bezier; pub mod painting; diff --git a/crates/egui_demo_lib/src/demo/modals.rs b/crates/egui_demo_lib/src/demo/modals.rs new file mode 100644 index 00000000000..e222479390e --- /dev/null +++ b/crates/egui_demo_lib/src/demo/modals.rs @@ -0,0 +1,237 @@ +use egui::{vec2, Align, ComboBox, Context, Id, Layout, Modal, ProgressBar, Ui, Widget, 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, ctx: &Context, open: &mut bool) { + use crate::View as _; + Window::new(self.name()) + .open(open) + .default_size(vec2(512.0, 512.0)) + .vscroll(false) + .show(ctx, |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; + + if ui.button("Open User Modal").clicked() { + *user_modal_open = true; + } + + if ui.button("Open Save Modal").clicked() { + *save_modal_open = true; + } + + 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(); + + ui.with_layout(Layout::right_to_left(Align::Min), |ui| { + if ui.button("Save").clicked() { + *save_modal_open = true; + } + if ui.button("Cancel").clicked() { + *user_modal_open = false; + } + }); + }); + + 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); + + ui.with_layout(Layout::right_to_left(Align::Min), |ui| { + if ui.button("Yes Please").clicked() { + *save_progress = Some(0.0); + } + + if ui.button("No Thanks").clicked() { + *save_modal_open = false; + } + }); + }); + + 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.ctx().request_repaint(); + } + }); + } + } +} + +#[cfg(test)] +mod tests { + use crate::demo::modals::Modals; + use crate::Demo; + use egui::accesskit::Role; + use egui::Key; + use egui_kittest::kittest::Queryable; + use egui_kittest::Harness; + + #[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_state( + |ctx, modals| { + modals.show(ctx, &mut true); + }, + initial_state, + ); + + harness.get_by_role(Role::ComboBox).click(); + + harness.run(); + assert!(harness.ctx.memory(|mem| mem.any_popup_open())); + assert!(harness.state().user_modal_open); + + harness.press_key(Key::Escape); + harness.run(); + assert!(!harness.ctx.memory(|mem| mem.any_popup_open())); + 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_state( + |ctx, modals| { + modals.show(ctx, &mut true); + }, + initial_state, + ); + + assert!(harness.state().user_modal_open); + assert!(harness.state().save_modal_open); + + harness.press_key(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_state( + |ctx, modals| { + modals.show(ctx, &mut true); + }, + initial_state, + ); + + let mut results = Vec::new(); + + harness.run(); + results.push(harness.try_wgpu_snapshot("modals_1")); + + harness.get_by_name("Save").click(); + // TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests + harness.run(); + harness.run(); + harness.run(); + results.push(harness.try_wgpu_snapshot("modals_2")); + + harness.get_by_name("Yes Please").click(); + // TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests + harness.run(); + harness.run(); + harness.run(); + results.push(harness.try_wgpu_snapshot("modals_3")); + + for result in results { + result.unwrap(); + } + } +} diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png new file mode 100644 index 00000000000..52570abf39d --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8621e1fe1795ee780e603045574abf208bc94a39eac3bc822988026c7de185d +size 7640 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png new file mode 100644 index 00000000000..9960411d907 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67363dd4c7dbef7d5c1da75067dce5aee399b4db16a9e62216aac1dba6458a3a +size 26197 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png new file mode 100644 index 00000000000..1e450fdcd82 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7db471ddf1fd7efa4ce4e5cfaf9f84c44c42bd5eb56973f86637410441a87de1 +size 28846 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png new file mode 100644 index 00000000000..d834f632e26 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:411405b136d26c1fa7778b88fd0a359e4f700c0fd7ee7f3c7334576cdc2ac3c8 +size 27010 diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index 8ef0a7fded7..4fe49a89d68 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -2,8 +2,6 @@ #![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; -use eframe::egui::modal::Modal; -use eframe::egui::{Align, ComboBox, Id, Layout, ProgressBar, Widget}; fn main() -> eframe::Result { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -13,105 +11,23 @@ fn main() -> eframe::Result { ..Default::default() }; - let mut user_modal_open = false; - let mut save_modal_open = false; - let mut save_progress = None; - - let roles = ["user", "admin"]; - let mut role = roles[0]; - - let mut name = "John Doe".to_string(); + // Our application state: + let mut name = "Arthur".to_owned(); + let mut age = 42; eframe::run_simple_native("My egui App", options, move |ctx, _frame| { egui::CentralPanel::default().show(ctx, |ui| { - if ui.button("Open User Modal").clicked() { - user_modal_open = true; - } - - if ui.button("Open Save Modal").clicked() { - save_modal_open = true; - } - - 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(&mut name); - - ComboBox::new("role", "Role") - .selected_text(role) - .show_ui(ui, |ui| { - for r in &roles { - ui.selectable_value(&mut role, r, *r); - } - }); - - ui.separator(); - - ui.with_layout(Layout::right_to_left(Align::Min), |ui| { - if ui.button("Save").clicked() { - save_modal_open = true; - } - if ui.button("Cancel").clicked() { - user_modal_open = false; - } - }); - }); - - 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); - - ui.with_layout(Layout::right_to_left(Align::Min), |ui| { - if ui.button("Yes Please").clicked() { - save_progress = Some(0.0); - } - - if ui.button("No Thanks").clicked() { - save_modal_open = false; - } - }); - }); - - 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.ctx().request_repaint(); - } - }); - } - }); - - egui::Window::new("My Window").show(ctx, |ui| { - if ui.button("show modal").clicked() { - user_modal_open = true; + ui.heading("My egui Application"); + ui.horizontal(|ui| { + let name_label = ui.label("Your name: "); + ui.text_edit_singleline(&mut name) + .labelled_by(name_label.id); + }); + ui.add(egui::Slider::new(&mut age, 0..=120).text("age")); + if ui.button("Increment").clicked() { + age += 1; } + ui.label(format!("Hello '{name}', age {age}")); }); }) }