From 3dfacda48cfb924741445b7a232e33c3277b5321 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Thu, 14 Nov 2024 11:50:24 +0100 Subject: [PATCH 1/8] Fix disabled widgets "eating" focus fixes https://github.com/emilk/egui/issues/5359 --- Cargo.lock | 1 + crates/egui/Cargo.toml | 4 ++++ crates/egui/src/context.rs | 6 ++++-- crates/egui/tests/regression_tests.rs | 30 +++++++++++++++++++++++++++ crates/egui_kittest/src/lib.rs | 19 +++++++++++++++++ 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 crates/egui/tests/regression_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 000165018c6..db2ed667e9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1229,6 +1229,7 @@ dependencies = [ "ahash", "backtrace", "document-features", + "egui_kittest", "emath", "epaint", "log", diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 487a4eac6e9..d000c0da5d6 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -98,3 +98,7 @@ log = { workspace = true, optional = true } puffin = { workspace = true, optional = true } ron = { workspace = true, optional = true } serde = { workspace = true, optional = true, features = ["derive", "rc"] } + + +[dev-dependencies] +egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 331430c7865..99a1b536258 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1160,6 +1160,8 @@ impl Context { /// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)). #[allow(clippy::too_many_arguments)] pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response { + let interested_in_focus = w.enabled && w.sense.focusable && w.layer_id.allow_interaction(); + // Remember this widget self.write(|ctx| { let viewport = ctx.viewport(); @@ -1169,12 +1171,12 @@ impl Context { // but also to know when we have reached the widget we are checking for cover. viewport.this_pass.widgets.insert(w.layer_id, w); - if allow_focus && w.sense.focusable { + if allow_focus && interested_in_focus { ctx.memory.interested_in_focus(w.id); } }); - if allow_focus && (!w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction()) { + if allow_focus && !interested_in_focus { // Not interested or allowed input: self.memory_mut(|mem| mem.surrender_focus(w.id)); } diff --git a/crates/egui/tests/regression_tests.rs b/crates/egui/tests/regression_tests.rs new file mode 100644 index 00000000000..b24235713b5 --- /dev/null +++ b/crates/egui/tests/regression_tests.rs @@ -0,0 +1,30 @@ +use egui::Button; +use egui_kittest::kittest::Queryable; +use egui_kittest::Harness; + +#[test] +pub fn focus_should_skip_over_disabled_buttons() { + let mut harness = Harness::new_ui(|ui| { + ui.add(Button::new("Button 1")); + ui.add_enabled(false, Button::new("Button Disabled")); + ui.add(Button::new("Button 3")); + }); + + harness.press_key(egui::Key::Tab); + harness.run(); + + let button_1 = harness.get_by_name("Button 1"); + assert!(button_1.is_focused()); + + harness.press_key(egui::Key::Tab); + harness.run(); + + let button_3 = harness.get_by_name("Button 3"); + assert!(button_3.is_focused()); + + harness.press_key(egui::Key::Tab); + harness.run(); + + let button_1 = harness.get_by_name("Button 1"); + assert!(button_1.is_focused()); +} diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 8e410d84d11..2c75732bc7d 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -249,6 +249,25 @@ impl<'a, State> Harness<'a, State> { pub fn state_mut(&mut self) -> &mut State { &mut self.state } + + /// Press a key. + /// This will create a key down event and a key up event. + pub fn press_key(&mut self, key: egui::Key) { + self.input.events.push(egui::Event::Key { + key, + pressed: true, + modifiers: Default::default(), + repeat: false, + physical_key: None, + }); + self.input.events.push(egui::Event::Key { + key, + pressed: false, + modifiers: Default::default(), + repeat: false, + physical_key: None, + }); + } } /// Utilities for stateless harnesses. From 18311d74be2f168b151d8bf99cd533426705aa7f Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 5 Nov 2024 22:48:32 +0100 Subject: [PATCH 2/8] Add modal --- crates/egui/src/containers/mod.rs | 1 + crates/egui/src/containers/modal.rs | 79 +++++++++++++++++ examples/hello_world_simple/src/main.rs | 110 +++++++++++++++++++++--- 3 files changed, 177 insertions(+), 13 deletions(-) create mode 100644 crates/egui/src/containers/modal.rs diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index 3dd75a4458e..5b967bafb3c 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod area; pub mod collapsing_header; mod combo_box; pub mod frame; +pub mod modal; pub mod panel; pub mod popup; pub(crate) mod resize; diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs new file mode 100644 index 00000000000..561f5ddc5bb --- /dev/null +++ b/crates/egui/src/containers/modal.rs @@ -0,0 +1,79 @@ +use crate::{ + Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, +}; +use emath::{Align2, Vec2}; + +pub struct Modal { + pub area: Area, + pub backdrop_color: Color32, + pub frame: Option, +} + +pub struct ModalResponse { + pub response: Response, + pub backdrop_response: Response, + pub inner: T, +} + +impl Modal { + pub fn new(id: Id) -> Self { + Self { + area: Area::new(id) + .sense(Sense::hover()) + .anchor(Align2::CENTER_CENTER, Vec2::ZERO) + .order(Order::Foreground), + backdrop_color: Color32::from_black_alpha(100), + frame: None, + } + } + + pub fn show(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse { + let InnerResponse { + inner: (inner, backdrop_response), + response, + } = self.area.show(ctx, |ui| { + // TODO: Is screen_rect the right thing to use here? + let mut backdrop = ui.new_child(UiBuilder::new().max_rect(ui.ctx().screen_rect())); + let backdrop_response = backdrop_ui(&mut backdrop, self.backdrop_color); + + let frame = self.frame.unwrap_or_else(|| Frame::popup(ui.style())); + + // We need the extra scope with the sense since frame can't have a sense and since we + // need to prevent the clicks from passing through to the backdrop. + let inner = ui + .scope_builder( + UiBuilder::new().sense(Sense { + click: true, + drag: true, + focusable: false, + }), + |ui| frame.show(ui, content).inner, + ) + .inner; + + (inner, backdrop_response) + }); + + ModalResponse { + response, + backdrop_response, + inner, + } + } +} + +fn backdrop_ui(ui: &mut Ui, color: Color32) -> Response { + // Ensure we capture any click and drag events + let response = ui.allocate_response( + ui.available_size(), + Sense { + click: true, + drag: true, + focusable: false, + }, + ); + + ui.painter().rect_filled(response.rect, 0.0, color); + + response +} diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index 4fe49a89d68..653943e79e2 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -2,6 +2,8 @@ #![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`). @@ -11,23 +13,105 @@ fn main() -> eframe::Result { ..Default::default() }; - // Our application state: - let mut name = "Arthur".to_owned(); - let mut age = 42; + let mut save_modal_open = false; + let mut user_modal_open = false; + let mut save_progress = None; + + let roles = ["user", "admin"]; + let mut role = roles[0]; + + let mut name = "John Doe".to_string(); eframe::run_simple_native("My egui App", options, move |ctx, _frame| { egui::CentralPanel::default().show(ctx, |ui| { - 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; + if ui.button("Open Modal A").clicked() { + save_modal_open = true; + } + + if ui.button("Open Modal B").clicked() { + user_modal_open = true; + } + + if save_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() { + user_modal_open = true; + } + if ui.button("Cancel").clicked() { + save_modal_open = false; + } + }); + }); + + if modal.backdrop_response.clicked() { + save_modal_open = false; + } + } + + if user_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() { + user_modal_open = false; + } + }); + }); + + if modal.backdrop_response.clicked() { + user_modal_open = false; + } + } + + if let Some(progress) = save_progress { + let modal = 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; + user_modal_open = false; + save_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.label(format!("Hello '{name}', age {age}")); }); }) } From 0b7f8dec3270243277442786cae87b63b3798bfb Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 6 Nov 2024 13:48:10 +0100 Subject: [PATCH 3/8] Add set_modal_layer --- crates/egui/src/containers/modal.rs | 1 + crates/egui/src/context.rs | 2 +- crates/egui/src/memory/mod.rs | 72 +++++++++++++++++++++++-- crates/egui/src/widgets/drag_value.rs | 2 +- examples/hello_world_simple/src/main.rs | 26 ++++----- 5 files changed, 83 insertions(+), 20 deletions(-) diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs index 561f5ddc5bb..7fb4f7b0486 100644 --- a/crates/egui/src/containers/modal.rs +++ b/crates/egui/src/containers/modal.rs @@ -28,6 +28,7 @@ impl Modal { } pub fn show(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse { + ctx.memory_mut(|mem| mem.set_modal_layer(self.area.layer())); let InnerResponse { inner: (inner, backdrop_response), response, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 99a1b536258..f0fd8969fc4 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1172,7 +1172,7 @@ impl Context { viewport.this_pass.widgets.insert(w.layer_id, w); if allow_focus && interested_in_focus { - ctx.memory.interested_in_focus(w.id); + ctx.memory.interested_in_focus(w.id, w.layer_id); } }); diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 75183bdd15b..5197f8e6bf4 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -513,6 +513,11 @@ pub(crate) struct Focus { /// Set when looking for widget with navigational keys like arrows, tab, shift+tab. focus_direction: FocusDirection, + /// The top-most modal layer from the previous frame. + top_modal_layer: Option, + /// The top-most modal layer from the current frame. + top_modal_layer_current_frame: Option, + /// A cache of widget IDs that are interested in focus with their corresponding rectangles. focus_widgets_cache: IdMap, } @@ -623,13 +628,15 @@ impl Focus { self.focused_widget = None; } } + + self.top_modal_layer = self.top_modal_layer_current_frame.take(); } pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool { self.id_previous_frame == Some(id) } - fn interested_in_focus(&mut self, id: Id) { + fn interested_in_focus(&mut self, id: Id, layer_id: LayerId) { #[cfg(feature = "accesskit")] { if self.id_requested_by_accesskit == Some(id.accesskit_id()) { @@ -676,6 +683,10 @@ impl Focus { self.last_interested = Some(id); } + fn set_modal_layer(&mut self, layer_id: LayerId) { + self.top_modal_layer_current_frame = Some(layer_id); + } + fn reset_focus(&mut self) { self.focus_direction = FocusDirection::None; } @@ -884,9 +895,37 @@ impl Memory { /// e.g. before deciding which type of underlying widget to use, /// as in the [`crate::DragValue`] widget, so a widget can be focused /// and rendered correctly in a single frame. + /// + /// Pass in the `layer_id` of the layer that the widget is in. #[inline(always)] - pub fn interested_in_focus(&mut self, id: Id) { - self.focus_mut().interested_in_focus(id); + pub fn interested_in_focus(&mut self, id: Id, layer_id: LayerId) { + // If the widget is on a layer below the current modal layer, ignore it. + if let Some(modal_layer) = self.focus().and_then(|f| f.top_modal_layer) { + if matches!( + self.areas().compare_order(layer_id, modal_layer), + std::cmp::Ordering::Less + ) { + return; + } + } + + self.focus_mut().interested_in_focus(id, layer_id); + } + + /// Limit focus to widgets on the given layer and above. + /// If this is called multiple times per frame, the top layer wins. + pub fn set_modal_layer(&mut self, layer_id: LayerId) { + if let Some(current) = self.focus().and_then(|f| f.top_modal_layer_current_frame) { + dbg!(self.areas().compare_order(layer_id, current)); + if matches!( + self.areas().compare_order(layer_id, current), + std::cmp::Ordering::Less + ) { + return; + } + } + + self.focus_mut().set_modal_layer(layer_id); } /// Stop editing the active [`TextEdit`](crate::TextEdit) (if any). @@ -1037,6 +1076,9 @@ impl Memory { // ---------------------------------------------------------------------------- +/// Map containing the index of each layer in the order list, for quick lookups. +type OrderMap = HashMap; + /// Keeps track of [`Area`](crate::containers::area::Area)s, which are free-floating [`Ui`](crate::Ui)s. /// These [`Area`](crate::containers::area::Area)s can be in any [`Order`]. #[derive(Clone, Debug, Default)] @@ -1048,6 +1090,9 @@ pub struct Areas { /// Back-to-front, top is last. order: Vec, + /// Actual order of the layers, pre-calculated each frame. + order_map: OrderMap, + visible_last_frame: ahash::HashSet, visible_current_frame: ahash::HashSet, @@ -1079,8 +1124,24 @@ impl Areas { } /// For each layer, which [`Self::order`] is it in? - pub(crate) fn order_map(&self) -> HashMap { - self.order + pub(crate) fn order_map(&self) -> &OrderMap { + &self.order_map + } + + /// Compare the order of two layers, based on the order list from last frame. + /// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list. + pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> std::cmp::Ordering { + if let (Some(a), Some(b)) = (self.order_map.get(&a), self.order_map.get(&b)) { + a.cmp(b) + } else { + a.order.cmp(&b.order) + } + } + + /// Calculate the order map. + fn calculate_order_map(&mut self) { + self.order_map = self + .order .iter() .enumerate() .map(|(i, id)| (*id, i)) @@ -1209,6 +1270,7 @@ impl Areas { }; order.splice(parent_pos..=parent_pos, moved_layers); } + self.calculate_order_map(); } } diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index feae91ade1e..5f19a84294c 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -452,7 +452,7 @@ impl<'a> Widget for DragValue<'a> { // in button mode for just one frame. This is important for // screen readers. let is_kb_editing = ui.memory_mut(|mem| { - mem.interested_in_focus(id); + mem.interested_in_focus(id, ui.layer_id()); mem.has_focus(id) }); diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index 653943e79e2..9cfa6dd8d38 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -13,8 +13,8 @@ fn main() -> eframe::Result { ..Default::default() }; - let mut save_modal_open = false; let mut user_modal_open = false; + let mut save_modal_open = false; let mut save_progress = None; let roles = ["user", "admin"]; @@ -24,15 +24,15 @@ fn main() -> eframe::Result { eframe::run_simple_native("My egui App", options, move |ctx, _frame| { egui::CentralPanel::default().show(ctx, |ui| { - if ui.button("Open Modal A").clicked() { - save_modal_open = true; + if ui.button("Open User Modal").clicked() { + user_modal_open = true; } - if ui.button("Open Modal B").clicked() { - user_modal_open = true; + if ui.button("Open Save Modal").clicked() { + save_modal_open = true; } - if save_modal_open { + if user_modal_open { let modal = Modal::new(Id::new("Modal A")).show(ui.ctx(), |ui| { ui.set_width(250.0); @@ -53,20 +53,20 @@ fn main() -> eframe::Result { ui.with_layout(Layout::right_to_left(Align::Min), |ui| { if ui.button("Save").clicked() { - user_modal_open = true; + save_modal_open = true; } if ui.button("Cancel").clicked() { - save_modal_open = false; + user_modal_open = false; } }); }); if modal.backdrop_response.clicked() { - save_modal_open = false; + user_modal_open = false; } } - if user_modal_open { + 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?"); @@ -79,13 +79,13 @@ fn main() -> eframe::Result { } if ui.button("No Thanks").clicked() { - user_modal_open = false; + save_modal_open = false; } }); }); if modal.backdrop_response.clicked() { - user_modal_open = false; + save_modal_open = false; } } @@ -98,8 +98,8 @@ fn main() -> eframe::Result { if progress >= 1.0 { save_progress = None; - user_modal_open = false; save_modal_open = false; + user_modal_open = false; } else { save_progress = Some(progress + 0.003); ui.ctx().request_repaint(); From d4019c36d7d85405df6fe318ddb739ca52958115 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Thu, 14 Nov 2024 12:42:30 +0100 Subject: [PATCH 4/8] Fix focus problems --- crates/egui/src/context.rs | 3 ++- crates/egui/src/layers.rs | 1 + crates/egui/src/memory/mod.rs | 38 ++++++++++++++++++++++++----------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index f0fd8969fc4..dfa1820180c 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1160,7 +1160,8 @@ impl Context { /// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)). #[allow(clippy::too_many_arguments)] pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response { - let interested_in_focus = w.enabled && w.sense.focusable && w.layer_id.allow_interaction(); + let interested_in_focus = + w.enabled && w.sense.focusable && self.memory(|mem| mem.allows_interaction(w.layer_id)); // Remember this widget self.write(|ctx| { diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 9105ece2591..b44daa41858 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -95,6 +95,7 @@ impl LayerId { } #[inline(always)] + #[deprecated = "Use `Memory::allows_interaction` instead"] pub fn allow_interaction(&self) -> bool { self.order.allow_interaction() } diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 5197f8e6bf4..0892e7c397d 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -636,7 +636,7 @@ impl Focus { self.id_previous_frame == Some(id) } - fn interested_in_focus(&mut self, id: Id, layer_id: LayerId) { + fn interested_in_focus(&mut self, id: Id) { #[cfg(feature = "accesskit")] { if self.id_requested_by_accesskit == Some(id.accesskit_id()) { @@ -687,6 +687,10 @@ impl Focus { self.top_modal_layer_current_frame = Some(layer_id); } + pub(crate) fn top_modal_layer(&self) -> Option { + self.top_modal_layer + } + fn reset_focus(&mut self) { self.focus_direction = FocusDirection::None; } @@ -888,6 +892,24 @@ impl Memory { } } + /// Does this layer allow interaction? + /// Returns true if + /// - the layer is not behind a modal layer + /// - the [`Order`] allows interaction + pub fn allows_interaction(&self, layer_id: LayerId) -> bool { + let is_above_modal_layer = + if let Some(modal_layer) = self.focus().and_then(|f| f.top_modal_layer) { + matches!( + self.areas().compare_order(layer_id, modal_layer), + std::cmp::Ordering::Equal | std::cmp::Ordering::Greater + ) + } else { + true + }; + let ordering_allows_interaction = layer_id.order.allow_interaction(); + is_above_modal_layer && ordering_allows_interaction + } + /// Register this widget as being interested in getting keyboard focus. /// This will allow the user to select it with tab and shift-tab. /// This is normally done automatically when handling interactions, @@ -899,24 +921,16 @@ impl Memory { /// Pass in the `layer_id` of the layer that the widget is in. #[inline(always)] pub fn interested_in_focus(&mut self, id: Id, layer_id: LayerId) { - // If the widget is on a layer below the current modal layer, ignore it. - if let Some(modal_layer) = self.focus().and_then(|f| f.top_modal_layer) { - if matches!( - self.areas().compare_order(layer_id, modal_layer), - std::cmp::Ordering::Less - ) { - return; - } + if !self.allows_interaction(layer_id) { + return; } - - self.focus_mut().interested_in_focus(id, layer_id); + self.focus_mut().interested_in_focus(id); } /// Limit focus to widgets on the given layer and above. /// If this is called multiple times per frame, the top layer wins. pub fn set_modal_layer(&mut self, layer_id: LayerId) { if let Some(current) = self.focus().and_then(|f| f.top_modal_layer_current_frame) { - dbg!(self.areas().compare_order(layer_id, current)); if matches!( self.areas().compare_order(layer_id, current), std::cmp::Ordering::Less From 886f636939f22d7e0237ec9e3cc6924ee4d4b212 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Thu, 14 Nov 2024 13:07:17 +0100 Subject: [PATCH 5/8] Allow closing the modal via escape --- crates/egui/src/containers/modal.rs | 34 ++++++++++++++++++++----- crates/egui/src/memory/mod.rs | 9 +++++-- examples/hello_world_simple/src/main.rs | 6 ++--- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs index 7fb4f7b0486..441eaa68eef 100644 --- a/crates/egui/src/containers/modal.rs +++ b/crates/egui/src/containers/modal.rs @@ -9,12 +9,6 @@ pub struct Modal { pub frame: Option, } -pub struct ModalResponse { - pub response: Response, - pub backdrop_response: Response, - pub inner: T, -} - impl Modal { pub fn new(id: Id) -> Self { Self { @@ -28,7 +22,10 @@ impl Modal { } pub fn show(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse { - ctx.memory_mut(|mem| mem.set_modal_layer(self.area.layer())); + let is_top_modal = ctx.memory_mut(|mem| { + mem.set_modal_layer(self.area.layer()); + mem.top_modal_layer() == Some(self.area.layer()) + }); let InnerResponse { inner: (inner, backdrop_response), response, @@ -59,6 +56,7 @@ impl Modal { response, backdrop_response, inner, + is_top_modal, } } } @@ -78,3 +76,25 @@ fn backdrop_ui(ui: &mut Ui, color: Color32) -> Response { response } + +pub struct ModalResponse { + pub response: Response, + pub backdrop_response: Response, + pub inner: T, + pub is_top_modal: 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 + pub fn should_close(&self) -> bool { + self.backdrop_response.clicked() + || (self.is_top_modal + && self + .response + .ctx + .input(|i| i.key_pressed(crate::Key::Escape))) + } +} diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 0892e7c397d..5dbc199bf9a 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -942,6 +942,11 @@ impl Memory { self.focus_mut().set_modal_layer(layer_id); } + /// Get the top modal layer (from the previous frame). + pub fn top_modal_layer(&self) -> Option { + self.focus()?.top_modal_layer() + } + /// Stop editing the active [`TextEdit`](crate::TextEdit) (if any). #[inline(always)] pub fn stop_text_input(&mut self) { @@ -1152,14 +1157,14 @@ impl Areas { } } - /// Calculate the order map. + /// Calculates the order map. fn calculate_order_map(&mut self) { self.order_map = self .order .iter() .enumerate() .map(|(i, id)| (*id, i)) - .collect() + .collect(); } pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::AreaState) { diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index 9cfa6dd8d38..8ef0a7fded7 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -61,7 +61,7 @@ fn main() -> eframe::Result { }); }); - if modal.backdrop_response.clicked() { + if modal.should_close() { user_modal_open = false; } } @@ -84,13 +84,13 @@ fn main() -> eframe::Result { }); }); - if modal.backdrop_response.clicked() { + if modal.should_close() { save_modal_open = false; } } if let Some(progress) = save_progress { - let modal = Modal::new(Id::new("Modal C")).show(ui.ctx(), |ui| { + Modal::new(Id::new("Modal C")).show(ui.ctx(), |ui| { ui.set_width(70.0); ui.heading("Saving..."); From bf4d7df88421d3d5e3fd9a1df05f1a40453f45c7 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Thu, 14 Nov 2024 14:09:59 +0100 Subject: [PATCH 6/8] 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}")); }); }) } From 3517f7c40f4e4e67a00c12a4bb67ae6ad7c4bf3b Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Thu, 14 Nov 2024 14:35:12 +0100 Subject: [PATCH 7/8] Add documentation --- crates/egui/src/containers/modal.rs | 62 ++++++++++++++++++++++++++--- crates/egui/src/memory/mod.rs | 1 + 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs index 83ff3d0684a..dbcdc73f18d 100644 --- a/crates/egui/src/containers/modal.rs +++ b/crates/egui/src/containers/modal.rs @@ -3,6 +3,12 @@ use crate::{ }; use emath::{Align2, Vec2}; +/// A modal dialog. +/// Similar to a [`crate::Window`] but centered and with a backdrop that +/// blocks input to the rest of the UI. +/// +/// You can show multiple modals on top of each other. The top most modal will always be +/// the most recently shown one. pub struct Modal { pub area: Area, pub backdrop_color: Color32, @@ -10,17 +16,55 @@ pub struct Modal { } impl Modal { + /// Create a new Modal. The id is passed to the area. pub fn new(id: Id) -> Self { Self { - area: Area::new(id) - .sense(Sense::hover()) - .anchor(Align2::CENTER_CENTER, Vec2::ZERO) - .order(Order::Foreground), + area: Self::default_area(id), backdrop_color: Color32::from_black_alpha(100), frame: None, } } + /// Returns an area customized for a modal. + /// Makes these changes to the default area: + /// - sense: hover + /// - anchor: center + /// - order: foreground + pub fn default_area(id: Id) -> Area { + Area::new(id) + .sense(Sense::hover()) + .anchor(Align2::CENTER_CENTER, Vec2::ZERO) + .order(Order::Foreground) + } + + /// Set the frame of the modal. + /// + /// Default is [`Frame::popup`]. + #[inline] + pub fn frame(mut self, frame: Frame) -> Self { + self.frame = Some(frame); + self + } + + /// Set the backdrop color of the modal. + /// + /// Default is `Color32::from_black_alpha(100)`. + #[inline] + pub fn backdrop_color(mut self, color: Color32) -> Self { + self.backdrop_color = color; + self + } + + /// Set the area of the modal. + /// + /// Default is [`Modal::default_area`]. + #[inline] + pub fn area(mut self, area: Area) -> Self { + self.area = area; + self + } + + /// Show the modal. pub fn show(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse { let (is_top_modal, any_popup_open) = ctx.memory_mut(|mem| { mem.set_modal_layer(self.area.layer()); @@ -33,7 +77,6 @@ impl Modal { inner: (inner, backdrop_response), response, } = self.area.show(ctx, |ui| { - // TODO: Is screen_rect the right thing to use here? let mut backdrop = ui.new_child(UiBuilder::new().max_rect(ui.ctx().screen_rect())); let backdrop_response = backdrop_ui(&mut backdrop, self.backdrop_color); @@ -81,11 +124,20 @@ fn backdrop_ui(ui: &mut Ui, color: Color32) -> Response { response } +/// The response of a modal dialog. pub struct ModalResponse { + /// The response of the modal contents pub response: Response, + + /// The response of the modal backdrop pub backdrop_response: Response, + + /// The inner response from the content closure pub inner: T, + + /// Is this the top most modal? 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. diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 5dbc199bf9a..32eccc4f211 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -515,6 +515,7 @@ pub(crate) struct Focus { /// The top-most modal layer from the previous frame. top_modal_layer: Option, + /// The top-most modal layer from the current frame. top_modal_layer_current_frame: Option, From afea4349e3615902505690b7b4cec3bfc64b8b53 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Thu, 14 Nov 2024 16:06:31 +0100 Subject: [PATCH 8/8] Make the demo a bit more pretty --- crates/egui_demo_lib/src/demo/modals.rs | 29 ++++++++++++++----- .../tests/snapshots/demos/Modals.png | 4 +-- .../tests/snapshots/modals_1.png | 4 +-- .../tests/snapshots/modals_2.png | 4 +-- .../tests/snapshots/modals_3.png | 4 +-- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/modals.rs b/crates/egui_demo_lib/src/demo/modals.rs index e222479390e..75ca2f61161 100644 --- a/crates/egui_demo_lib/src/demo/modals.rs +++ b/crates/egui_demo_lib/src/demo/modals.rs @@ -1,4 +1,4 @@ -use egui::{vec2, Align, ComboBox, Context, Id, Layout, Modal, ProgressBar, Ui, Widget, Window}; +use egui::{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))] @@ -36,8 +36,8 @@ impl crate::Demo for Modals { use crate::View as _; Window::new(self.name()) .open(open) - .default_size(vec2(512.0, 512.0)) .vscroll(false) + .resizable(false) .show(ctx, |ui| self.ui(ui)); } } @@ -52,13 +52,22 @@ impl crate::View for Modals { name, } = self; - if ui.button("Open User Modal").clicked() { - *user_modal_open = true; - } + 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; - } + 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 top most 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| { @@ -134,6 +143,10 @@ impl crate::View for Modals { } }); } + + ui.vertical_centered(|ui| { + ui.add(crate::egui_github_link_file!()); + }); } } diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png index 52570abf39d..fae10cda862 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8621e1fe1795ee780e603045574abf208bc94a39eac3bc822988026c7de185d -size 7640 +oid sha256:a2c1613e655d683fda1b6cffc78645c84a3303759aa99951c2b374b387bc9ba5 +size 32892 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png index 9960411d907..e060f1a27e1 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_1.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67363dd4c7dbef7d5c1da75067dce5aee399b4db16a9e62216aac1dba6458a3a -size 26197 +oid sha256:612fc82a06832ae31ad5d6fecf3954dca89358aa6ddbe3364687c1c155eb09e4 +size 48220 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png index 1e450fdcd82..52a24fe4420 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_2.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7db471ddf1fd7efa4ce4e5cfaf9f84c44c42bd5eb56973f86637410441a87de1 -size 28846 +oid sha256:a9b0e4690fe300c6427d0bf43323023244b71e162097c83e9c2ab7f6e77dc263 +size 48029 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png index d834f632e26..38c2768e4be 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_3.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:411405b136d26c1fa7778b88fd0a359e4f700c0fd7ee7f3c7334576cdc2ac3c8 -size 27010 +oid sha256:78f9c429024268e96a3f0d733172129c0eee29255504204026654c6242796501 +size 43853