1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 22:30:03 -04:00

Fix stuck menu when submenu vanishes (#7589)

* Closes https://github.com/rerun-io/rerun/issues/11301

This fixes a bug where a menu could get stuck, not closing at all, when
the currently open submenu stops being shown.
I also added a way to reproduce this to the demo, as well as a test
ensuring that there is no race condition in the fix.
This commit is contained in:
Lucas Meurer
2025-10-07 10:16:35 +02:00
committed by GitHub
parent f0faacc7d1
commit 65249013c4
4 changed files with 137 additions and 22 deletions

View File

@@ -152,7 +152,8 @@ impl MenuState {
pub fn from_id<R>(ctx: &Context, id: Id, f: impl FnOnce(&mut Self) -> R) -> R {
let pass_nr = ctx.cumulative_pass_nr();
ctx.data_mut(|data| {
let state = data.get_temp_mut_or_insert_with(id.with(Self::ID), || Self {
let state_id = id.with(Self::ID);
let mut state = data.get_temp(state_id).unwrap_or(Self {
open_item: None,
last_visible_pass: pass_nr,
});
@@ -160,14 +161,38 @@ impl MenuState {
if state.last_visible_pass + 1 < pass_nr {
state.open_item = None;
}
state.last_visible_pass = pass_nr;
f(state)
if let Some(item) = state.open_item {
if data
.get_temp(item.with(Self::ID))
.is_none_or(|item: Self| item.last_visible_pass + 1 < pass_nr)
{
// If the open item wasn't shown for at least a frame, reset the open item
state.open_item = None;
}
}
let r = f(&mut state);
data.insert_temp(state_id, state);
r
})
}
pub fn mark_shown(ctx: &Context, id: Id) {
let pass_nr = ctx.cumulative_pass_nr();
Self::from_id(ctx, id, |state| {
state.last_visible_pass = pass_nr;
});
}
/// Is the menu with this id the deepest sub menu? (-> no child sub menu is open)
pub fn is_deepest_sub_menu(ctx: &Context, id: Id) -> bool {
Self::from_id(ctx, id, |state| state.open_item.is_none())
///
/// Note: This only returns correct results if called after the menu contents were shown.
pub fn is_deepest_open_sub_menu(ctx: &Context, id: Id) -> bool {
let pass_nr = ctx.cumulative_pass_nr();
let open_item = Self::from_id(ctx, id, |state| state.open_item);
// If we have some open item, check if that was actually shown this frame
open_item.is_none_or(|submenu_id| {
Self::from_id(ctx, submenu_id, |state| state.last_visible_pass != pass_nr)
})
}
}
@@ -399,6 +424,9 @@ impl SubMenu {
}
/// Show the submenu.
///
/// This does some heuristics to check if the `button_response` was the last thing in the
/// menu that was hovered/clicked, and if so, shows the submenu.
pub fn show<R>(
self,
ui: &Ui,
@@ -409,6 +437,7 @@ impl SubMenu {
let id = Self::id_from_widget_id(button_response.id);
// Get the state from the parent menu
let (open_item, menu_id, parent_config) = MenuState::from_ui(ui, |state, stack| {
(state.open_item, stack.id, MenuConfig::from_stack(stack))
});
@@ -452,7 +481,7 @@ impl SubMenu {
set_open = Some(true);
is_open = true;
// Ensure that all other sub menus are closed when we open the menu
MenuState::from_id(ui.ctx(), id, |state| {
MenuState::from_id(ui.ctx(), menu_id, |state| {
state.open_item = None;
});
}
@@ -488,7 +517,7 @@ impl SubMenu {
if let Some(popup_response) = &popup_response {
// If no child sub menu is open means we must be the deepest child sub menu.
let is_deepest_submenu = MenuState::is_deepest_sub_menu(ui.ctx(), id);
let is_deepest_submenu = MenuState::is_deepest_open_sub_menu(ui.ctx(), id);
// If the user clicks and the cursor is not hovering over our menu rect, it's
// safe to assume they clicked outside the menu, so we close everything.

View File

@@ -604,8 +604,11 @@ impl<'a> Popup<'a> {
PopupCloseBehavior::IgnoreClicks => false,
};
// Mark the menu as shown, so the sub menu open state is not reset
MenuState::mark_shown(&ctx, id);
// If a submenu is open, the CloseBehavior is handled there
let is_any_submenu_open = !MenuState::is_deepest_sub_menu(&response.response.ctx, id);
let is_any_submenu_open = !MenuState::is_deepest_open_sub_menu(&response.response.ctx, id);
let should_close = (!is_any_submenu_open && closed_by_click)
|| ctx.input(|i| i.key_pressed(Key::Escape))