diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 4a457bf03d6..85f67b0b380 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -525,7 +525,9 @@ impl<'a> Popup<'a> { Some(SetOpenCommand::Toggle) => { mem.toggle_popup(id); } - None => {} + None => { + mem.keep_popup_open(id); + } }); } diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index d38e39f7591..f187d97cc23 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -94,7 +94,7 @@ pub struct Memory { /// If position is [`None`], the popup position will be calculated based on some configuration /// (e.g. relative to some other widget). #[cfg_attr(feature = "persistence", serde(skip))] - popup: Option<(Id, Option)>, + popup: Option, #[cfg_attr(feature = "persistence", serde(skip))] everything_is_visible: bool, @@ -807,6 +807,15 @@ impl Memory { self.caches.update(); self.areas_mut().end_pass(); self.focus_mut().end_pass(used_ids); + + // Clean up abandoned popups. + if let Some(popup) = &mut self.popup { + if popup.open_this_frame { + popup.open_this_frame = false; + } else { + self.popup = None; + } + } } pub(crate) fn set_viewport_id(&mut self, viewport_id: ViewportId) { @@ -1068,13 +1077,37 @@ impl Memory { } } +/// State of an open popup. +#[derive(Clone, Copy, Debug)] +struct OpenPopup { + /// Id of the popup. + id: Id, + + /// Optional position of the popup. + pos: Option, + + /// Whether this popup was still open this frame. Otherwise it's considered abandoned and `Memory::popup` will be cleared. + open_this_frame: bool, +} + +impl OpenPopup { + /// Create a new `OpenPopup`. + fn new(id: Id, pos: Option) -> Self { + Self { + id, + pos, + open_this_frame: true, + } + } +} + /// ## Popups /// Popups are things like combo-boxes, color pickers, menus etc. /// Only one can be open at a time. impl Memory { /// Is the given popup open? pub fn is_popup_open(&self, popup_id: Id) -> bool { - self.popup.is_some_and(|(id, _)| id == popup_id) || self.everything_is_visible() + self.popup.is_some_and(|state| state.id == popup_id) || self.everything_is_visible() } /// Is any popup open? @@ -1083,19 +1116,34 @@ impl Memory { } /// Open the given popup and close all others. + /// + /// Note that you must call `keep_popup_open` on subsequent frames as long as the popup is open. pub fn open_popup(&mut self, popup_id: Id) { - self.popup = Some((popup_id, None)); + self.popup = Some(OpenPopup::new(popup_id, None)); + } + + /// Popups must call this every frame while open. + /// + /// This is needed because in some cases popups can go away without `close_popup` being + /// called. For example, when a context menu is open and the underlying widget stops + /// being rendered. + pub fn keep_popup_open(&mut self, popup_id: Id) { + if let Some(state) = self.popup.as_mut() { + if state.id == popup_id { + state.open_this_frame = true; + } + } } /// Open the popup and remember its position. pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into>) { - self.popup = Some((popup_id, pos.into())); + self.popup = Some(OpenPopup::new(popup_id, pos.into())); } /// Get the position for this popup. pub fn popup_position(&self, id: Id) -> Option { self.popup - .and_then(|(popup_id, pos)| if popup_id == id { pos } else { None }) + .and_then(|state| if state.id == id { state.pos } else { None }) } /// Close the open popup, if any.