Skip to content

Commit

Permalink
Add proper example and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasmerlin committed Nov 14, 2024
1 parent 886f636 commit a985092
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 105 deletions.
1 change: 1 addition & 0 deletions crates/egui/src/containers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub use {
collapsing_header::{CollapsingHeader, CollapsingResponse},
combo_box::*,
frame::Frame,
modal::{Modal, ModalResponse},
panel::{CentralPanel, SidePanel, TopBottomPanel},
popup::*,
resize::Resize,
Expand Down
22 changes: 14 additions & 8 deletions crates/egui/src/containers/modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ impl Modal {
}

pub fn show<T>(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse<T> {
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),
Expand Down Expand Up @@ -57,6 +60,7 @@ impl Modal {
backdrop_response,
inner,
is_top_modal,
any_popup_open,
}
}
}
Expand All @@ -82,19 +86,21 @@ pub struct ModalResponse<T> {
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<T> ModalResponse<T> {
/// 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)
}
}
1 change: 1 addition & 0 deletions crates/egui_demo_lib/src/demo/demo_app_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ impl Default for Demos {
Box::<super::highlighting::Highlighting>::default(),
Box::<super::interactive_container::InteractiveContainerDemo>::default(),
Box::<super::MiscDemoWindow>::default(),
Box::<super::modals::Modals>::default(),
Box::<super::multi_touch::MultiTouch>::default(),
Box::<super::painting::Painting>::default(),
Box::<super::pan_zoom::PanZoom>::default(),
Expand Down
1 change: 1 addition & 0 deletions crates/egui_demo_lib/src/demo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
237 changes: 237 additions & 0 deletions crates/egui_demo_lib/src/demo/modals.rs
Original file line number Diff line number Diff line change
@@ -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<f32>,

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: Modals::ROLES[0],
name: "John Doe".to_string(),
}
}
}

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 Modals {
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();
}
}
}
3 changes: 3 additions & 0 deletions crates/egui_demo_lib/tests/snapshots/demos/Modals.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions crates/egui_demo_lib/tests/snapshots/modals_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions crates/egui_demo_lib/tests/snapshots/modals_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions crates/egui_demo_lib/tests/snapshots/modals_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit a985092

Please sign in to comment.