Deprecate `Memory::popup` API in favor of new `Popup` API (#7317)

* Closes  #7037
* Closes #7297

This deprecates all popup-related function in `Memory`, replacing them
with the new `egui::Popup`.

The new API is nicer in all ways, so we should encourage people to use
it.
This commit is contained in:
Emil Ernerfeldt 2025-07-09 12:55:06 +02:00 committed by GitHub
parent fbe0aadf63
commit a7f14ca176
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 108 additions and 33 deletions

View File

@ -293,7 +293,7 @@ impl ComboBox {
/// Check if the [`ComboBox`] with the given id has its popup menu currently opened.
pub fn is_open(ctx: &Context, id: Id) -> bool {
ctx.memory(|m| m.is_popup_open(Self::widget_to_popup_id(id)))
Popup::is_id_open(ctx, Self::widget_to_popup_id(id))
}
/// Convert a [`ComboBox`] id to the id used to store it's popup state.
@ -315,7 +315,7 @@ fn combo_box_dyn<'c, R>(
) -> InnerResponse<Option<R>> {
let popup_id = ComboBox::widget_to_popup_id(button_id);
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
let is_popup_open = Popup::is_id_open(ui.ctx(), popup_id);
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());

View File

@ -1,7 +1,8 @@
use emath::{Align2, Vec2};
use crate::{
Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind,
};
use emath::{Align2, Vec2};
/// A modal dialog.
///
@ -80,13 +81,11 @@ impl Modal {
frame,
} = self;
let (is_top_modal, any_popup_open) = ctx.memory_mut(|mem| {
let is_top_modal = ctx.memory_mut(|mem| {
mem.set_modal_layer(area.layer());
(
mem.top_modal_layer() == Some(area.layer()),
mem.any_popup_open(),
)
mem.top_modal_layer() == Some(area.layer())
});
let any_popup_open = crate::Popup::is_any_open(ctx);
let InnerResponse {
inner: (inner, backdrop_response),
response,

View File

@ -1,11 +1,15 @@
use crate::containers::menu::{MenuConfig, MenuState, menu_style};
use crate::style::StyleModifier;
#![expect(deprecated)] // This is a new, safe wrapper around the old `Memory::popup` API.
use std::iter::once;
use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
use crate::{
Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
Sense, Ui, UiKind, UiStackInfo,
containers::menu::{MenuConfig, MenuState, menu_style},
style::StyleModifier,
};
use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
use std::iter::once;
/// What should we anchor the popup to?
///
@ -64,9 +68,7 @@ impl PopupAnchor {
match self {
Self::ParentRect(rect) => Some(rect),
Self::Pointer => ctx.pointer_hover_pos().map(Rect::from_pos),
Self::PointerFixed => ctx
.memory(|mem| mem.popup_position(popup_id))
.map(Rect::from_pos),
Self::PointerFixed => Popup::position_of_id(ctx, popup_id).map(Rect::from_pos),
Self::Position(pos) => Some(Rect::from_pos(pos)),
}
}
@ -122,12 +124,12 @@ enum OpenKind<'a> {
impl OpenKind<'_> {
/// Returns `true` if the popup should be open
fn is_open(&self, id: Id, ctx: &Context) -> bool {
fn is_open(&self, popup_id: Id, ctx: &Context) -> bool {
match self {
OpenKind::Open => true,
OpenKind::Closed => false,
OpenKind::Bool(open) => **open,
OpenKind::Memory { .. } => ctx.memory(|mem| mem.is_popup_open(id)),
OpenKind::Memory { .. } => Popup::is_id_open(ctx, popup_id),
}
}
}
@ -217,7 +219,7 @@ impl<'a> Popup<'a> {
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
pub fn from_response(response: &Response) -> Self {
let mut popup = Self::new(
response.id.with("popup"),
Self::default_response_id(response),
response.ctx.clone(),
response,
response.layer_id,
@ -455,7 +457,7 @@ impl<'a> Popup<'a> {
OpenKind::Open => true,
OpenKind::Closed => false,
OpenKind::Bool(open) => **open,
OpenKind::Memory { .. } => self.ctx.memory(|mem| mem.is_popup_open(self.id)),
OpenKind::Memory { .. } => Self::is_id_open(&self.ctx, self.id),
}
}
@ -504,26 +506,26 @@ impl<'a> Popup<'a> {
let id = self.id;
if let OpenKind::Memory { set } = self.open_kind {
self.ctx.memory_mut(|mem| match set {
match set {
Some(SetOpenCommand::Bool(open)) => {
if open {
match self.anchor {
PopupAnchor::PointerFixed => {
mem.open_popup_at(id, hover_pos);
self.ctx.memory_mut(|mem| mem.open_popup_at(id, hover_pos));
}
_ => mem.open_popup(id),
_ => Popup::open_id(&self.ctx, id),
}
} else {
mem.close_popup(id);
Self::close_id(&self.ctx, id);
}
}
Some(SetOpenCommand::Toggle) => {
mem.toggle_popup(id);
Self::toggle_id(&self.ctx, id);
}
None => {
mem.keep_popup_open(id);
self.ctx.memory_mut(|mem| mem.keep_popup_open(id));
}
});
}
}
if !self.open_kind.is_open(self.id, &self.ctx) {
@ -627,3 +629,65 @@ impl<'a> Popup<'a> {
Some(response)
}
}
/// ## Static methods
impl Popup<'_> {
/// The default ID when constructing a popup from the [`Response`] of e.g. a button.
pub fn default_response_id(response: &Response) -> Id {
response.id.with("popup")
}
/// Is the given popup open?
///
/// This assumes the use of either:
/// * [`Self::open_memory`]
/// * [`Self::from_toggle_button_response`]
/// * [`Self::menu`]
/// * [`Self::context_menu`]
///
/// The popup id should be the same as either you set with [`Self::id`] or the
/// default one from [`Self::default_response_id`].
pub fn is_id_open(ctx: &Context, popup_id: Id) -> bool {
ctx.memory(|mem| mem.is_popup_open(popup_id))
}
/// Is any popup open?
///
/// This assumes the egui memory is being used to track the open state of popups.
pub fn is_any_open(ctx: &Context) -> bool {
ctx.memory(|mem| mem.any_popup_open())
}
/// Open the given popup and close all others.
///
/// If you are NOT using [`Popup::show`], you must
/// also call [`crate::Memory::keep_popup_open`] as long as
/// you're showing the popup.
pub fn open_id(ctx: &Context, popup_id: Id) {
ctx.memory_mut(|mem| mem.open_popup(popup_id));
}
/// Toggle the given popup between closed and open.
///
/// Note: At most, only one popup can be open at a time.
pub fn toggle_id(ctx: &Context, popup_id: Id) {
ctx.memory_mut(|mem| mem.toggle_popup(popup_id));
}
/// Close all currently open popups.
pub fn close_all(ctx: &Context) {
ctx.memory_mut(|mem| mem.close_all_popups());
}
/// Close the given popup, if it is open.
///
/// See also [`Self::close_all`] if you want to close any / all currently open popups.
pub fn close_id(ctx: &Context, popup_id: Id) {
ctx.memory_mut(|mem| mem.close_popup(popup_id));
}
/// Get the position for this popup, if it is open.
pub fn position_of_id(ctx: &Context, popup_id: Id) -> Option<Pos2> {
ctx.memory(|mem| mem.popup_position(popup_id))
}
}

View File

@ -1012,11 +1012,11 @@ impl OpenPopup {
}
}
/// ## Popups
/// Popups are things like combo-boxes, color pickers, menus etc.
/// Only one can be open at a time.
/// ## Deprecated popup API
/// Use [`crate::Popup`] instead.
impl Memory {
/// Is the given popup open?
#[deprecated = "Use Popup::is_id_open instead"]
pub fn is_popup_open(&self, popup_id: Id) -> bool {
self.popups
.get(&self.viewport_id)
@ -1025,6 +1025,7 @@ impl Memory {
}
/// Is any popup open?
#[deprecated = "Use Popup::is_any_open instead"]
pub fn any_popup_open(&self) -> bool {
self.popups.contains_key(&self.viewport_id) || self.everything_is_visible()
}
@ -1032,6 +1033,7 @@ 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.
#[deprecated = "Use Popup::open_id instead"]
pub fn open_popup(&mut self, popup_id: Id) {
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, None));
@ -1042,6 +1044,7 @@ impl Memory {
/// 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.
#[deprecated = "Use Popup::show instead"]
pub fn keep_popup_open(&mut self, popup_id: Id) {
if let Some(state) = self.popups.get_mut(&self.viewport_id) {
if state.id == popup_id {
@ -1051,12 +1054,14 @@ impl Memory {
}
/// Open the popup and remember its position.
#[deprecated = "Use Popup with PopupAnchor::Position instead"]
pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into<Option<Pos2>>) {
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, pos.into()));
}
/// Get the position for this popup.
#[deprecated = "Use Popup::position_of_id instead"]
pub fn popup_position(&self, id: Id) -> Option<Pos2> {
self.popups
.get(&self.viewport_id)
@ -1064,6 +1069,7 @@ impl Memory {
}
/// Close any currently open popup.
#[deprecated = "Use Popup::close_all instead"]
pub fn close_all_popups(&mut self) {
self.popups.clear();
}
@ -1071,7 +1077,9 @@ impl Memory {
/// Close the given popup, if it is open.
///
/// See also [`Self::close_all_popups`] if you want to close any / all currently open popups.
#[deprecated = "Use Popup::close_id instead"]
pub fn close_popup(&mut self, popup_id: Id) {
#[expect(deprecated)]
if self.is_popup_open(popup_id) {
self.popups.remove(&self.viewport_id);
}
@ -1080,14 +1088,18 @@ impl Memory {
/// Toggle the given popup between closed and open.
///
/// Note: At most, only one popup can be open at a time.
#[deprecated = "Use Popup::toggle_id instead"]
pub fn toggle_popup(&mut self, popup_id: Id) {
#[expect(deprecated)]
if self.is_popup_open(popup_id) {
self.close_popup(popup_id);
} else {
self.open_popup(popup_id);
}
}
}
impl Memory {
/// If true, all windows, menus, tooltips, etc., will be visible at once.
///
/// This is useful for testing, benchmarking, pre-caching, etc.

View File

@ -491,7 +491,7 @@ pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> b
pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response {
let popup_id = ui.auto_id_with("popup");
let open = ui.memory(|mem| mem.is_popup_open(popup_id));
let open = Popup::is_id_open(ui.ctx(), popup_id);
let mut button_response = color_button(ui, (*hsva).into(), open);
if ui.style().explanation_tooltips {
button_response = button_response.on_hover_text("Click to edit color");

View File

@ -164,8 +164,8 @@ impl crate::View for Modals {
mod tests {
use crate::Demo as _;
use crate::demo::modals::Modals;
use egui::Key;
use egui::accesskit::Role;
use egui::{Key, Popup};
use egui_kittest::kittest::Queryable as _;
use egui_kittest::{Harness, SnapshotResults};
@ -187,12 +187,12 @@ mod tests {
// Harness::run would fail because we keep requesting repaints to simulate progress.
harness.run_ok();
assert!(harness.ctx.memory(|mem| mem.any_popup_open()));
assert!(Popup::is_any_open(&harness.ctx));
assert!(harness.state().user_modal_open);
harness.key_press(Key::Escape);
harness.run_ok();
assert!(!harness.ctx.memory(|mem| mem.any_popup_open()));
assert!(!Popup::is_any_open(&harness.ctx));
assert!(harness.state().user_modal_open);
}