mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
gpui: Fix hover state not clearing when mouse leaves window (#60275)
# Objective Fix two gaps in element hover tracking at window boundaries. Hover was only re-evaluated on `MouseMove`, so when the pointer left the window no event fired `on_hover(false)` and the element stayed hovered. Symmetrically on Wayland, no `Motion` follows `Enter` until the pointer moves again, so hover was not established at the entry pixel. Both cases are easy to miss since most hover-styled elements don't sit flush against the window edge, but they surfaced while implementing layer_shell popups with input_regions, which should close when stop hovering. ## Solution The hover compare-and-fire logic in `div` is refactored into a shared `update_hover` closure, and a second listener on `MouseExitEvent` clears hover when the pointer leaves the window. It clears unconditionally because `MouseExited` doesn't update the tracked mouse position, so a hit test during that dispatch would still report the element as hovered. On Wayland, a `MouseMove` is synthesized at the entry position on `wl_pointer.enter`, mirroring the `MouseExited` already dispatched on `Leave`. ## Testing Tested manually on Wayland/Linux: hover on a window-edge element clears when the pointer leaves the window, and hover is established immediately when the pointer enters a surface with an element under the entry pixel. Not tested on other platforms. The `div` change relies on each platform's existing `MouseExited` dispatch: macOS and X11 emit it, so they get the exit fix too. Windows never dispatches `MouseExited` (`WM_MOUSELEAVE` only flips the window-level hover flag), so the stuck-hover case might remain there, unchanged from before. Before: https://github.com/user-attachments/assets/6af83bb3-de9d-40e2-a64c-bdefc98fc96d After: https://github.com/user-attachments/assets/721a9b5c-108a-499f-867e-da111836a34a Here is an example application to test this: ```rs #![cfg_attr(target_family = "wasm", no_main)] use gpui::{ App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size, }; use gpui_platform::application; struct HoverExit { hovered: bool, } impl Render for HoverExit { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { // Fills the whole window so its edge is the window edge: moving the mouse // out of the window is what exercises the MouseExited path. div() .id("hover-exit") .size_full() .flex() .justify_center() .items_center() .text_xl() .text_color(rgb(0xffffff)) .bg(if self.hovered { rgb(0x585f58) } else { rgb(0x505050) }) .child(if self.hovered { "HOVERED" } else { "not hovered" }) .on_hover(cx.listener(|this, hovered, _, cx| { this.hovered = *hovered; cx.notify(); })) } } fn run_example() { application().run(|cx: &mut App| { let bounds = Bounds::centered(None, size(px(240.), px(160.0)), cx); cx.open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), app_id: Some("gpui-hover-exit".to_string()), ..Default::default() }, |_, cx| cx.new(|_| HoverExit { hovered: false }), ) .unwrap(); cx.activate(true); }); } #[cfg(not(target_family = "wasm"))] fn main() { run_example(); } #[cfg(target_family = "wasm")] #[wasm_bindgen::prelude::wasm_bindgen(start)] pub fn start() { gpui_platform::web_init(); run_example(); } ``` ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Fixed element hover state not clearing when the mouse leaves the window
This commit is contained in:
parent
23c0080d1d
commit
664b7ecb40
2 changed files with 33 additions and 12 deletions
|
|
@ -2860,7 +2860,6 @@ impl Interactivity {
|
|||
}
|
||||
|
||||
if let Some(hover_listener) = self.hover_listener.take() {
|
||||
let hitbox = hitbox.clone();
|
||||
let was_hovered = element_state
|
||||
.hover_listener_state
|
||||
.get_or_insert_with(Default::default)
|
||||
|
|
@ -2869,22 +2868,35 @@ impl Interactivity {
|
|||
.pending_mouse_down
|
||||
.get_or_insert_with(Default::default)
|
||||
.clone();
|
||||
|
||||
window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
let is_hovered = has_mouse_down.borrow().is_none()
|
||||
&& !cx.has_active_drag()
|
||||
&& hitbox.is_hovered(window);
|
||||
let hover_listener = Rc::new(hover_listener);
|
||||
let update_hover = move |is_hovered: bool, window: &mut Window, cx: &mut App| {
|
||||
let mut was_hovered = was_hovered.borrow_mut();
|
||||
|
||||
if is_hovered != *was_hovered {
|
||||
*was_hovered = is_hovered;
|
||||
drop(was_hovered);
|
||||
|
||||
hover_listener(&is_hovered, window, cx);
|
||||
}
|
||||
};
|
||||
|
||||
window.on_mouse_event({
|
||||
let update_hover = update_hover.clone();
|
||||
let hitbox = hitbox.clone();
|
||||
move |_: &MouseMoveEvent, phase, window, cx| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
let is_hovered = has_mouse_down.borrow().is_none()
|
||||
&& !cx.has_active_drag()
|
||||
&& hitbox.is_hovered(window);
|
||||
update_hover(is_hovered, window, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// The pointer can leave the window without a final MouseMove, so also
|
||||
// clear hover on MouseExited.
|
||||
window.on_mouse_event(move |_: &MouseExitEvent, phase, window, cx| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
update_hover(false, window, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1884,8 +1884,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
|||
surface_y,
|
||||
..
|
||||
} => {
|
||||
let position = point(px(surface_x as f32), px(surface_y as f32));
|
||||
state.serial_tracker.update(SerialKind::MouseEnter, serial);
|
||||
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
|
||||
state.mouse_location = Some(position);
|
||||
state.button_pressed = None;
|
||||
|
||||
if let Some(window) = get_window(&mut state, &surface.id()) {
|
||||
|
|
@ -1908,8 +1909,16 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
|||
);
|
||||
}
|
||||
}
|
||||
let modifiers = state.modifiers;
|
||||
drop(state);
|
||||
window.set_hovered(true);
|
||||
// No Motion follows Enter unless the pointer keeps moving, so synthesize
|
||||
// a MouseMove to establish hover at the entry position.
|
||||
window.handle_input(PlatformInput::MouseMove(MouseMoveEvent {
|
||||
position,
|
||||
pressed_button: None,
|
||||
modifiers,
|
||||
}));
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::Leave { .. } => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue