diff --git a/Cargo.lock b/Cargo.lock index 22e0de3845..638abc4b9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11791,6 +11791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.13.1", + "block2 0.6.2", "objc2 0.6.3", "objc2-core-foundation", "objc2-foundation 0.3.2", diff --git a/Cargo.toml b/Cargo.toml index 558afe022b..65e6f16477 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -693,12 +693,15 @@ num-format = "0.4.4" objc = "0.2" objc2 = "0.6" objc2-app-kit = { version = "0.3.2", default-features = false, features = [ + "NSAlert", + "NSApplication", "NSButton", "NSControl", "NSGraphics", "NSResponder", "NSView", "NSWindow", + "block2", "objc2-core-foundation", ] } objc2-foundation = { version = "=0.3.2", default-features = false, features = [ diff --git a/crates/gpui_macos/src/platform.rs b/crates/gpui_macos/src/platform.rs index c1d857c78a..c6f75a85bb 100644 --- a/crates/gpui_macos/src/platform.rs +++ b/crates/gpui_macos/src/platform.rs @@ -44,6 +44,7 @@ use objc::{ runtime::{Class, Object, Sel}, sel, sel_impl, }; +use objc2::MainThreadMarker; use parking_lot::Mutex; use ptr::null_mut; use semver::Version; @@ -163,7 +164,7 @@ unsafe fn build_classes() { } } -pub struct MacPlatform(Mutex); +pub struct MacPlatform(Mutex, MainThreadMarker); pub(crate) struct MacPlatformState { background_executor: BackgroundExecutor, @@ -195,6 +196,7 @@ pub(crate) struct MacPlatformState { impl MacPlatform { pub fn new(headless: bool) -> Self { + let marker = MainThreadMarker::new().expect("Mac platform not created on main thread"); let dispatcher = Arc::new(MacDispatcher::new()); #[cfg(feature = "font-kit")] @@ -213,7 +215,7 @@ impl MacPlatform { let keyboard_layout = MacKeyboardLayout::new(); let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); - Self(Mutex::new(MacPlatformState { + let state = Mutex::new(MacPlatformState { headless, text_system, background_executor: BackgroundExecutor::new(dispatcher.clone()), @@ -238,7 +240,8 @@ impl MacPlatform { keyboard_mapper, cursor_visible: Arc::new(AtomicBool::new(true)), system_notifications: crate::system_notifications::SystemNotificationState::new(), - })) + }); + Self(state, marker) } unsafe fn create_menu_bar( @@ -674,6 +677,7 @@ impl Platform for MacPlatform { foreground_executor, background_executor, renderer_context, + self.1, ))) } diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index faf27e036b..88a694c971 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -7,6 +7,7 @@ use crate::{ #[cfg(any(test, feature = "test-support"))] use anyhow::Result; use block::ConcreteBlock; +use block2::RcBlock; use cocoa::{ appkit::{ NSApplication, NSBackingStoreBuffered, NSColor, NSEvent, NSEventModifierFlags, NSEventType, @@ -49,10 +50,10 @@ use objc::{ runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES}, sel, sel_impl, }; -use objc2::{rc::Retained, runtime::AnyObject as Objc2Object}; +use objc2::{MainThreadMarker, rc::Retained, runtime::AnyObject as Objc2Object}; use objc2_app_kit::{ - NSBeep, NSButton as Objc2NSButton, NSView as Objc2NSView, NSWindow as Objc2NSWindow, - NSWindowButton as Objc2NSWindowButton, + NSAlert, NSAlertStyle, NSBeep, NSButton as Objc2NSButton, NSView as Objc2NSView, + NSWindow as Objc2NSWindow, NSWindowButton as Objc2NSWindowButton, }; use objc2_foundation::{NSPoint as Objc2NSPoint, NSRect as Objc2NSRect}; use parking_lot::Mutex; @@ -878,7 +879,7 @@ impl MacWindowState { unsafe impl Send for MacWindowState {} -pub(crate) struct MacWindow(Arc>); +pub(crate) struct MacWindow(Arc>, MainThreadMarker); impl MacWindow { pub fn open( @@ -902,6 +903,7 @@ impl MacWindow { foreground_executor: ForegroundExecutor, background_executor: BackgroundExecutor, renderer_context: renderer::Context, + marker: MainThreadMarker, ) -> Self { unsafe { let pool = NSAutoreleasePool::new(nil); @@ -1011,7 +1013,7 @@ impl MacWindow { let native_view = NSView::initWithFrame_(native_view, NSView::bounds(content_view)); assert!(!native_view.is_null()); - let mut window = Self(Arc::new(Mutex::new(MacWindowState { + let state = Arc::new(Mutex::new(MacWindowState { handle, foreground_executor, background_executor, @@ -1065,7 +1067,8 @@ impl MacWindow { closed: Arc::new(AtomicBool::new(false)), accesskit_adapter: None, sheet_parent: None, - }))); + })); + let mut window = Self(state, marker); (*native_window).set_ivar( WINDOW_STATE_IVAR, @@ -1511,6 +1514,8 @@ impl PlatformWindow for MacWindow { detail: Option<&str>, answers: &[PromptButton], ) -> Option> { + use objc2_foundation::{NSInteger, NSString}; + // NSAlert's first button keeps Return and Cancel keeps Escape, but the keyboard // focus (and therefore Space) defaults to Cancel, leaving the middle button of // prompts like "Save / Don't Save / Cancel" unreachable from the keyboard. Move @@ -1523,69 +1528,67 @@ impl PlatformWindow for MacWindow { .map(|(ix, _)| ix) .filter(|&ix| ix > 0); - unsafe { - let alert: id = msg_send![class!(NSAlert), alloc]; - let alert: id = msg_send![alert, init]; - let alert_style = match level { - PromptLevel::Info => 1, - PromptLevel::Warning => 0, - PromptLevel::Critical => 2, - }; - let _: () = msg_send![alert, setAlertStyle: alert_style]; - let _: () = msg_send![alert, setMessageText: ns_string(msg)]; - if let Some(detail) = detail { - let _: () = msg_send![alert, setInformativeText: ns_string(detail)]; - } + let alert = NSAlert::new(self.1); + alert.setAlertStyle(match level { + PromptLevel::Critical => NSAlertStyle::Critical, + PromptLevel::Warning => NSAlertStyle::Warning, + PromptLevel::Info => NSAlertStyle::Informational, + }); + let message = NSString::from_str(msg); + alert.setMessageText(message.as_ref()); - let mut initial_focus_button: Option = None; - for (ix, answer) in answers.iter().enumerate() { - let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())]; - let _: () = msg_send![button, setTag: ix as NSInteger]; - - if answer.is_cancel() { - if let Some(key) = std::char::from_u32(crate::events::ESCAPE_KEY as u32) { - let _: () = - msg_send![button, setKeyEquivalent: ns_string(&key.to_string())]; - } - } else if Some(ix) == initial_focus_ix { - initial_focus_button = Some(button); - } - } - - if let Some(button) = initial_focus_button { - let alert_window: id = msg_send![alert, window]; - let _: () = msg_send![alert_window, setInitialFirstResponder: button]; - } - - let (done_tx, done_rx) = oneshot::channel(); - let done_tx = Cell::new(Some(done_tx)); - let block = ConcreteBlock::new(move |answer: NSInteger| { - let _: () = msg_send![alert, release]; - if let Some(done_tx) = done_tx.take() { - let _ = done_tx.send(answer.try_into().unwrap()); - } - }); - let block = block.copy(); - let lock = self.0.lock(); - let native_window = lock.native_window; - let closed = lock.closed.clone(); - let executor = lock.foreground_executor.clone(); - executor - .spawn(async move { - if !closed.load(Ordering::Acquire) { - let _: () = msg_send![ - alert, - beginSheetModalForWindow: native_window - completionHandler: block - ]; - } else { - let _: () = msg_send![alert, release]; - } - }) - .detach(); - - Some(done_rx) + if let Some(detail) = detail { + let detail_text = NSString::from_str(detail); + alert.setInformativeText(detail_text.as_ref()); } + + let mut initial_focus_button: Option> = None; + for (ix, answer) in answers.iter().enumerate() { + let title = NSString::from_str(answer.label()); + let button = alert.addButtonWithTitle(&title); + button.setTag(ix as NSInteger); + + if answer.is_cancel() { + if let Some(key) = core::char::from_u32(crate::events::ESCAPE_KEY as u32) { + let key = NSString::from_str(&key.to_string()); + button.setKeyEquivalent(&key); + } + } else if Some(ix) == initial_focus_ix { + initial_focus_button = Some(button); + } + } + + if let Some(button) = initial_focus_button { + alert.window().setInitialFirstResponder(Some(&button)); + } + + let (done_tx, done_rx) = oneshot::channel(); + let done_tx = Cell::new(Some(done_tx)); + + let block = RcBlock::new(move |answer: NSInteger| { + if let Some(done_tx) = done_tx.take() { + let _ = done_tx.send(answer.try_into().unwrap()); + } + }); + + let lock = self.0.lock(); + let native_window = lock.native_window; + let closed = lock.closed.clone(); + let executor = lock.foreground_executor.clone(); + executor + .spawn(async move { + if !closed.load(Ordering::Acquire) { + // SAFETY: `native_window` is an Objective-C `NSWindow` pointer + // owned by the platform window; bridge it into objc2. + let sheet_window: &Objc2NSWindow = + unsafe { &*(native_window as *const Objc2NSWindow) }; + + alert.beginSheetModalForWindow_completionHandler(sheet_window, Some(&block)); + } + }) + .detach(); + + Some(done_rx) } fn activate(&self) {