gpui_macos: Improve mouse tracking for normal windows (#64234)

To reproduce:

1. Place Safari over part of Zed, then focus Zed.
2. Right-click Safari to open its context menu, then left-click Safari
anywhere in it.
3. Move the pointer over Safari where it covers the Zed. Zed shows hover
effects underneath.

Use a view tracking area and disable `acceptsMouseMovedEvents` for
normal macOS windows. This addresses hover effects on covered content
when the pointer moves over another window.

[Chromium also requests mouse movement through tracking
areas](https://github.com/chromium/chromium/blob/5a43095e95ffe566d1512b9d67d534e3ae05b975/components/remote_cocoa/app_shim/bridged_content_view.mm#L436-L465).
A search of its macOS window/view code found no explicit use of
`acceptsMouseMovedEvents`, which [defaults to
`false`](https://developer.apple.com/documentation/appkit/nswindow/acceptsmousemovedevents).

Tracking stays enabled in unfocused windows while the app is active.

Before:


https://github.com/user-attachments/assets/f21cd3e5-04d5-4d34-b495-02f8d0f68ecf

After:


https://github.com/user-attachments/assets/89a8b9c9-5aa3-4075-aa91-13e200ea3613

Release Notes:

- [GPUI] Fixed mouse tracking on macOS so moving the pointer over
another app’s window no longer triggers hover effects in covered GPUI
windows.
- Fixed an issue on macOS where moving the pointer over another app
could trigger hover effects in a Zed window underneath it.

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
This commit is contained in:
Smit BarmaseandAnthony Eid authored and GitHub committed 2026-09-15 16:28:53 +00:00
1 parent a95da07db2
commit db7f9ceed7
2 files changed
+43 -24

No files matched your search

+1
View File
@@ -708,6 +708,7 @@ objc2-app-kit = { version = "0.3.2", default-features = false, features = [
"NSResponder",
"NSSavePanel",
"NSScreen",
"NSTrackingArea",
"NSView",
"NSWindow",
"NSWorkspace",
+42 -24
View File
@@ -56,8 +56,9 @@ use objc2::{
};
use objc2_app_kit::{
NSAlert, NSAlertStyle, NSBeep, NSButton as Objc2NSButton, NSDraggingImageComponent,
NSDraggingImageComponentIconKey, NSDraggingItem, NSPasteboardWriting, NSView as Objc2NSView,
NSWindow as Objc2NSWindow, NSWindowButton as Objc2NSWindowButton, NSWorkspace,
NSDraggingImageComponentIconKey, NSDraggingItem, NSPasteboardWriting, NSTrackingArea,
NSTrackingAreaOptions, NSView as Objc2NSView, NSWindow as Objc2NSWindow,
NSWindowButton as Objc2NSWindowButton, NSWorkspace,
};
use objc2_foundation::{NSPoint as Objc2NSPoint, NSRect as Objc2NSRect, NSURL};
use parking_lot::Mutex;
@@ -101,14 +102,6 @@ const NSFloatingWindowLevel: NSInteger = 3;
#[allow(non_upper_case_globals)]
const NSPopUpWindowLevel: NSInteger = 101;
#[allow(non_upper_case_globals)]
const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
#[allow(non_upper_case_globals)]
const NSTrackingMouseMoved: NSUInteger = 0x02;
#[allow(non_upper_case_globals)]
const NSTrackingActiveAlways: NSUInteger = 0x80;
#[allow(non_upper_case_globals)]
const NSTrackingInVisibleRect: NSUInteger = 0x200;
#[allow(non_upper_case_globals)]
const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4;
#[allow(non_upper_case_globals)]
const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
@@ -1198,15 +1191,29 @@ impl MacWindow {
let main_window: id = msg_send![app, mainWindow];
let mut sheet_parent = None;
// SAFETY: This is the live GPUIView (an NSView subclass) added to the
// content view above, and window creation runs on the main thread.
let tracking_view = &*native_view.cast::<Objc2NSView>();
match kind {
WindowKind::Normal | WindowKind::Floating => {
if kind == WindowKind::Floating {
// Let the window float keep above normal windows.
native_window.setLevel_(NSFloatingWindowLevel);
native_window.setAcceptsMouseMovedEvents_(YES);
} else {
native_window.setLevel_(NSNormalWindowLevel);
native_window.setAcceptsMouseMovedEvents_(NO);
add_mouse_tracking_area(
tracking_view,
NSTrackingAreaOptions::MouseEnteredAndExited
| NSTrackingAreaOptions::MouseMoved
// Track while this application is active, even if another
// window in the application has focus.
| NSTrackingAreaOptions::ActiveInActiveApp
| NSTrackingAreaOptions::InVisibleRect,
);
}
native_window.setAcceptsMouseMovedEvents_(YES);
if let Some(tabbing_identifier) = tabbing_identifier {
let tabbing_id = ns_string(tabbing_identifier.as_str());
@@ -1218,19 +1225,15 @@ impl MacWindow {
// `AnchoredPopup` is rejected in `MacPlatform::open_window`, grouped here only
// for exhaustiveness.
WindowKind::PopUp | WindowKind::AnchoredPopup(_) => {
// Use a tracking area to allow receiving MouseMoved events even when
// the window or application aren't active, which is often the case
// e.g. for notification windows.
let tracking_area: id = msg_send![class!(NSTrackingArea), alloc];
let _: () = msg_send![
tracking_area,
initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.))
options: NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect
owner: native_view
userInfo: nil
];
let _: () =
msg_send![native_view, addTrackingArea: tracking_area.autorelease()];
add_mouse_tracking_area(
tracking_view,
NSTrackingAreaOptions::MouseEnteredAndExited
| NSTrackingAreaOptions::MouseMoved
// Track even when another application is active, e.g. so
// notification windows can respond to hover.
| NSTrackingAreaOptions::ActiveAlways
| NSTrackingAreaOptions::InVisibleRect,
);
native_window.setLevel_(NSPopUpWindowLevel);
let _: () = msg_send![
@@ -2480,6 +2483,21 @@ extern "C" fn dealloc_view(this: &Object, _: Sel) {
}
}
fn add_mouse_tracking_area(native_view: &Objc2NSView, options: NSTrackingAreaOptions) {
// SAFETY: NSView provides the tracking-event callbacks, and the owner is
// the same view that retains the tracking area. No user info is supplied.
let tracking_area = unsafe {
NSTrackingArea::initWithRect_options_owner_userInfo(
NSTrackingArea::alloc(),
Objc2NSRect::ZERO,
options,
Some(native_view),
None,
)
};
native_view.addTrackingArea(&tracking_area);
}
extern "C" fn reset_cursor_rects(this: &Object, _: Sel) {
// SAFETY: AppKit invokes cursor-rect updates on the main thread for GPUIView instances,
// whose WINDOW_STATE_IVAR is initialized when the view is created. The cursor registered