use std::cell::{Cell, OnceCell};
use crate::{
DesktopEvent, DesktopShellConfig, EventRegistry, EventResponse, WindowId, WindowOptions,
WindowStateStore,
};
use objc2::rc::{autoreleasepool, Retained};
use objc2::{define_class, msg_send, DefinedClass, MainThreadMarker, MainThreadOnly};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate, NSStatusItem, NSWindow,
NSWindowDelegate,
};
use objc2_foundation::{NSNotification, NSObject, NSObjectProtocol, NSString};
use objc2_web_kit::WKWebView;
use super::geometry::{clamp_coordinate, clamp_dimension};
use super::host_message::DesktopHostMessageHandler;
use super::launch::{build_window_and_webview, persist_window_state_if_enabled};
use super::menu::NativeMenu;
use super::navigation::DesktopNavigationDelegate;
use super::scheme::DesktopSchemeHandler;
use super::theme::DesktopThemeObserver;
use super::window::DesktopWindow;
use super::{dispatch_event, MacosLaunchOptions};
pub(super) struct AppDelegateIvars {
pub(in crate::macos) executor: std::sync::Arc<crate::execution::ApplicationExecutor>,
pub(in crate::macos) runtime: std::sync::Arc<crate::DesktopRuntime>,
pub(in crate::macos) command_wake: OnceCell<super::commands::CommandWake>,
#[cfg(feature = "application-ipc")]
pub(in crate::macos) ipc: Option<std::rc::Rc<super::ipc::MacIpc>>,
pub(in crate::macos) window: OnceCell<Retained<DesktopWindow>>,
pub(in crate::macos) webview: OnceCell<Retained<WKWebView>>,
pub(in crate::macos) scheme_handler: OnceCell<Retained<DesktopSchemeHandler>>,
pub(in crate::macos) navigation_delegate: OnceCell<Retained<DesktopNavigationDelegate>>,
pub(in crate::macos) host_message_handler: OnceCell<Retained<DesktopHostMessageHandler>>,
pub(in crate::macos) theme_observer: OnceCell<Retained<DesktopThemeObserver>>,
pub(in crate::macos) tray_item: OnceCell<Retained<NSStatusItem>>,
pub(in crate::macos) main_menu: OnceCell<NativeMenu>,
pub(in crate::macos) title: Retained<NSString>,
pub(in crate::macos) options: WindowOptions,
pub(in crate::macos) shell: DesktopShellConfig,
pub(in crate::macos) events: EventRegistry,
pub(in crate::macos) window_handle: crate::WindowHandle,
pub(in crate::macos) state_store: Option<WindowStateStore>,
pub(in crate::macos) zoomed: Cell<bool>,
pub(in crate::macos) exiting: Cell<bool>,
}
define_class!(
#[unsafe(super = NSObject)]
#[thread_kind = MainThreadOnly]
#[ivars = AppDelegateIvars]
pub(super) struct DesktopAppDelegate;
unsafe impl NSObjectProtocol for DesktopAppDelegate {}
unsafe impl NSApplicationDelegate for DesktopAppDelegate {
#[unsafe(method(applicationDidFinishLaunching:))]
fn did_finish_launching(&self, notification: &NSNotification) {
autoreleasepool(|_| {
let Some(app_obj) = notification.object() else {
return;
};
let Ok(app) = app_obj.downcast::<NSApplication>() else {
return;
};
build_window_and_webview(self, &app);
app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
#[allow(deprecated)]
app.activateIgnoringOtherApps(true);
});
}
}
#[allow(non_snake_case)]
unsafe impl NSWindowDelegate for DesktopAppDelegate {
#[unsafe(method(windowShouldClose:))]
fn windowShouldClose(&self, _window: &NSWindow) -> bool {
!matches!(
dispatch_for_delegate(
self,
DesktopEvent::WindowCloseRequested {
window_id: WindowId::PRIMARY
}
),
EventResponse::PreventDefault
)
}
#[unsafe(method(windowDidResize:))]
fn windowDidResize(&self, _notification: &NSNotification) {
if let Some(window) = self.ivars().window.get() {
let zoomed = window.isZoomed();
if self.ivars().zoomed.replace(zoomed) != zoomed {
dispatch_for_delegate(
self,
if zoomed {
DesktopEvent::WindowMaximized {
window_id: WindowId::PRIMARY,
}
} else {
DesktopEvent::WindowUnmaximized {
window_id: WindowId::PRIMARY,
}
},
);
}
let size = window.frame().size;
dispatch_for_delegate(
self,
DesktopEvent::WindowResized {
window_id: WindowId::PRIMARY,
width: clamp_dimension(size.width),
height: clamp_dimension(size.height),
},
);
}
}
#[unsafe(method(windowDidMove:))]
fn windowDidMove(&self, _notification: &NSNotification) {
if let Some(window) = self.ivars().window.get() {
let origin = window.frame().origin;
dispatch_for_delegate(
self,
DesktopEvent::WindowMoved {
window_id: WindowId::PRIMARY,
x: clamp_coordinate(origin.x),
y: clamp_coordinate(origin.y),
},
);
}
}
#[unsafe(method(windowDidChangeBackingProperties:))]
fn windowDidChangeBackingProperties(&self, _notification: &NSNotification) {
if let Some(window) = self.ivars().window.get() {
dispatch_for_delegate(
self,
DesktopEvent::ScaleFactorChanged {
scale: window.backingScaleFactor(),
},
);
}
}
#[unsafe(method(windowDidMiniaturize:))]
fn windowDidMiniaturize(&self, _notification: &NSNotification) {
dispatch_for_delegate(
self,
DesktopEvent::WindowMinimized {
window_id: WindowId::PRIMARY,
},
);
}
#[unsafe(method(windowDidDeminiaturize:))]
fn windowDidDeminiaturize(&self, _notification: &NSNotification) {
dispatch_for_delegate(
self,
DesktopEvent::WindowRestored {
window_id: WindowId::PRIMARY,
},
);
}
#[unsafe(method(windowDidBecomeKey:))]
fn windowDidBecomeKey(&self, _notification: &NSNotification) {
dispatch_for_delegate(
self,
DesktopEvent::WindowFocused {
window_id: WindowId::PRIMARY,
},
);
}
#[unsafe(method(windowDidResignKey:))]
fn windowDidResignKey(&self, _notification: &NSNotification) {
dispatch_for_delegate(
self,
DesktopEvent::WindowBlurred {
window_id: WindowId::PRIMARY,
},
);
}
#[unsafe(method(windowDidEnterFullScreen:))]
fn windowDidEnterFullScreen(&self, _notification: &NSNotification) {
dispatch_for_delegate(
self,
DesktopEvent::WindowEnteredFullscreen {
window_id: WindowId::PRIMARY,
},
);
}
#[unsafe(method(windowDidExitFullScreen:))]
fn windowDidExitFullScreen(&self, _notification: &NSNotification) {
dispatch_for_delegate(
self,
DesktopEvent::WindowLeftFullscreen {
window_id: WindowId::PRIMARY,
},
);
}
#[unsafe(method(windowWillClose:))]
fn windowWillClose(&self, _notification: &NSNotification) {
if self.ivars().exiting.replace(true) {
return;
}
if let Some(wake) = self.ivars().command_wake.get() {
wake.close();
}
#[cfg(feature = "application-ipc")]
if let Some(ipc) = &self.ivars().ipc {
ipc.close();
}
persist_window_state_if_enabled(self);
dispatch_for_delegate(
self,
DesktopEvent::WindowClosed {
window_id: WindowId::PRIMARY,
},
);
dispatch_for_delegate(self, DesktopEvent::Exiting);
NSApplication::sharedApplication(self.mtm()).terminate(None);
}
}
);
pub(super) fn dispatch_for_delegate(
delegate: &DesktopAppDelegate,
event: DesktopEvent,
) -> EventResponse {
let ivars = delegate.ivars();
let Some(webview) = ivars.webview.get() else {
return EventResponse::Continue;
};
dispatch_event(&ivars.events, webview, event)
}
impl DesktopAppDelegate {
pub(super) fn new(mtm: MainThreadMarker, options: MacosLaunchOptions) -> Retained<Self> {
let maximized = options.options.maximized;
let this = Self::alloc(mtm).set_ivars(AppDelegateIvars {
executor: options.executor,
runtime: options.runtime,
command_wake: OnceCell::new(),
#[cfg(feature = "application-ipc")]
ipc: options.ipc,
window: OnceCell::new(),
webview: OnceCell::new(),
scheme_handler: OnceCell::new(),
navigation_delegate: OnceCell::new(),
host_message_handler: OnceCell::new(),
theme_observer: OnceCell::new(),
tray_item: OnceCell::new(),
main_menu: OnceCell::new(),
title: options.title,
options: options.options,
shell: options.shell,
events: options.events,
window_handle: options.window_handle,
state_store: options.state_store,
zoomed: Cell::new(maximized),
exiting: Cell::new(false),
});
unsafe { msg_send![super(this), init] }
}
}