mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 06:40:06 -04:00
Add Modal and Memory::set_modal_layer (#5358)
* Closes #686 * Closes #839 * #5370 should be merged before this * [x] I have followed the instructions in the PR template This adds modals to egui. This PR - adds a new `Modal` struct - adds `Memory::set_modal_layer` to limit focus to a layer and above (used by the modal struct, but could also be used by custom modal implementations) - adds `Memory::allows_interaction` to check if a layer is behind a modal layer, deprecating `Layer::allows_interaction` Current problems: - ~When a button is focused before the modal opens, it stays focused and you also can't hit tab to focus the next widget. Seems like focus is "stuck" on that widget until you hit escape. This might be related to https://github.com/emilk/egui/issues/5359~ fixed! Possible future improvements: - The titlebar from `window` should be made into a separate widget and added to the modal - The state whether the modal is open should be stored in egui (optionally), similar to popup and menu. Ideally before this we would refactor popup state to unify popup and menu --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
287
crates/egui_demo_lib/src/demo/modals.rs
Normal file
287
crates/egui_demo_lib/src/demo/modals.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
use egui::{ComboBox, Context, Id, 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: 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)
|
||||
.vscroll(false)
|
||||
.resizable(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;
|
||||
|
||||
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() {
|
||||
*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);
|
||||
|
||||
egui::Sides::new().show(
|
||||
ui,
|
||||
|_ui| {},
|
||||
|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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[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_label("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_label("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();
|
||||
}
|
||||
}
|
||||
|
||||
// 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_state(
|
||||
|ctx, modals| {
|
||||
modals.show(ctx, &mut true);
|
||||
},
|
||||
initial_state,
|
||||
);
|
||||
|
||||
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
|
||||
harness.run();
|
||||
harness.run();
|
||||
harness.run();
|
||||
|
||||
harness.get_by_label("Yes Please").simulate_click();
|
||||
|
||||
harness.run();
|
||||
|
||||
// This snapshots should show the progress bar modal on top of the save modal.
|
||||
harness.wgpu_snapshot("modals_backdrop_should_prevent_focusing_lower_area");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user