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:
1 parent
1344a8a846
commit
fd35a813e9
4 files changed
+81
-70
No files matched your search
Generated
+1
@@ -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",
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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,
|
||||
)))
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user