# 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
window2 files changedLine totals unavailable