picker: Fix mouse overrides selected item highlight in searchable dropdown (#50827)

Om Chillure and Nathan Sobo created

### Fixes #31865

### Problem
When navigating a picker (command palette, file finder, etc.) with the
keyboard and then hovering with the mouse, two items would appear
highlighted at the same time - one from the keyboard selection
ghost_element_selected background and one from the CSS style
ghost_element_hover background. This happened because GPUI's CSS hover
is applied at paint time independently from the Rust selection state.

### Solution
Added a keyboard_navigated: bool flag to [Picker] to track whether the
last navigation was via keyboard or mouse.

When a keyboard navigation action fires (↑, ↓, Home, End, etc.),
`keyboard_navigated` is set to true. A transparent
`block_mouse_except_scroll` overlay div is added on top of each list
item, preventing the CSS style from firing on `ListItem` children.

When the mouse moves over any item, the overlay's `on_mouse_move`
handler fires, clears keyboard_navigated, moves the selection to that
item, and re-renders - removing all overlays and restoring normal mouse
hover behavior.
The `on_hover` handler also moves the selection to whichever item the
mouse enters, so mouse navigation is fully functional.
Clicks work through the overlay since `block_mouse_except_scroll` only
affects hover hit-testing, not click events on the parent div.

### Behavior
Matches VS Code / IntelliJ — keyboard navigation suppresses hover
highlights until the mouse moves, at which point normal hover resumes.
Only one item is ever highlighted at a time.

### Video

[Screencast from 2026-03-05
19-20-16.webm](https://github.com/user-attachments/assets/7a8a543a-95f3-481f-b59d-171604e5f883)

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>

Change summary

crates/gpui/src/elements/div.rs |  3 ++-
crates/gpui/src/window.rs       | 30 ++++++++++++++++++++----------
crates/picker/src/picker.rs     |  6 ++++++
3 files changed, 28 insertions(+), 11 deletions(-)

Detailed changes

crates/gpui/src/elements/div.rs 🔗

@@ -2589,7 +2589,8 @@ impl Interactivity {
                     let pending_mouse_down = pending_mouse_down.clone();
                     let source_bounds = hitbox.bounds;
                     move |window: &Window| {
-                        pending_mouse_down.borrow().is_none()
+                        !window.last_input_was_keyboard()
+                            && pending_mouse_down.borrow().is_none()
                             && source_bounds.contains(&window.mouse_position())
                     }
                 });

crates/gpui/src/window.rs 🔗

@@ -560,7 +560,8 @@ pub enum WindowControlArea {
 pub struct HitboxId(u64);
 
 impl HitboxId {
-    /// Checks if the hitbox with this ID is currently hovered. Except when handling
+    /// Checks if the hitbox with this ID is currently hovered. Returns `false` during keyboard
+    /// input modality so that keyboard navigation suppresses hover highlights. Except when handling
     /// `ScrollWheelEvent`, this is typically what you want when determining whether to handle mouse
     /// events or paint hover styles.
     ///
@@ -570,6 +571,9 @@ impl HitboxId {
         if window.captured_hitbox == Some(self) {
             return true;
         }
+        if window.last_input_was_keyboard() {
+            return false;
+        }
         let hit_test = &window.mouse_hit_test;
         for id in hit_test.ids.iter().take(hit_test.hover_hitbox_count) {
             if self == *id {
@@ -608,13 +612,15 @@ pub struct Hitbox {
 }
 
 impl Hitbox {
-    /// Checks if the hitbox is currently hovered. Except when handling `ScrollWheelEvent`, this is
-    /// typically what you want when determining whether to handle mouse events or paint hover
-    /// styles.
+    /// Checks if the hitbox is currently hovered. Returns `false` during keyboard input modality
+    /// so that keyboard navigation suppresses hover highlights. Except when handling
+    /// `ScrollWheelEvent`, this is typically what you want when determining whether to handle mouse
+    /// events or paint hover styles.
     ///
     /// This can return `false` even when the hitbox contains the mouse, if a hitbox in front of
     /// this sets `HitboxBehavior::BlockMouse` (`InteractiveElement::occlude`) or
-    /// `HitboxBehavior::BlockMouseExceptScroll` (`InteractiveElement::block_mouse_except_scroll`).
+    /// `HitboxBehavior::BlockMouseExceptScroll` (`InteractiveElement::block_mouse_except_scroll`),
+    /// or if the current input modality is keyboard (see [`Window::last_input_was_keyboard`]).
     ///
     /// Handling of `ScrollWheelEvent` should typically use `should_handle_scroll` instead.
     /// Concretely, this is due to use-cases like overlays that cause the elements under to be
@@ -4028,14 +4034,18 @@ impl Window {
     /// Dispatch a mouse or keyboard event on the window.
     #[profiling::function]
     pub fn dispatch_event(&mut self, event: PlatformInput, cx: &mut App) -> DispatchEventResult {
-        // Track whether this input was keyboard-based for focus-visible styling
+        // Track input modality for focus-visible styling and hover suppression.
+        // Hover is suppressed during keyboard modality so that keyboard navigation
+        // doesn't show hover highlights on the item under the mouse cursor.
+        let old_modality = self.last_input_modality;
         self.last_input_modality = match &event {
-            PlatformInput::KeyDown(_) | PlatformInput::ModifiersChanged(_) => {
-                InputModality::Keyboard
-            }
-            PlatformInput::MouseDown(e) if e.is_focusing() => InputModality::Mouse,
+            PlatformInput::KeyDown(_) => InputModality::Keyboard,
+            PlatformInput::MouseMove(_) | PlatformInput::MouseDown(_) => InputModality::Mouse,
             _ => self.last_input_modality,
         };
+        if self.last_input_modality != old_modality {
+            self.refresh();
+        }
 
         // Handlers may set this to false by calling `stop_propagation`.
         cx.propagate_event = true;

crates/picker/src/picker.rs 🔗

@@ -788,6 +788,12 @@ impl<D: PickerDelegate> Picker<D> {
                     this.handle_click(ix, event.modifiers.platform, window, cx)
                 }),
             )
+            .on_hover(cx.listener(move |this, hovered: &bool, window, cx| {
+                if *hovered {
+                    this.set_selected_index(ix, None, false, window, cx);
+                    cx.notify();
+                }
+            }))
             .children(self.delegate.render_match(
                 ix,
                 ix == self.delegate.selected_index(),