From 1eba1ca72e2c5fc50ae54b034edbc799ff222dd8 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 30 Apr 2026 16:19:27 +0200 Subject: [PATCH] Fix cursor style changes across windows (#55323) We were storing a cursor hidden boolean in the window state, but that state is actually global to the application. 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 is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --- crates/gpui_macos/src/platform.rs | 23 ++++++++--- crates/gpui_macos/src/window.rs | 64 +++++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/crates/gpui_macos/src/platform.rs b/crates/gpui_macos/src/platform.rs index fa37597065f..fefe957bb25 100644 --- a/crates/gpui_macos/src/platform.rs +++ b/crates/gpui_macos/src/platform.rs @@ -52,7 +52,7 @@ use std::{ ptr, rc::Rc, slice, str, - sync::{Arc, OnceLock}, + sync::{Arc, OnceLock, atomic::AtomicBool}, }; use util::{ ResultExt, @@ -179,6 +179,7 @@ pub(crate) struct MacPlatformState { dock_menu: Option, menus: Option>, keyboard_mapper: Rc, + cursor_hidden: Arc, } impl MacPlatform { @@ -215,6 +216,7 @@ impl MacPlatform { on_thermal_state_change: None, menus: None, keyboard_mapper, + cursor_hidden: Arc::new(AtomicBool::new(false)), })) } @@ -619,12 +621,22 @@ impl Platform for MacPlatform { handle: AnyWindowHandle, options: WindowParams, ) -> Result> { - let renderer_context = self.0.lock().renderer_context.clone(); + let (cursor_hidden, foreground_executor, background_executor, renderer_context) = { + let guard = self.0.lock(); + ( + guard.cursor_hidden.clone(), + guard.foreground_executor.clone(), + guard.background_executor.clone(), + guard.renderer_context.clone(), + ) + }; + Ok(Box::new(MacWindow::open( handle, options, - self.foreground_executor(), - self.background_executor(), + cursor_hidden, + foreground_executor, + background_executor, renderer_context, ))) } @@ -979,8 +991,9 @@ impl Platform for MacPlatform { /// Match cursor style to one of the styles available /// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor). fn set_cursor_style(&self, style: CursorStyle) { + let cursor_hidden = self.0.lock().cursor_hidden.clone(); unsafe { - set_active_window_cursor_style(style); + set_active_window_cursor_style(style, &cursor_hidden); } } diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index c4c47c4f542..2ab4b52d8d8 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -324,17 +324,34 @@ pub(crate) fn convert_mouse_position(position: NSPoint, window_height: Pixels) - /// This function is not thread safe. Callers must ensure this is called on the AppKit main /// thread because it reads the active AppKit window and updates GPUI window state associated /// with Objective-C objects. -pub(crate) unsafe fn set_active_window_cursor_style(style: CursorStyle) { +pub(crate) unsafe fn set_active_window_cursor_style( + style: CursorStyle, + cursor_hidden: &AtomicBool, +) { // SAFETY: The caller guarantees AppKit main-thread access. The class check ensures the // window has our WINDOW_STATE_IVAR before reading it. unsafe { let app = NSApplication::sharedApplication(nil); + let key_window: id = msg_send![app, keyWindow]; let main_window: id = msg_send![app, mainWindow]; - if main_window.is_null() || !msg_send![main_window, isKindOfClass: WINDOW_CLASS] { - return; - } + let active_window = if !key_window.is_null() + && msg_send![key_window, isKindOfClass: WINDOW_CLASS] + { + Some(key_window) + } else if !main_window.is_null() && msg_send![main_window, isKindOfClass: WINDOW_CLASS] { + Some(main_window) + } else { + None + }; - let window_state = get_window_state(&*main_window); + let Some(active_window) = active_window else { + if !matches!(style, CursorStyle::None) { + unhide_cursor(cursor_hidden); + } + return; + }; + + let window_state = get_window_state(&*active_window); let mut window_state = window_state.lock(); if window_state.cursor_style != style { window_state.cursor_style = style; @@ -346,6 +363,22 @@ pub(crate) unsafe fn set_active_window_cursor_style(style: CursorStyle) { } } +/// Unhides the cursor if this GPUI platform instance has hidden it. +/// +/// # Safety +/// +/// Must be called on the AppKit main thread. +unsafe fn unhide_cursor(cursor_hidden: &AtomicBool) { + unsafe { + if cursor_hidden + .compare_exchange(true, false, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + let _: () = msg_send![class!(NSCursor), unhide]; + } + } +} + unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const Class { unsafe { let mut decl = ClassDecl::new(name, superclass).unwrap(); @@ -463,7 +496,7 @@ struct MacWindowState { blurred_view: Option, background_appearance: WindowBackgroundAppearance, cursor_style: CursorStyle, - cursor_hidden: bool, + cursor_hidden: Arc, display_link: Option, renderer: renderer::Renderer, request_frame_callback: Option>, @@ -665,6 +698,7 @@ impl MacWindow { tabbing_identifier, .. }: WindowParams, + cursor_hidden: Arc, foreground_executor: ForegroundExecutor, background_executor: BackgroundExecutor, renderer_context: renderer::Context, @@ -782,7 +816,7 @@ impl MacWindow { blurred_view: None, background_appearance: WindowBackgroundAppearance::Opaque, cursor_style: CursorStyle::Arrow, - cursor_hidden: false, + cursor_hidden, display_link: None, renderer: renderer::new_renderer( renderer_context, @@ -1820,18 +1854,21 @@ extern "C" fn reset_cursor_rects(this: &Object, _: Sel) { let cursor_hidden; { - let mut window_state = window_state.lock(); + let window_state = window_state.lock(); if matches!(window_state.cursor_style, CursorStyle::None) { - if !window_state.cursor_hidden { + if window_state + .cursor_hidden + .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { let _: () = msg_send![class!(NSCursor), hide]; - window_state.cursor_hidden = true; } return; } cursor_style = window_state.cursor_style; - cursor_hidden = window_state.cursor_hidden; + cursor_hidden = window_state.cursor_hidden.clone(); }; let cursor: id = match cursor_style { @@ -1871,10 +1908,7 @@ extern "C" fn reset_cursor_rects(this: &Object, _: Sel) { CursorStyle::None => unreachable!(), }; - if cursor_hidden { - let _: () = msg_send![class!(NSCursor), unhide]; - window_state.lock().cursor_hidden = false; - } + unhide_cursor(&cursor_hidden); let bounds = NSView::bounds(this as *const Object as id); let _: () = msg_send![this, addCursorRect: bounds cursor: cursor];