fix(widgets): open context-menu submenus on hover

The right-click ContextMenu's hover listener was a no-op: it never
tracked the hovered row and never rendered a submenu popup, so every
menu item with a submenu (Proxy, Cache, Color, Show Thumbnails, Zoom,
Playback Resolution...) was a dead end. Hover now drives the row
highlight and opens the parent's submenu anchored to its right (the
menu bar's model), with keyboard Left/Right to close/open it and Enter
expanding a parent row instead of firing it.
This commit is contained in:
2026-08-19 15:21:54 +08:00
parent a761d96c82
commit e521a8af83
+103 -7
View File
@@ -390,6 +390,11 @@ struct ContextMenuState {
position: Point<Pixels>,
menu: Menu,
hovered: Option<usize>,
/// The row whose submenu is open (same one-level model as the menu
/// bar: a hovered parent opens its submenu to the right).
submenu: Option<usize>,
/// The hovered row inside the open submenu (row highlight).
submenu_hovered: Option<usize>,
}
impl ContextMenu {
@@ -412,6 +417,8 @@ impl ContextMenu {
position,
menu,
hovered: None,
submenu: None,
submenu_hovered: None,
});
cx.notify();
}
@@ -440,6 +447,8 @@ impl Render for ContextMenu {
if let Some(state) = self.open.take() {
let menu = state.menu.clone();
let hovered = state.hovered;
let submenu = state.submenu;
let submenu_hovered = state.submenu_hovered;
let position = state.position;
let popup = menu_popup_element(
0,
@@ -454,7 +463,16 @@ impl Render for ContextMenu {
});
this.hide(cx);
}),
cx.listener(|_this, _item: &MenuHovered, _window, _cx| {}),
cx.listener(|this, item: &MenuHovered, _window, cx| {
// Mouse hover drives the row highlight and opens the
// hovered parent's submenu (the menu-bar behavior).
if let Some(state) = this.open.as_mut() {
state.hovered = Some(item.index);
state.submenu = if item.submenu { Some(item.index) } else { None };
state.submenu_hovered = None;
cx.notify();
}
}),
)
.track_focus(&self.focus_handle)
.on_mouse_up_out(
@@ -470,21 +488,53 @@ impl Render for ContextMenu {
let delta = if event.keystroke.key == "up" { -1 } else { 1 };
if let Some(next) = state.menu.navigate(state.hovered, delta) {
state.hovered = Some(next);
state.submenu = state
.menu
.items
.get(next)
.and_then(|item| item.submenu.as_ref().map(|_| next));
cx.notify();
}
}
}
"right" => {
// Open the highlighted parent's submenu (also the
// behavior of Enter on a parent row).
if let Some(state) = this.open.as_mut()
&& let Some(hovered) = state.hovered
&& let Some(item) = state.menu.items.get(hovered)
&& item.submenu.is_some()
{
state.submenu = Some(hovered);
state.submenu_hovered = None;
cx.notify();
}
}
"left" => {
if let Some(state) = this.open.as_mut()
&& state.submenu.take().is_some()
{
cx.notify();
}
}
"enter" | "space" => {
if let Some(state) = this.open.as_ref()
if let Some(state) = this.open.as_mut()
&& let Some(hovered) = state.hovered
&& let Some(item) = state.menu.items.get(hovered)
&& item.enabled
{
cx.emit(ContextMenuEvent {
item: item.id,
label: item.label.clone(),
});
this.hide(cx);
if item.submenu.is_some() {
state.submenu = Some(hovered);
state.submenu_hovered = None;
cx.notify();
} else {
let event = ContextMenuEvent {
item: item.id,
label: item.label.clone(),
};
cx.emit(event);
this.hide(cx);
}
}
}
"escape" => this.hide(cx),
@@ -494,11 +544,57 @@ impl Render for ContextMenu {
root =
root.child(deferred(anchored().position(position).child(popup)).with_priority(1));
// A hovered/opened parent's submenu, anchored to the right of
// the popup (mirrors the menu bar's submenu placement).
if let Some(hovered) = hovered
&& submenu == Some(hovered)
&& let Some(item) = menu.items.get(hovered)
&& let Some(submenu_menu) = item.submenu.clone()
{
let sub_popup = menu_popup_element(
1000,
&submenu_menu,
submenu_hovered,
"menu-submenu-popup",
&colors,
cx.listener(|this, clicked: &MenuClicked, _window, cx| {
cx.emit(ContextMenuEvent {
item: clicked.id,
label: clicked.label.clone(),
});
this.hide(cx);
}),
cx.listener(|this, item: &MenuHovered, _window, cx| {
if let Some(state) = this.open.as_mut() {
state.submenu_hovered = Some(item.index);
cx.notify();
}
}),
);
let width = f32::from(menu_width(&menu));
// Align the submenu with the parent's row so it opens next
// to the item, not the first row.
let top = ROW_HEIGHT * (hovered as f32 + 1.0);
root = root.child(
deferred(
anchored()
.position(position)
.offset(point(px(width + 2.0), px(top)))
.snap_to_window_with_margin(px(8.0))
.child(sub_popup),
)
.with_priority(2),
);
}
window.focus(&self.focus_handle, cx);
self.open = Some(ContextMenuState {
position,
menu,
hovered,
submenu,
submenu_hovered,
});
}