gpui_macos: Migrate PlatformWindow::prompt to objc2 on MacOS (#59572)

# Objective

Rust 1.96 emits a warning when compiling the `block` crate, that it will
no longer compile past some future version of Rust. The `block` crate
provides `ConcreteBlock` and `RcBlock`, which are stack- and heap-based
functions often passed as callbacks to MacOS. The old `objc` crate
family is deprecated and no longer maintained.

The goal of the PR is to replace one call to `block::ConcreteBlock` by
migrating a standalone portion of code to the objc2 crate family. A full
migration to `objc2` by Zed has been long on the horizon (#22408)

Additional benefits: The new code is completely typed, uses automatic
memory management, and reduces lines of unsafe Rust from 50+ to 1. The
lingering unsafe converts a raw pointer stored by `self` to the
respective `objc2` type, since the rest of the window implementation
does not use the new crates yet.

## Solution

Migrate `PlatformWindow::prompt`'s MacOS implementation to `objc2`.

## Testing

Direct platform code is quite fragile and hard to test, however you can
exercise the codepath by running the following `gpui` example and
clicking on the prompt buttons.

```sh
cargo run -p gpui --example window
```

I've also built Zed with these changes and tried codepaths which
activate prompts, such as deleting a file.

## 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:

- N/A or Added/Fixed/Improved ...
This commit is contained in:
tidely authored and GitHub committed 2026-08-31 14:31:41 +00:00
1 parent 1344a8a846
commit fd35a813e9
4 files changed
+81 -70

No files matched your search

Generated
+1
View File
@@ -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",
+3
View File
@@ -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 = [
+7 -3
View File
@@ -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<MacPlatformState>);
pub struct MacPlatform(Mutex<MacPlatformState>, 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,
)))
}
+70 -67
View File
@@ -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<Mutex<MacWindowState>>);
pub(crate) struct MacWindow(Arc<Mutex<MacWindowState>>, 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<oneshot::Receiver<usize>> {
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<id> = 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<Retained<Objc2NSButton>> = 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) {