From e120daa1a9ccec2fe594fc640951fb85e9443b6d Mon Sep 17 00:00:00 2001 From: Umatriz Date: Fri, 7 Jun 2024 15:35:42 +0300 Subject: [PATCH 1/6] feat: add `PopupCloseBehavior` `PopupCloseBehavior` determines when popup will be closed. - `CloseOnClick` popup will be closed if the click happens anywhere even in the popup's body - `CloseOnClickAway` popup will be closed if the click happens somewhere else but in the popup's body. --- crates/egui/src/containers/combo_box.rs | 1 + crates/egui/src/containers/popup.rs | 31 ++++++++-- .../src/demo/demo_app_windows.rs | 1 + crates/egui_demo_lib/src/demo/tests.rs | 56 +++++++++++++++++++ 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index f4c18b51b92..ab14e38ccf3 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -380,6 +380,7 @@ fn combo_box_dyn<'c, R>( popup_id, &button_response, above_or_below, + PopupCloseBehavior::CloseOnClick, |ui| { ScrollArea::vertical() .max_height(height) diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index a196830d509..cbc8e943a97 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -253,11 +253,24 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool { }) } +/// Determines popup's close behavior +#[derive(Clone, Copy)] +pub enum PopupCloseBehavior { + /// Popup will be closed on click + /// It is used in the [`ComboBox`] + CloseOnClick, + + /// Popup will be closed if the click happened somewhere else + /// but in the popup's body + CloseOnClickAway, +} + /// Helper for [`popup_above_or_below_widget`]. pub fn popup_below_widget( ui: &Ui, popup_id: Id, widget_response: &Response, + close_behavior: PopupCloseBehavior, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { popup_above_or_below_widget( @@ -265,6 +278,7 @@ pub fn popup_below_widget( popup_id, widget_response, AboveOrBelow::Below, + close_behavior, add_contents, ) } @@ -299,6 +313,7 @@ pub fn popup_above_or_below_widget( popup_id: Id, widget_response: &Response, above_or_below: AboveOrBelow, + close_behavior: PopupCloseBehavior, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { if parent_ui.memory(|mem| mem.is_popup_open(popup_id)) { @@ -311,7 +326,7 @@ pub fn popup_above_or_below_widget( let frame_margin = frame.total_margin(); let inner_width = widget_response.rect.width() - frame_margin.sum().x; - let inner = Area::new(popup_id) + let response = Area::new(popup_id) .kind(UiKind::Popup) .order(Order::Foreground) .fixed_pos(pos) @@ -327,13 +342,19 @@ pub fn popup_above_or_below_widget( .inner }) .inner - }) - .inner; + }); + + let is_close = match close_behavior { + PopupCloseBehavior::CloseOnClick => widget_response.clicked_elsewhere(), + PopupCloseBehavior::CloseOnClickAway => { + widget_response.clicked_elsewhere() && response.response.clicked_elsewhere() + } + }; - if parent_ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() { + if parent_ui.input(|i| i.key_pressed(Key::Escape)) || is_close { parent_ui.memory_mut(|mem| mem.close_popup()); } - Some(inner) + Some(response.inner) } else { None } 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 74c5edefb52..adc53d53bf4 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -113,6 +113,7 @@ impl Default for Tests { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), ]) } } diff --git a/crates/egui_demo_lib/src/demo/tests.rs b/crates/egui_demo_lib/src/demo/tests.rs index 44e355d0e8e..6641690481f 100644 --- a/crates/egui_demo_lib/src/demo/tests.rs +++ b/crates/egui_demo_lib/src/demo/tests.rs @@ -1,3 +1,5 @@ +use crate::demo::View; + #[derive(Default)] pub struct CursorTest {} @@ -601,3 +603,57 @@ fn lorem_ipsum(ui: &mut egui::Ui, text: &str) { }, ); } + +// ---------------------------------------------------------------------------- + +#[derive(Default)] +pub struct PopupsTest { + is_checked: bool, + selected: u8, +} + +impl super::Demo for PopupsTest { + fn name(&self) -> &'static str { + "Popups test" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + use egui::*; + + Window::new("Popups test") + .open(open) + .show(ctx, |ui| self.ui(ui)); + } +} + +impl super::View for PopupsTest { + fn ui(&mut self, ui: &mut egui::Ui) { + use egui::*; + + let button_response = ui.button("Open a popup"); + let popup_id = Id::new("test_popup_1_id"); + + if button_response.clicked() { + ui.memory_mut(|mem| mem.toggle_popup(popup_id)); + } + + popup::popup_below_widget( + ui, + popup_id, + &button_response, + PopupCloseBehavior::CloseOnClickAway, + |ui| { + ui.label("Some text"); + ui.checkbox(&mut self.is_checked, "A checkbox"); + }, + ); + + ComboBox::from_label("ComboBox") + .selected_text(format!("{}", self.selected)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.selected, 1, "1"); + ui.selectable_value(&mut self.selected, 2, "2"); + ui.selectable_value(&mut self.selected, 3, "3"); + }); + } +} From 44c6d3b61fe34ea0b4b1b3c9efa9b92b133ab2ae Mon Sep 17 00:00:00 2001 From: Umatriz Date: Fri, 7 Jun 2024 16:08:01 +0300 Subject: [PATCH 2/6] feat: add helper labels to the test --- crates/egui_demo_lib/src/demo/tests.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/egui_demo_lib/src/demo/tests.rs b/crates/egui_demo_lib/src/demo/tests.rs index 6641690481f..b58bdcb67ca 100644 --- a/crates/egui_demo_lib/src/demo/tests.rs +++ b/crates/egui_demo_lib/src/demo/tests.rs @@ -630,6 +630,7 @@ impl super::View for PopupsTest { fn ui(&mut self, ui: &mut egui::Ui) { use egui::*; + ui.label("PopupCloseBehavior::CloseOnClickAway example"); let button_response = ui.button("Open a popup"); let popup_id = Id::new("test_popup_1_id"); @@ -648,6 +649,7 @@ impl super::View for PopupsTest { }, ); + ui.label("PopupCloseBehavior::CloseOnClick example"); ComboBox::from_label("ComboBox") .selected_text(format!("{}", self.selected)) .show_ui(ui, |ui| { From c9e14fdcc2a27947151e91e717efa82d03a80690 Mon Sep 17 00:00:00 2001 From: Umatriz Date: Fri, 7 Jun 2024 16:24:57 +0300 Subject: [PATCH 3/6] remove unused import --- crates/egui_demo_lib/src/demo/tests.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/tests.rs b/crates/egui_demo_lib/src/demo/tests.rs index b58bdcb67ca..988f297d711 100644 --- a/crates/egui_demo_lib/src/demo/tests.rs +++ b/crates/egui_demo_lib/src/demo/tests.rs @@ -1,5 +1,3 @@ -use crate::demo::View; - #[derive(Default)] pub struct CursorTest {} @@ -620,9 +618,10 @@ impl super::Demo for PopupsTest { fn show(&mut self, ctx: &egui::Context, open: &mut bool) { use egui::*; - Window::new("Popups test") - .open(open) - .show(ctx, |ui| self.ui(ui)); + Window::new("Popups test").open(open).show(ctx, |ui| { + use super::View as _; + self.ui(ui); + }); } } From 738bb07ab478cebac3fc3259d6edccd8e189a3f3 Mon Sep 17 00:00:00 2001 From: Umatriz Date: Sat, 8 Jun 2024 08:52:06 +0300 Subject: [PATCH 4/6] remove `PopupTest` and move it to the `examples` --- Cargo.lock | 8 +++ .../src/demo/demo_app_windows.rs | 1 - crates/egui_demo_lib/src/demo/tests.rs | 57 ------------------- examples/popups/Cargo.toml | 22 +++++++ examples/popups/README.md | 5 ++ examples/popups/src/main.rs | 52 +++++++++++++++++ 6 files changed, 87 insertions(+), 58 deletions(-) create mode 100644 examples/popups/Cargo.toml create mode 100644 examples/popups/README.md create mode 100644 examples/popups/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 9453d2070c6..e05a5881d27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2867,6 +2867,14 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" +[[package]] +name = "popups" +version = "0.27.2" +dependencies = [ + "eframe", + "env_logger", +] + [[package]] name = "powerfmt" version = "0.2.0" 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 adc53d53bf4..74c5edefb52 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -113,7 +113,6 @@ impl Default for Tests { Box::::default(), Box::::default(), Box::::default(), - Box::::default(), ]) } } diff --git a/crates/egui_demo_lib/src/demo/tests.rs b/crates/egui_demo_lib/src/demo/tests.rs index 988f297d711..44e355d0e8e 100644 --- a/crates/egui_demo_lib/src/demo/tests.rs +++ b/crates/egui_demo_lib/src/demo/tests.rs @@ -601,60 +601,3 @@ fn lorem_ipsum(ui: &mut egui::Ui, text: &str) { }, ); } - -// ---------------------------------------------------------------------------- - -#[derive(Default)] -pub struct PopupsTest { - is_checked: bool, - selected: u8, -} - -impl super::Demo for PopupsTest { - fn name(&self) -> &'static str { - "Popups test" - } - - fn show(&mut self, ctx: &egui::Context, open: &mut bool) { - use egui::*; - - Window::new("Popups test").open(open).show(ctx, |ui| { - use super::View as _; - self.ui(ui); - }); - } -} - -impl super::View for PopupsTest { - fn ui(&mut self, ui: &mut egui::Ui) { - use egui::*; - - ui.label("PopupCloseBehavior::CloseOnClickAway example"); - let button_response = ui.button("Open a popup"); - let popup_id = Id::new("test_popup_1_id"); - - if button_response.clicked() { - ui.memory_mut(|mem| mem.toggle_popup(popup_id)); - } - - popup::popup_below_widget( - ui, - popup_id, - &button_response, - PopupCloseBehavior::CloseOnClickAway, - |ui| { - ui.label("Some text"); - ui.checkbox(&mut self.is_checked, "A checkbox"); - }, - ); - - ui.label("PopupCloseBehavior::CloseOnClick example"); - ComboBox::from_label("ComboBox") - .selected_text(format!("{}", self.selected)) - .show_ui(ui, |ui| { - ui.selectable_value(&mut self.selected, 1, "1"); - ui.selectable_value(&mut self.selected, 2, "2"); - ui.selectable_value(&mut self.selected, 3, "3"); - }); - } -} diff --git a/examples/popups/Cargo.toml b/examples/popups/Cargo.toml new file mode 100644 index 00000000000..2f9d6b09263 --- /dev/null +++ b/examples/popups/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "popups" +edition.workspace = true +license.workspace = true +rust-version.workspace = true +version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } +env_logger = { version = "0.10", default-features = false, features = [ + "auto-color", + "humantime", +] } + + +[lints] +workspace = true diff --git a/examples/popups/README.md b/examples/popups/README.md new file mode 100644 index 00000000000..ce32e895ac2 --- /dev/null +++ b/examples/popups/README.md @@ -0,0 +1,5 @@ +Example of how to use menus, popups, context menus and tooltips. + +```sh +cargo run -p popups +``` diff --git a/examples/popups/src/main.rs b/examples/popups/src/main.rs new file mode 100644 index 00000000000..3f729d3ece2 --- /dev/null +++ b/examples/popups/src/main.rs @@ -0,0 +1,52 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + +use eframe::egui::*; + +fn main() -> Result<(), eframe::Error> { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let options = eframe::NativeOptions::default(); + + eframe::run_native("Popups", options, Box::new(|_| Ok(Box::::default()))) +} + +#[derive(Default)] +struct MyApp { + checkbox: bool, + number: u8, +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) { + CentralPanel::default().show(ctx, |ui| { + ui.label("PopupCloseBehavior::CloseOnClickAway popup"); + let response = ui.button("Open"); + let popup_id = Id::new("popup_id"); + + if response.clicked() { + ui.memory_mut(|mem| mem.toggle_popup(popup_id)); + } + + popup_below_widget( + ui, + popup_id, + &response, + PopupCloseBehavior::CloseOnClickAway, + |ui| { + ui.set_min_width(300.0); + ui.label("This popup will be open even if you click the checkbox"); + ui.checkbox(&mut self.checkbox, "Checkbox"); + }, + ); + + ui.label("PopupCloseBehavior::CloseOnClick popup"); + ComboBox::from_label("ComboBox") + .selected_text(format!("{}", self.number)) + .show_ui(ui, |ui| { + for num in 0..10 { + ui.selectable_value(&mut self.number, num, format!("{num}")); + } + }); + }); + } +} From 85f57f4f447750e61f3e1eacb879afe0d36ffbc0 Mon Sep 17 00:00:00 2001 From: Umatriz Date: Tue, 25 Jun 2024 16:22:33 +0300 Subject: [PATCH 5/6] fixes - Adds new option `PopupCloseBehavior::IgnoreClicks` - Renames `CloseOnClickAway` -> `CloseOnClickOutside` --- crates/egui/src/containers/popup.rs | 18 ++++++++++++------ examples/popups/src/main.rs | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index cbc8e943a97..67665f087f7 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -256,13 +256,18 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool { /// Determines popup's close behavior #[derive(Clone, Copy)] pub enum PopupCloseBehavior { - /// Popup will be closed on click - /// It is used in the [`ComboBox`] + /// Popup will be closed on click anywhere, inside or outside the popup. + /// + /// It is used in [`ComboBox`]. CloseOnClick, /// Popup will be closed if the click happened somewhere else /// but in the popup's body - CloseOnClickAway, + CloseOnClickOutside, + + /// Clicks will be ignored. Popup might be closed manually by calling [`Memory::close_popup`] + /// or by pressing the escape button + IgnoreClicks, } /// Helper for [`popup_above_or_below_widget`]. @@ -344,14 +349,15 @@ pub fn popup_above_or_below_widget( .inner }); - let is_close = match close_behavior { + let should_close = match close_behavior { PopupCloseBehavior::CloseOnClick => widget_response.clicked_elsewhere(), - PopupCloseBehavior::CloseOnClickAway => { + PopupCloseBehavior::CloseOnClickOutside => { widget_response.clicked_elsewhere() && response.response.clicked_elsewhere() } + PopupCloseBehavior::IgnoreClicks => false, }; - if parent_ui.input(|i| i.key_pressed(Key::Escape)) || is_close { + if parent_ui.input(|i| i.key_pressed(Key::Escape)) || should_close { parent_ui.memory_mut(|mem| mem.close_popup()); } Some(response.inner) diff --git a/examples/popups/src/main.rs b/examples/popups/src/main.rs index 3f729d3ece2..761f6406b8f 100644 --- a/examples/popups/src/main.rs +++ b/examples/popups/src/main.rs @@ -31,7 +31,7 @@ impl eframe::App for MyApp { ui, popup_id, &response, - PopupCloseBehavior::CloseOnClickAway, + PopupCloseBehavior::CloseOnClickOutside, |ui| { ui.set_min_width(300.0); ui.label("This popup will be open even if you click the checkbox"); From 74ab38b0e457aa5daff1f88e0ba514ad4fcd0350 Mon Sep 17 00:00:00 2001 From: Umatriz Date: Thu, 27 Jun 2024 08:58:51 +0300 Subject: [PATCH 6/6] fix the doctest --- crates/egui/src/containers/popup.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 67665f087f7..e15e18b3d24 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -306,7 +306,8 @@ pub fn popup_below_widget( /// ui.memory_mut(|mem| mem.toggle_popup(popup_id)); /// } /// let below = egui::AboveOrBelow::Below; -/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| { +/// let close_on_click_outside = egui::popup::PopupCloseBehavior::CloseOnClickOutside; +/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |ui| { /// ui.set_min_width(200.0); // if you want to control the size /// ui.label("Some more info, or things you can select:"); /// ui.label("…");