use std::rc::Rc;
use std::sync::Arc;
use std::time::{Duration, Instant};
use objc2::rc::{Retained, autoreleasepool};
use objc2::runtime::ProtocolObject;
use objc2::{AnyThread, MainThreadMarker, available};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification,
NSApplicationWillTerminateNotification, NSDraggingItem, NSPasteboard, NSPasteboardItem,
NSPasteboardTypeFileURL, NSPasteboardWriting, NSWindow,
};
use objc2_core_foundation::{
CFIndex, CFRunLoopActivity, CGPoint, CGRect, CGSize, kCFRunLoopCommonModes,
};
use objc2_foundation::{NSArray, NSNotificationCenter, NSObjectProtocol, NSString};
use rwh_06::HasDisplayHandle;
use tracing::debug_span;
use wingy_common::core_foundation::{MainRunLoop, MainRunLoopObserver, tracing_observers};
use wingy_common::foundation::create_observer;
use wingy_core::application::ApplicationHandler;
use wingy_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
use wingy_core::data_transfer::{
DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData,
};
use wingy_core::error::{CreateWindowError, CustomCursorError, EventLoopError, TransferError};
use wingy_core::event::WindowEvent;
use wingy_core::event_loop::pump_events::PumpStatus;
use wingy_core::event_loop::{
ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents,
DndAction, DragIcon, EventLoopProvider, EventLoopProxy as CoreEventLoopProxy,
OwnedDisplayHandle as CoreOwnedDisplayHandle,
};
use wingy_core::monitor::MonitorHandle as CoreMonitorHandle;
use wingy_core::window::{Theme, WindowId};
use super::app::override_send_event;
use super::app_state::AppState;
use super::cursor::CustomCursor;
use super::event::dummy_event;
use super::monitor;
use crate::ActivationPolicy;
use crate::cursor::image_from_icon;
use crate::data_transfer::{
Pasteboard, PasteboardType, PasteboardTypeSpec, PasteboardWriter,
dnd_actions_to_ns_drag_operation,
};
use crate::window::Window;
#[derive(Debug)]
pub struct ActiveEventLoop {
pub(super) app_state: Rc<AppState>,
pub(super) mtm: MainThreadMarker,
}
impl ActiveEventLoop {
pub(crate) fn hide_application(&self) {
NSApplication::sharedApplication(self.mtm).hide(None)
}
pub(crate) fn hide_other_applications(&self) {
NSApplication::sharedApplication(self.mtm).hideOtherApplications(None)
}
pub(crate) fn set_allows_automatic_window_tabbing(&self, enabled: bool) {
NSWindow::setAllowsAutomaticWindowTabbing(enabled, self.mtm)
}
pub(crate) fn allows_automatic_window_tabbing(&self) -> bool {
NSWindow::allowsAutomaticWindowTabbing(self.mtm)
}
fn clipboard_by_id(&self, id: DataTransferId) -> Option<Pasteboard> {
let pb = Pasteboard::new(id, NSPasteboard::generalPasteboard(), self.mtm);
(self.app_state.data_transfer().clipboard_transfer_id(pb.change_count()) == id)
.then_some(pb)
}
}
impl RootActiveEventLoop for ActiveEventLoop {
fn create_proxy(&self) -> CoreEventLoopProxy {
CoreEventLoopProxy::new(self.app_state.event_loop_proxy().clone())
}
fn create_window(
&self,
window_attributes: wingy_core::window::WindowAttributes,
) -> Result<Box<dyn wingy_core::window::Window>, CreateWindowError> {
Ok(Box::new(Window::new(self, window_attributes)?))
}
fn create_custom_cursor(
&self,
source: CustomCursorSource,
) -> Result<CoreCustomCursor, CustomCursorError> {
Ok(CoreCustomCursor(Arc::new(CustomCursor::new(source)?)))
}
fn available_monitors(&self) -> Box<dyn Iterator<Item = CoreMonitorHandle>> {
Box::new(
monitor::available_monitors()
.into_iter()
.map(|monitor| CoreMonitorHandle(Arc::new(monitor))),
)
}
fn primary_monitor(&self) -> Option<wingy_core::monitor::MonitorHandle> {
let monitor = monitor::primary_monitor();
Some(CoreMonitorHandle(Arc::new(monitor)))
}
fn listen_device_events(&self, _allowed: DeviceEvents) {}
fn system_theme(&self) -> Option<Theme> {
let app = NSApplication::sharedApplication(self.mtm);
if available!(macos = 10.14) {
Some(super::window_delegate::appearance_to_theme(&app.effectiveAppearance()))
} else {
Some(Theme::Light)
}
}
fn set_control_flow(&self, control_flow: ControlFlow) {
self.app_state.set_control_flow(control_flow)
}
fn control_flow(&self) -> ControlFlow {
self.app_state.control_flow()
}
fn exit(&self) {
self.app_state.exit()
}
fn exiting(&self) -> bool {
self.app_state.exiting()
}
fn owned_display_handle(&self) -> CoreOwnedDisplayHandle {
CoreOwnedDisplayHandle::new(Arc::new(OwnedDisplayHandle))
}
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
self
}
fn fetch_data_transfer(
&self,
id: DataTransferId,
type_: &dyn TransferType,
) -> Result<AsyncRequestSerial, TransferError> {
let (pb, window_id) = match self.app_state.data_transfer().pasteboards().get(id) {
Some(pb) => {
let Some(window_id) = self.app_state.data_transfer().pasteboards().window_id(id)
else {
return Err(TransferError::Failed);
};
(pb, window_id)
},
None => {
let pb = self.clipboard_by_id(id).ok_or(TransferError::UnknownTransfer(id))?;
let window_id =
self.app_state.clipboard_window(self.mtm).ok_or(TransferError::Failed)?;
(pb, window_id)
},
};
let type_ = match type_.cast_ref::<PasteboardType>() {
Some(pb_type) => pb_type.clone(),
None => {
let hint = type_.hint().ok_or(TransferError::Failed)?;
pb.types()
.iter()
.find(|advertised| {
advertised.hint().is_some_and(|haystack| haystack.matches(&hint))
})
.cloned()
.ok_or(TransferError::Failed)?
},
};
let serial = AsyncRequestSerial::get();
let data: Arc<dyn TypedData> =
Arc::new(pb.with_type(PasteboardTypeSpec::PasteboardType(type_)));
self.app_state.maybe_queue_with_handler(move |app, event_loop| {
app.window_event(
event_loop,
window_id,
WindowEvent::DataTransferReceived { id, serial, value: data },
);
});
Ok(serial)
}
fn data_transfer(&self, id: DataTransferId) -> Result<Box<dyn DataTransfer>, TransferError> {
if let Some(pb) = self.app_state.data_transfer().pasteboards().get(id) {
return Ok(Box::new(pb));
}
let pb = self.clipboard_by_id(id).ok_or(TransferError::UnknownTransfer(id))?;
Ok(Box::new(pb))
}
fn clipboard(&self) -> Result<Option<DataTransferId>, TransferError> {
let pb = NSPasteboard::generalPasteboard();
if pb.types().is_none_or(|types| types.is_empty()) {
return Ok(None);
}
Ok(Some(self.app_state.data_transfer().clipboard_transfer_id(pb.changeCount())))
}
fn set_clipboard(&self, send_data: Box<dyn DataTransferSend>) -> Result<(), TransferError> {
let mut uris = match send_data.data_for_type(&TypeHint::UriList) {
Some(SendData::Uris(uris)) => uris,
Some(SendData::String(string)) => vec![string],
_ => vec![],
}
.into_iter()
.map(|uri| NSString::from_str(&uri));
let first_uri = uris.next();
let mut objects: Vec<Retained<ProtocolObject<dyn NSPasteboardWriting>>> =
vec![ProtocolObject::from_retained(PasteboardWriter::new(send_data, first_uri))];
for uri in uris {
let item = NSPasteboardItem::new();
if !item.setString_forType(&uri, unsafe { NSPasteboardTypeFileURL }) {
return Err(os_error!("failed to write a URI to a pasteboard item").into());
}
objects.push(ProtocolObject::from_retained(item));
}
let pb = NSPasteboard::generalPasteboard();
pb.clearContents();
if pb.writeObjects(&NSArray::from_retained_slice(&objects)) {
Ok(())
} else {
Err(os_error!("failed to write to the general pasteboard").into())
}
}
fn set_valid_dnd_actions(
&self,
id: DataTransferId,
actions: &[DndAction],
) -> Result<(), TransferError> {
if self.app_state.data_transfer().set_valid_dnd_actions(id, actions) {
Ok(())
} else {
Err(TransferError::UnknownTransfer(id))
}
}
fn start_drag(
&self,
source: WindowId,
send_data: Box<dyn DataTransferSend>,
actions: &[DndAction],
icon: Option<DragIcon>,
) -> Result<DataTransferId, TransferError> {
let drag_operation = dnd_actions_to_ns_drag_operation(actions);
self.app_state
.with_window_delegate_on_main(source, move |delegate| {
let (dragging_rect_offset_x, dragging_rect_offset_y) =
icon.as_ref().map(|icon| (icon.offset_x, icon.offset_y)).unwrap_or_default();
let drag_image = icon.and_then(|icon| image_from_icon(&icon.icon).ok());
let Some(event) = delegate.window().currentEvent() else {
return Err(TransferError::Failed);
};
let dragging_rect_size = drag_image
.as_ref()
.map(|img| img.size())
.unwrap_or(CGSize::new(16., 16.));
let event_location = event.locationInWindow();
let dragging_rect_location = CGPoint::new(
event_location.x + dragging_rect_offset_x as f64,
event_location.y - dragging_rect_size.height - dragging_rect_offset_y as f64,
);
let dragging_rect = CGRect::new(dragging_rect_location, dragging_rect_size);
let mut uris = send_data
.data_for_type(&TypeHint::UriList)
.and_then(|file_uris| {
let ns_url_from_str = |str: String| NSString::from_str(&str);
match file_uris {
SendData::Uris(os_strings) => Some(
None.into_iter().chain(os_strings.into_iter().map(ns_url_from_str)),
),
SendData::String(string) => Some(
Some(NSString::from_str(&string))
.into_iter()
.chain(Vec::new().into_iter().map(ns_url_from_str)),
),
SendData::Bytes(_) => None,
_ => None,
}
})
.into_iter()
.flatten();
let first_uri = uris.next();
let mut pasteboard_items = uris
.map(|ns_url| {
let dragging_item = NSDraggingItem::initWithPasteboardWriter(
NSDraggingItem::alloc(),
ProtocolObject::from_ref(&*ns_url),
);
dragging_item
})
.collect::<Vec<_>>();
let first_dragging_item = NSDraggingItem::initWithPasteboardWriter(
NSDraggingItem::alloc(),
ProtocolObject::from_ref(&*PasteboardWriter::new(send_data, first_uri)),
);
unsafe {
first_dragging_item.setDraggingFrame_contents(
dragging_rect,
drag_image.as_ref().map(AsRef::as_ref),
)
};
pasteboard_items.insert(0, first_dragging_item);
let pasteboard_items = NSArray::from_retained_slice(&pasteboard_items);
let session = delegate.window().beginDraggingSessionWithItems_event_source(
&pasteboard_items,
&event,
ProtocolObject::from_ref(&*delegate),
);
let id = DataTransferId::from_raw(session.draggingSequenceNumber() as i64);
delegate.view().set_dragging_session(session, drag_operation);
Ok(id)
})
.ok_or(TransferError::Failed)?
}
}
impl rwh_06::HasDisplayHandle for ActiveEventLoop {
fn display_handle(&self) -> Result<rwh_06::DisplayHandle<'_>, rwh_06::HandleError> {
let raw = rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new());
unsafe { Ok(rwh_06::DisplayHandle::borrow_raw(raw)) }
}
}
#[derive(Debug)]
pub struct EventLoop {
app: Retained<NSApplication>,
app_state: Rc<AppState>,
window_target: ActiveEventLoop,
_did_finish_launching_observer: Retained<ProtocolObject<dyn NSObjectProtocol>>,
_will_terminate_observer: Retained<ProtocolObject<dyn NSObjectProtocol>>,
_tracing_observers: Option<(MainRunLoopObserver, MainRunLoopObserver)>,
_before_waiting_observer: MainRunLoopObserver,
_after_waiting_observer: MainRunLoopObserver,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct PlatformSpecificEventLoopAttributes {
pub activation_policy: Option<ActivationPolicy>,
pub default_menu: bool,
pub activate_ignoring_other_apps: bool,
}
impl Default for PlatformSpecificEventLoopAttributes {
fn default() -> Self {
Self { activation_policy: None, default_menu: true, activate_ignoring_other_apps: true }
}
}
impl EventLoop {
pub fn new(attributes: &PlatformSpecificEventLoopAttributes) -> Result<Self, EventLoopError> {
let mtm = MainThreadMarker::new()
.expect("on macOS, `EventLoop` must be created on the main thread!");
let activation_policy = match attributes.activation_policy {
None => None,
Some(ActivationPolicy::Regular) => Some(NSApplicationActivationPolicy::Regular),
Some(ActivationPolicy::Accessory) => Some(NSApplicationActivationPolicy::Accessory),
Some(ActivationPolicy::Prohibited) => Some(NSApplicationActivationPolicy::Prohibited),
};
let app_state = AppState::setup_global(
mtm,
activation_policy,
attributes.default_menu,
attributes.activate_ignoring_other_apps,
)
.ok_or_else(|| EventLoopError::RecreationAttempt)?;
let app = NSApplication::sharedApplication(mtm);
override_send_event(&app);
let center = NSNotificationCenter::defaultCenter();
let weak_app_state = Rc::downgrade(&app_state);
let _did_finish_launching_observer = create_observer(
¢er,
unsafe { NSApplicationDidFinishLaunchingNotification },
move |notification| {
let _entered = debug_span!("NSApplicationDidFinishLaunchingNotification").entered();
if let Some(app_state) = weak_app_state.upgrade() {
app_state.did_finish_launching(notification);
}
},
);
let weak_app_state = Rc::downgrade(&app_state);
let _will_terminate_observer = create_observer(
¢er,
unsafe { NSApplicationWillTerminateNotification },
move |notification| {
let _entered = debug_span!("NSApplicationWillTerminateNotification").entered();
if let Some(app_state) = weak_app_state.upgrade() {
app_state.will_terminate(notification);
}
},
);
let main_loop = MainRunLoop::get(mtm);
let mode = unsafe { kCFRunLoopCommonModes }.unwrap();
let _tracing_observers = tracing_observers(mtm).inspect(|(start, end)| {
main_loop.add_observer(start, mode);
main_loop.add_observer(end, mode);
});
let app_state_clone = Rc::clone(&app_state);
let _before_waiting_observer = MainRunLoopObserver::new(
mtm,
CFRunLoopActivity::BeforeWaiting,
true,
CFIndex::MAX - 1,
move |_| app_state_clone.cleared(),
);
main_loop.add_observer(&_before_waiting_observer, mode);
let app_state_clone = Rc::clone(&app_state);
let _after_waiting_observer = MainRunLoopObserver::new(
mtm,
CFRunLoopActivity::AfterWaiting,
true,
CFIndex::MIN + 1,
move |_| app_state_clone.wakeup(),
);
main_loop.add_observer(&_after_waiting_observer, mode);
Ok(EventLoop {
app,
app_state: app_state.clone(),
window_target: ActiveEventLoop { app_state, mtm },
_did_finish_launching_observer,
_will_terminate_observer,
_tracing_observers,
_before_waiting_observer,
_after_waiting_observer,
})
}
pub fn window_target(&self) -> &dyn RootActiveEventLoop {
&self.window_target
}
pub fn run_app_on_demand<A: ApplicationHandler>(
&mut self,
app: A,
) -> Result<(), EventLoopError> {
self.app_state.clear_exit();
self.app_state.set_event_handler(app, || {
autoreleasepool(|_| {
self.app_state.set_wait_timeout(None);
self.app_state.set_stop_before_wait(false);
self.app_state.set_stop_after_wait(false);
self.app_state.set_stop_on_redraw(false);
if self.app_state.is_launched() {
debug_assert!(!self.app_state.is_running());
self.app_state.set_is_running(true);
self.app_state.dispatch_init_events();
}
self.app.run();
self.app_state.internal_exit()
})
});
Ok(())
}
pub fn pump_app_events<A: ApplicationHandler>(
&mut self,
timeout: Option<Duration>,
app: A,
) -> PumpStatus {
self.app_state.set_event_handler(app, || {
autoreleasepool(|_| {
if !self.app_state.is_launched() {
debug_assert!(!self.app_state.is_running());
self.app_state.set_stop_on_launch();
self.app.run();
} else if !self.app_state.is_running() {
self.app_state.set_is_running(true);
self.app_state.dispatch_init_events();
} else {
match timeout {
Some(Duration::ZERO) => {
self.app_state.set_wait_timeout(None);
self.app_state.set_stop_before_wait(true);
},
Some(duration) => {
self.app_state.set_stop_before_wait(false);
let timeout = Instant::now() + duration;
self.app_state.set_wait_timeout(Some(timeout));
self.app_state.set_stop_after_wait(true);
},
None => {
self.app_state.set_wait_timeout(None);
self.app_state.set_stop_before_wait(false);
self.app_state.set_stop_after_wait(true);
},
}
self.app_state.set_stop_on_redraw(true);
self.app.run();
}
if self.app_state.exiting() {
self.app_state.internal_exit();
PumpStatus::Exit(0)
} else {
PumpStatus::Continue
}
})
})
}
}
impl EventLoopProvider for EventLoop {
fn run_app<A: ApplicationHandler + 'static>(
mut self,
mut app: A,
) -> Result<(), EventLoopError> {
let result = self.run_app_on_demand(&mut app);
drop(app);
result
}
fn create_proxy(&self) -> CoreEventLoopProxy {
self.window_target().create_proxy()
}
fn owned_display_handle(&self) -> CoreOwnedDisplayHandle {
self.window_target().owned_display_handle()
}
fn listen_device_events(&self, allowed: DeviceEvents) {
self.window_target().listen_device_events(allowed);
}
fn set_control_flow(&self, control_flow: ControlFlow) {
self.window_target().set_control_flow(control_flow);
}
fn create_custom_cursor(
&self,
custom_cursor: CustomCursorSource,
) -> Result<CoreCustomCursor, CustomCursorError> {
self.window_target().create_custom_cursor(custom_cursor)
}
}
pub(crate) struct OwnedDisplayHandle;
impl HasDisplayHandle for OwnedDisplayHandle {
fn display_handle(&self) -> Result<rwh_06::DisplayHandle<'_>, rwh_06::HandleError> {
let raw = rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new());
unsafe { Ok(rwh_06::DisplayHandle::borrow_raw(raw)) }
}
}
pub(super) fn stop_app_immediately(app: &NSApplication) {
autoreleasepool(|_| {
app.stop(None);
app.postEvent_atStart(&dummy_event().unwrap(), true);
});
}
pub(super) fn notify_windows_of_exit(app: &NSApplication) {
for window in app.windows() {
window.close();
}
}