#[cfg(not(target_os = "macos"))]
compile_error!("the AppKit alert arm targets macOS only");
use std::panic::{AssertUnwindSafe, catch_unwind};
use block2::RcBlock;
use objc2::rc::Retained;
use objc2::runtime::{NSObject, NSObjectProtocol};
use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, sel};
use objc2_app_kit::{
NSAlert, NSAlertFirstButtonReturn, NSModalResponse, NSWindow, NSWindowWillCloseNotification,
};
use objc2_foundation::{NSNotification, NSNotificationCenter, NSString};
use super::apple_host::{
AfterDismiss, LivePresentation, dismiss_live, install_live, on_main, presenting_anchor,
take_live_any, take_live_as,
};
use super::{ActionRole, AlertHost, AlertOutcome, AlertSpec, PresentError, Sender};
type CompletionHandler = RcBlock<dyn Fn(NSModalResponse)>;
pub(crate) struct Host;
impl AlertHost for Host {
fn show_alert(
spec: AlertSpec,
tx: Sender<AlertOutcome>,
generation: u64,
) -> Result<(), PresentError> {
on_main(move |mtm| start(mtm, spec, tx, generation));
Ok(())
}
fn dismiss(generation: u64) {
dismiss_live(generation);
}
}
fn start(mtm: MainThreadMarker, spec: AlertSpec, tx: Sender<AlertOutcome>, generation: u64) {
if let Some(displaced) = take_live_any(mtm) {
displaced.dismiss(mtm, None);
}
let Some(window) = presenting_anchor(mtm) else {
tx.send(Err(PresentError::NoHost));
return;
};
let live = LiveAlert::build(mtm, &spec, tx, generation, window);
if let Some(unexpected) = install_live(mtm, generation, Box::new(live)) {
unexpected.dismiss(mtm, None);
}
present_live(mtm, generation);
}
fn present_live(mtm: MainThreadMarker, generation: u64) {
let Some(live) = take_live_as::<LiveAlert>(mtm, generation) else {
return;
};
live.alert
.beginSheetModalForWindow_completionHandler(&live.window, Some(&live.completion));
if let Some(unexpected) = install_live(mtm, generation, live) {
unexpected.dismiss(mtm, None);
}
}
fn guarded(which: &str, body: impl FnOnce()) {
if catch_unwind(AssertUnwindSafe(body)).is_err() {
log::error!("frust-native-widgets: the alert's {which} callback panicked");
}
}
fn resolve(generation: u64, outcome: Result<AlertOutcome, PresentError>) {
let Some(mtm) = MainThreadMarker::new() else {
log::error!("frust-native-widgets: an alert callback arrived off the main thread");
return;
};
if let Some(live) = take_live_as::<LiveAlert>(mtm, generation) {
live.finish(outcome);
}
}
fn map_response(response: NSModalResponse, action_ids: &[String]) -> AlertOutcome {
let offset = response - NSAlertFirstButtonReturn;
usize::try_from(offset)
.ok()
.and_then(|index| action_ids.get(index))
.map_or(AlertOutcome::Dismissed, |id| {
AlertOutcome::Action(id.clone())
})
}
struct LiveAlert {
tx: Sender<AlertOutcome>,
alert: Retained<NSAlert>,
window: Retained<NSWindow>,
observer: Retained<FrustNativeAlertObserver>,
completion: CompletionHandler,
}
impl LiveAlert {
fn build(
mtm: MainThreadMarker,
spec: &AlertSpec,
tx: Sender<AlertOutcome>,
generation: u64,
window: Retained<NSWindow>,
) -> Self {
let alert = NSAlert::new(mtm);
alert.setMessageText(&NSString::from_str(&spec.title));
alert.setInformativeText(&NSString::from_str(&spec.message));
let mut action_ids = Vec::with_capacity(spec.actions.len());
for action in &spec.actions {
let button = alert.addButtonWithTitle(&NSString::from_str(&action.label));
match action.role {
ActionRole::Default => {}
ActionRole::Cancel => button.setKeyEquivalent(&NSString::from_str("\u{1b}")),
ActionRole::Destructive => button.setHasDestructiveAction(true),
}
action_ids.push(action.id.clone());
}
let completion: CompletionHandler = RcBlock::new(move |response: NSModalResponse| {
guarded("sheet completion", || {
resolve(generation, Ok(map_response(response, &action_ids)));
});
});
let observer = FrustNativeAlertObserver::new(mtm, generation);
observer.observe_close(&window);
Self {
tx,
alert,
window,
observer,
completion,
}
}
fn finish(self, outcome: Result<AlertOutcome, PresentError>) {
let Self {
tx,
alert,
observer,
..
} = self;
observer.stop_observing();
tx.send(outcome);
let _ = Retained::autorelease_ptr(alert);
let _ = Retained::autorelease_ptr(observer);
}
}
impl LivePresentation for LiveAlert {
fn dismiss(self: Box<Self>, mtm: MainThreadMarker, then: Option<AfterDismiss>) {
let window = Retained::clone(&self.window);
let sheet = self.alert.window();
self.finish(Ok(AlertOutcome::Dismissed));
window.endSheet(&sheet);
if let Some(then) = then {
then(mtm);
}
}
}
struct ObserverIvars {
generation: u64,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[ivars = ObserverIvars]
struct FrustNativeAlertObserver;
unsafe impl NSObjectProtocol for FrustNativeAlertObserver {}
impl FrustNativeAlertObserver {
#[unsafe(method(windowWillClose:))]
fn window_will_close(&self, _notification: &NSNotification) {
let generation = self.ivars().generation;
guarded("window close", || {
resolve(generation, Ok(AlertOutcome::HostLost));
});
}
}
);
impl FrustNativeAlertObserver {
fn new(mtm: MainThreadMarker, generation: u64) -> Retained<Self> {
let this = Self::alloc(mtm).set_ivars(ObserverIvars { generation });
unsafe { msg_send![super(this), init] }
}
fn observe_close(&self, window: &NSWindow) {
let center = NSNotificationCenter::defaultCenter();
let name = unsafe { NSWindowWillCloseNotification };
unsafe {
center.addObserver_selector_name_object(
self,
sel!(windowWillClose:),
Some(name),
Some(window),
);
}
}
fn stop_observing(&self) {
unsafe { NSNotificationCenter::defaultCenter().removeObserver(self) };
}
}