From c4c1d34bd785e2bf0453f18293718fb4a17e5e87 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Wed, 19 Aug 2026 17:41:40 +0800 Subject: [PATCH] fix(widgets): context-menu hover tracking, submenus, and snap-proof placement The right-click ContextMenu never tracked hover (the listener was a no-op), so rows did not highlight and parent items never opened their submenus. Hover now drives the highlight and opens the parent's submenu, with Left/Right and Enter keyboard parity. The submenu is an absolutely positioned child of the popup instead of a second window-anchored popup computed from the raw click point, so it follows the popup's final snapped/switched bounds and can never detach from its parent row. --- crates/gpui_widgets/src/menu/mod.rs | 45 +++++++++++++++++------------ 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/crates/gpui_widgets/src/menu/mod.rs b/crates/gpui_widgets/src/menu/mod.rs index 348484b3fb..c5e4b7835d 100644 --- a/crates/gpui_widgets/src/menu/mod.rs +++ b/crates/gpui_widgets/src/menu/mod.rs @@ -450,7 +450,7 @@ impl Render for ContextMenu { let submenu = state.submenu; let submenu_hovered = state.submenu_hovered; let position = state.position; - let popup = menu_popup_element( + let mut popup = menu_popup_element( 0, &menu, hovered, @@ -542,11 +542,10 @@ 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). + // A hovered/opened parent's submenu, as an absolutely positioned + // child of the popup: it follows the popup's FINAL bounds (after + // snap/anchor-switch), so it can never detach from its parent the + // way a second window-anchored popup did. if let Some(hovered) = hovered && submenu == Some(hovered) && let Some(item) = menu.items.get(hovered) @@ -572,22 +571,30 @@ impl Render for ContextMenu { } }), ); - 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), + // The parent's row top: the popup's py-1 padding plus every + // preceding row (a separator is 1px + my-1 on both sides). + let mut top = px(4.0); + for item in menu.items.iter().take(hovered) { + top += if Menu::is_separator(item) { + px(9.0) + } else { + px(ROW_HEIGHT) + }; + } + let width = menu_width(&menu) + px(2.0); + popup = popup.child( + div() + .absolute() + .left(width) + .top(top) + .child(sub_popup) + .into_any_element(), ); } + root = + root.child(deferred(anchored().position(position).child(popup)).with_priority(1)); + window.focus(&self.focus_handle, cx); self.open = Some(ContextMenuState { position,