Fix first LSP server item flickering and staying highlighted when hovering between multiple language server entries in the status bar menu (#61400)

# Objective

- When a submenu is open in a ContextMenu (e.g., an LSP server entry in
the status bar menu), focus moves from the main menu to the
submenu entity. Hovering over a different submenu trigger calls
`on_hover(true)` which runs `window.focus()` to return focus to the main
menu. This fires `on_focus_in`, which runs `select_toggled_or_first` →
`select_first`, re-selecting the first item. The same issue occurs
in `on_hover(false)` when the mouse leaves a trigger with an open
submenu.

## Solution

- Add a `suppress_focus_selection` flag to `ContextMenu`. Before each
`window.focus()` call in `on_hover` handlers, set the flag to
`true`. The `on_focus_in` callback checks this flag and skips
auto-selection when focus is returning from submenu interaction. The
flag is
reset to `false` after each `on_focus_in` invocation.

## Testing

- Tested manually by opening the LSP status bar menu with multiple
running language servers and hovering between server entries. The
first item no longer flickers or stays highlighted after hovering over
other items.

## Showcase
### before fixed


https://github.com/user-attachments/assets/26efb133-fb7e-4dbe-88b7-88d510dfd3a4



### after fixed


https://github.com/user-attachments/assets/7081c07d-d7f6-4a35-abe3-486bfba8591f


## Release Notes:
- Fixed a bug where the first LSP server item would flicker and stay
highlighted when hovering between multiple language server entries in
the status bar menu.
This commit is contained in:
John authored and GitHub committed 2026-07-23 03:38:00 +00:00
1 parent 4ed45d1b9f
commit b2f2f8d297
1 file changed
+14 -1
+14 -1
View File
@@ -233,6 +233,11 @@ pub struct ContextMenu {
submenu_trigger_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
submenu_trigger_mouse_down: bool,
ignore_blur_until: Option<Instant>,
/// When set to true, the next on_focus_in callback will not automatically
/// select an item. This prevents a visual flash where a submenu close in
/// on_hover(false) returns focus to the main menu and on_focus_in
/// re-selects the first item before the next on_hover(true) clears it.
suppress_focus_selection: bool,
}
#[derive(Copy, Clone, PartialEq, Eq)]
@@ -303,9 +308,10 @@ impl ContextMenu {
// menus we prefer the currently-checked item. We only do this when
// nothing is selected yet so we don't override an existing selection.
cx.on_focus_in(&focus_handle, window, |this, window, cx| {
if this.selected_index.is_none() {
if this.selected_index.is_none() && !this.suppress_focus_selection {
this.select_toggled_or_first(window, cx);
}
this.suppress_focus_selection = false;
})
.detach();
@@ -333,6 +339,7 @@ impl ContextMenu {
submenu_trigger_bounds: Rc::new(Cell::new(None)),
submenu_trigger_mouse_down: false,
ignore_blur_until: None,
suppress_focus_selection: false,
},
window,
cx,
@@ -419,6 +426,7 @@ impl ContextMenu {
submenu_trigger_bounds: Rc::new(Cell::new(None)),
submenu_trigger_mouse_down: false,
ignore_blur_until: None,
suppress_focus_selection: false,
},
window,
cx,
@@ -488,6 +496,7 @@ impl ContextMenu {
submenu_trigger_bounds: Rc::new(Cell::new(None)),
submenu_trigger_mouse_down: false,
ignore_blur_until: None,
suppress_focus_selection: false,
},
window,
cx,
@@ -1324,6 +1333,7 @@ impl ContextMenu {
submenu_trigger_bounds: Rc::new(Cell::new(None)),
submenu_trigger_mouse_down: false,
ignore_blur_until: None,
suppress_focus_selection: false,
};
menu = (builder)(menu, window, cx);
@@ -1649,6 +1659,7 @@ impl ContextMenu {
if *hovered {
this.clear_selected();
this.suppress_focus_selection = true;
window.focus(&this.focus_handle.clone(), cx);
this.hover_target = HoverTarget::MainMenu;
this.submenu_safety_threshold_x = Some(mouse_pos.x - px(50.0));
@@ -1686,6 +1697,7 @@ impl ContextMenu {
{
this.close_submenu(false, cx);
this.clear_selected();
this.suppress_focus_selection = true;
window.focus(&this.focus_handle.clone(), cx);
cx.notify();
}
@@ -1973,6 +1985,7 @@ impl ContextMenu {
item.on_hover(cx.listener(move |this, hovered, window, cx| {
if *hovered {
this.clear_selected();
this.suppress_focus_selection = true;
window.focus(&this.focus_handle.clone(), cx);
if let SubmenuState::Open(open_submenu) = &this.submenu_state {