Skip to content

menu: fix submenu dismiss propagation and ESC handling#2232

Open
boboshan wants to merge 1 commit intolongbridge:mainfrom
boboshan:feat/submenu-parent-wire
Open

menu: fix submenu dismiss propagation and ESC handling#2232
boboshan wants to merge 1 commit intolongbridge:mainfrom
boboshan:feat/submenu-parent-wire

Conversation

@boboshan
Copy link
Copy Markdown
Contributor

@boboshan boboshan commented Apr 7, 2026

Summary

Fixes #2229 — three related fixes for submenu dismiss behavior in PopupMenu:

  1. Auto-wire parent_menu in render(): Submenus added via PopupMenuItem::submenu() (the data constructor) don't get parent_menu set, unlike PopupMenu::submenu() (the builder method). This breaks the dismiss chain. Now PopupMenu::render() auto-wires parent_menu on any submenu child that doesn't have it set. This is needed because table delegates operate in Context<TableState> and cannot call PopupMenu::submenu() which requires Context<PopupMenu>.

  2. Fix ESC not closing menus when a submenu is open: When a submenu is opened by hover, the parent menu retains focus. Pressing ESC would hit the parent's dismiss(), which bailed out early because active_submenu() was Some. Split dismiss into dismiss (ESC action handler that clears the active submenu first) and dismiss_menu (internal dismiss that propagates up the chain).

  3. Guard handle_dismiss for submenu clicks: handle_dismiss (click-outside) returns early if a submenu is active. Without this, clicking a submenu item triggers the parent's on_mouse_down_out, tearing down the submenu before the item's on_click fires.

Changes are in popup_menu.rs only — no table code changes needed.

No dependency on #2231.

AI Disclosure

Implementation was developed with AI assistance (Claude). All code has been reviewed, tested, and follows existing popup_menu.rs patterns.

Test plan

  • cargo run --example table — right-click context menu with submenus works
  • cargo run (story) — Menu story context menu: ESC closes full chain
  • Submenu item click fires handler and dismisses menu
  • Click outside closes all menus
  • Hover navigation between submenus still works

Copy link
Copy Markdown
Member

@huacnlee huacnlee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a monkey-patch, not a good solution.

@boboshan boboshan force-pushed the feat/submenu-parent-wire branch from ea6c0f7 to 3f1dfd1 Compare April 8, 2026 06:53
@boboshan boboshan changed the title table: auto-wire parent_menu on delegate context_menu submenus menu: fix submenu dismiss propagation and ESC handling Apr 8, 2026
@boboshan boboshan force-pushed the feat/submenu-parent-wire branch from 3f1dfd1 to 87e7f10 Compare April 9, 2026 12:34
Comment thread crates/ui/src/menu/popup_menu.rs Outdated
}
});
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every frame render will hit this loop...

This is unacceptable.

@boboshan
Copy link
Copy Markdown
Contributor Author

Thanks for the feedback — the render-time approach was wrong.

We've explored several alternatives but each has trade-offs:

Our problem: TableDelegate::context_menu() receives Context<TableState>, not Context<PopupMenu>. So the delegate can't use PopupMenu::submenu() (which auto-wires parent_menu). It must use PopupMenu::build() + PopupMenuItem::submenu(label, entity), which doesn't set parent_menu. Without it, dismiss doesn't propagate from child submenus to the root context menu.

Options we considered:

  1. Wire in set_selected_index (lazy, on first hover) — works but adds parent-wiring responsibility to an unrelated method
  2. Change TableDelegate::context_menu to receive Context<PopupMenu> — clean, but breaking API change
  3. Wire in the table's .context_menu() closure after delegate returns (once per menu open, not per frame) — works, but accesses menu_items directly

Do you have a preferred approach, or a different idea for handling submenus built outside Context<PopupMenu>?

Three related fixes for submenu dismiss behavior in PopupMenu:

1. Wire parent_menu in PopupMenu::build (one-shot at construction).
   Submenus added via PopupMenuItem::submenu(label, entity) — the data
   constructor — can't reach the parent's entity at build time, so they
   land with parent_menu = None. After the user closure populates the
   menu, iterate menu_items once and wire any orphan submenus to point
   at the menu being built. Zero per-frame cost, handles arbitrary
   nesting because every submenu also goes through PopupMenu::build.

   This was previously done in Render::render which iterated and
   notified every frame — replaced with the one-shot approach.

2. Fix ESC not closing menus when a submenu is open. When a submenu is
   opened by hover, the parent menu retains focus. Pressing ESC hit the
   parent's dismiss(), which bailed out early because active_submenu()
   was Some. Split dismiss into dismiss (Cancel handler — clears the
   active submenu first) and dismiss_menu (internal — propagates up the
   chain).

3. Guard handle_dismiss for submenu clicks. handle_dismiss
   (click-outside) now returns early if a submenu is active. Without
   this, clicking a submenu item triggers the parent's
   on_mouse_down_out, tearing down the submenu before the item's
   on_click fires.

Fixes longbridge#2229
@boboshan boboshan force-pushed the feat/submenu-parent-wire branch from 87e7f10 to 6f60e63 Compare April 25, 2026 17:34
@boboshan
Copy link
Copy Markdown
Contributor Author

Rebased onto current main and rewrote the wiring approach to address the per-frame objection.

The parent_menu wiring now happens once inside PopupMenu::build, after the user's builder closure populates menu_items. Every PopupMenu (context menus, dropdowns, plain popups) funnels through build, so the wiring is centralized. Nested submenus are handled because each submenu also goes through its own PopupMenu::build invocation. Zero per-frame cost.

The is_none() guard means the existing PopupMenu::submenu() builder path (which already wires parent_menu correctly) is unaffected — it just observes that the field is already set and skips. No API change to TableDelegate.

Tested locally in a downstream app with table context menus carrying nested collection submenus: ESC closes the full chain, click-outside closes everything, click on a submenu item fires the action and dismisses cleanly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

table: auto-wire parent_menu on delegate context_menu submenus

2 participants