### 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>
@@ -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;