use std::{
f64,
ffi::CStr,
os::raw::c_void,
sync::{Arc, Weak},
};
use objc2::{
msg_send,
rc::Retained,
runtime::{AnyClass as Class, AnyObject as Object, ClassBuilder as ClassDecl, Sel},
};
use objc2_app_kit::{
self as appkit, NSApplicationPresentationOptions, NSPasteboard, NSView, NSWindow,
};
use objc2_foundation::{ns_string, NSArray, NSAutoreleasePool, NSString, NSUInteger};
use once_cell::sync::Lazy;
use crate::{
dpi::{LogicalPosition, LogicalSize},
event::{Event, WindowEvent},
keyboard::ModifiersState,
platform_impl::platform::{
app_state::AppState,
event::{EventProxy, EventWrapper},
ffi::{id, nil, BOOL, NO, YES},
util::{self, IdRef},
view::ViewState,
window::{get_ns_theme, get_window_id, UnownedWindow},
},
window::{Fullscreen, WindowId},
};
pub struct WindowDelegateState {
ns_window: Retained<NSWindow>, ns_view: Retained<NSView>,
window: Weak<UnownedWindow>,
initial_fullscreen: bool,
previous_position: Option<(f64, f64)>,
previous_scale_factor: f64,
is_checking_zoomed_in: bool,
}
impl WindowDelegateState {
pub fn new(window: &Arc<UnownedWindow>, initial_fullscreen: bool) -> Self {
let scale_factor = window.scale_factor();
let mut delegate_state = WindowDelegateState {
ns_window: window.ns_window.clone(),
ns_view: window.ns_view.clone(),
window: Arc::downgrade(window),
initial_fullscreen,
previous_position: None,
previous_scale_factor: scale_factor,
is_checking_zoomed_in: false,
};
if (scale_factor - 1.0).abs() > f64::EPSILON {
delegate_state.emit_static_scale_factor_changed_event();
}
delegate_state
}
fn ns_view(&self) -> Retained<NSView> {
self.ns_window.contentView().unwrap()
}
fn with_window<F, T>(&mut self, callback: F) -> Option<T>
where
F: FnOnce(&UnownedWindow) -> T,
{
self.window.upgrade().map(|ref window| callback(window))
}
pub fn emit_event(&mut self, event: WindowEvent<'static>) {
let event = Event::WindowEvent {
window_id: WindowId(get_window_id(&self.ns_window)),
event,
};
AppState::queue_event(EventWrapper::StaticEvent(event));
}
pub fn emit_static_scale_factor_changed_event(&mut self) {
let scale_factor = self.get_scale_factor();
if (scale_factor - self.previous_scale_factor).abs() < f64::EPSILON {
return;
};
self.previous_scale_factor = scale_factor;
let wrapper = EventWrapper::EventProxy(EventProxy::DpiChangedProxy {
ns_window: self.ns_window.clone(),
suggested_size: self.view_size(),
scale_factor,
});
AppState::queue_event(wrapper);
}
pub fn emit_resize_event(&mut self) {
let rect = NSView::frame(&self.ns_view());
let scale_factor = self.get_scale_factor();
let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64);
let size = logical_size.to_physical(scale_factor);
self.emit_event(WindowEvent::Resized(size));
}
fn emit_move_event(&mut self) {
let rect = NSWindow::frame(&self.ns_window);
let x = rect.origin.x as f64;
let y = util::bottom_left_to_top_left(rect);
let moved = self.previous_position != Some((x, y));
if moved {
self.previous_position = Some((x, y));
let scale_factor = self.get_scale_factor();
let physical_pos = LogicalPosition::<f64>::from((x, y)).to_physical(scale_factor);
self.emit_event(WindowEvent::Moved(physical_pos));
}
}
fn get_scale_factor(&self) -> f64 {
NSWindow::backingScaleFactor(&self.ns_window) as f64
}
fn view_size(&self) -> LogicalSize<f64> {
let ns_size = NSView::frame(&self.ns_view()).size;
LogicalSize::new(ns_size.width as f64, ns_size.height as f64)
}
}
pub fn new_delegate(window: &Arc<UnownedWindow>, initial_fullscreen: bool) -> IdRef {
let state = WindowDelegateState::new(window, initial_fullscreen);
unsafe {
let state_ptr = Box::into_raw(Box::new(state)) as *mut c_void;
let delegate: id = msg_send![WINDOW_DELEGATE_CLASS.0, alloc];
IdRef::new(msg_send![delegate, initWithTao: state_ptr])
}
}
struct WindowDelegateClass(*const Class);
unsafe impl Send for WindowDelegateClass {}
unsafe impl Sync for WindowDelegateClass {}
static WINDOW_DELEGATE_CLASS: Lazy<WindowDelegateClass> = Lazy::new(|| unsafe {
let superclass = class!(NSResponder);
let mut decl = ClassDecl::new(
CStr::from_bytes_with_nul(b"TaoWindowDelegate\0").unwrap(),
superclass,
)
.unwrap();
decl.add_method(sel!(dealloc), dealloc as extern "C" fn(_, _));
decl.add_method(
sel!(initWithTao:),
init_with_tao as extern "C" fn(_, _, _) -> _,
);
decl.add_method(
sel!(markIsCheckingZoomedIn),
mark_is_checking_zoomed_in as extern "C" fn(_, _),
);
decl.add_method(
sel!(clearIsCheckingZoomedIn),
clear_is_checking_zoomed_in as extern "C" fn(_, _),
);
decl.add_method(
sel!(windowShouldClose:),
window_should_close as extern "C" fn(_, _, _) -> _,
);
decl.add_method(
sel!(windowWillClose:),
window_will_close as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(windowDidResize:),
window_did_resize as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(windowDidMove:),
window_did_move as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(windowDidChangeBackingProperties:),
window_did_change_backing_properties as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(windowDidBecomeKey:),
window_did_become_key as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(windowDidResignKey:),
window_did_resign_key as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(draggingEntered:),
dragging_entered as extern "C" fn(_, _, _) -> _,
);
decl.add_method(
sel!(prepareForDragOperation:),
prepare_for_drag_operation as extern "C" fn(_, _, _) -> _,
);
decl.add_method(
sel!(performDragOperation:),
perform_drag_operation as extern "C" fn(_, _, _) -> _,
);
decl.add_method(
sel!(concludeDragOperation:),
conclude_drag_operation as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(draggingExited:),
dragging_exited as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(window:willUseFullScreenPresentationOptions:),
window_will_use_fullscreen_presentation_options as extern "C" fn(_, _, _, _) -> _,
);
decl.add_method(
sel!(windowDidEnterFullScreen:),
window_did_enter_fullscreen as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(windowWillEnterFullScreen:),
window_will_enter_fullscreen as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(windowDidExitFullScreen:),
window_did_exit_fullscreen as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(windowWillExitFullScreen:),
window_will_exit_fullscreen as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(windowDidFailToEnterFullScreen:),
window_did_fail_to_enter_fullscreen as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(effectiveAppearanceDidChange:),
effective_appearance_did_change as extern "C" fn(_, _, _),
);
decl.add_method(
sel!(effectiveAppearanceDidChangedOnMainThread:),
effective_appearance_did_changed_on_main_thread as extern "C" fn(_, _, _),
);
decl.add_ivar::<*mut c_void>(CStr::from_bytes_with_nul(b"taoState\0").unwrap());
WindowDelegateClass(decl.register())
});
fn with_state<F: FnOnce(&mut WindowDelegateState) -> T, T>(this: &Object, callback: F) {
#[allow(deprecated)] let state_ptr = unsafe {
let state_ptr: *mut c_void = *this.get_ivar("taoState");
&mut *(state_ptr as *mut WindowDelegateState)
};
callback(state_ptr);
}
extern "C" fn dealloc(this: &Object, _sel: Sel) {
with_state(this, |state| unsafe {
drop(Box::from_raw(state as *mut WindowDelegateState));
});
}
extern "C" fn init_with_tao(this: &Object, _sel: Sel, state: *mut c_void) -> id {
#[allow(deprecated)] unsafe {
let this: id = msg_send![this, init];
if this != nil {
*(*this).get_mut_ivar("taoState") = state;
with_state(&*this, |state| {
let () = msg_send![&state.ns_window, setDelegate: this];
});
}
let notification_center: &Object =
msg_send![class!(NSDistributedNotificationCenter), defaultCenter];
let notification_name = ns_string!("AppleInterfaceThemeChangedNotification");
let _: () = msg_send![
notification_center,
addObserver: this
selector: sel!(effectiveAppearanceDidChange:)
name: &*notification_name
object: nil
];
this
}
}
extern "C" fn mark_is_checking_zoomed_in(this: &Object, _sel: Sel) {
with_state(&*this, |state| {
state.is_checking_zoomed_in = true;
});
}
extern "C" fn clear_is_checking_zoomed_in(this: &Object, _sel: Sel) {
with_state(&*this, |state| {
state.is_checking_zoomed_in = false;
});
}
extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL {
trace!("Triggered `windowShouldClose:`");
with_state(this, |state| state.emit_event(WindowEvent::CloseRequested));
trace!("Completed `windowShouldClose:`");
NO
}
extern "C" fn window_will_close(this: &Object, _: Sel, _: id) {
trace!("Triggered `windowWillClose:`");
with_state(this, |state| unsafe {
let _pool = NSAutoreleasePool::new();
let () = msg_send![&state.ns_window, setDelegate: nil];
state.emit_event(WindowEvent::Destroyed);
});
trace!("Completed `windowWillClose:`");
}
extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
trace!("Triggered `windowDidResize:`");
with_state(this, |state| {
if !state.is_checking_zoomed_in {
state.emit_resize_event();
state.emit_move_event();
}
});
trace!("Completed `windowDidResize:`");
}
extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
trace!("Triggered `windowDidMove:`");
with_state(this, |state| {
state.emit_move_event();
});
trace!("Completed `windowDidMove:`");
}
extern "C" fn window_did_change_backing_properties(this: &Object, _: Sel, _: id) {
trace!("Triggered `windowDidChangeBackingProperties:`");
with_state(this, |state| {
state.emit_static_scale_factor_changed_event();
});
trace!("Completed `windowDidChangeBackingProperties:`");
}
extern "C" fn window_did_become_key(this: &Object, _: Sel, _: id) {
trace!("Triggered `windowDidBecomeKey:`");
with_state(this, |state| {
state.emit_event(WindowEvent::Focused(true));
});
trace!("Completed `windowDidBecomeKey:`");
}
extern "C" fn window_did_resign_key(this: &Object, _: Sel, _: id) {
trace!("Triggered `windowDidResignKey:`");
with_state(this, |state| {
#[allow(deprecated)] let view_state: &mut ViewState = unsafe {
let ns_view: &Object = &state.ns_view;
let state_ptr: *mut c_void = *ns_view.get_ivar("taoState");
&mut *(state_ptr as *mut ViewState)
};
if !view_state.modifiers.is_empty() {
view_state.modifiers = ModifiersState::empty();
state.emit_event(WindowEvent::ModifiersChanged(view_state.modifiers));
}
state.emit_event(WindowEvent::Focused(false));
});
trace!("Completed `windowDidResignKey:`");
}
extern "C" fn dragging_entered(this: &Object, _: Sel, sender: id) -> BOOL {
trace!("Triggered `draggingEntered:`");
use std::path::PathBuf;
let pb: Retained<NSPasteboard> = unsafe { msg_send![sender, draggingPasteboard] };
let filenames =
unsafe { NSPasteboard::propertyListForType(&pb, appkit::NSFilenamesPboardType) }.unwrap();
for file in unsafe { Retained::cast_unchecked::<NSArray>(filenames) } {
let file = unsafe { Retained::cast_unchecked::<NSString>(file) };
unsafe {
let f = NSString::UTF8String(&file);
let path = CStr::from_ptr(f).to_string_lossy().into_owned();
with_state(this, |state| {
state.emit_event(WindowEvent::HoveredFile(PathBuf::from(path)));
});
}
}
trace!("Completed `draggingEntered:`");
YES
}
extern "C" fn prepare_for_drag_operation(_: &Object, _: Sel, _: id) -> BOOL {
trace!("Triggered `prepareForDragOperation:`");
trace!("Completed `prepareForDragOperation:`");
YES
}
extern "C" fn perform_drag_operation(this: &Object, _: Sel, sender: id) -> BOOL {
trace!("Triggered `performDragOperation:`");
use std::path::PathBuf;
let pb: Retained<NSPasteboard> = unsafe { msg_send![sender, draggingPasteboard] };
let filenames =
unsafe { NSPasteboard::propertyListForType(&pb, appkit::NSFilenamesPboardType) }.unwrap();
for file in unsafe { Retained::cast_unchecked::<NSArray>(filenames) } {
let file = unsafe { Retained::cast_unchecked::<NSString>(file) };
unsafe {
let f = NSString::UTF8String(&file);
let path = CStr::from_ptr(f).to_string_lossy().into_owned();
with_state(this, |state| {
state.emit_event(WindowEvent::DroppedFile(PathBuf::from(path)));
});
}
}
trace!("Completed `performDragOperation:`");
YES
}
extern "C" fn conclude_drag_operation(_: &Object, _: Sel, _: id) {
trace!("Triggered `concludeDragOperation:`");
trace!("Completed `concludeDragOperation:`");
}
extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
trace!("Triggered `draggingExited:`");
with_state(this, |state| {
state.emit_event(WindowEvent::HoveredFileCancelled)
});
trace!("Completed `draggingExited:`");
}
extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) {
trace!("Triggered `windowWillEnterFullscreen:`");
with_state(this, |state| {
state.with_window(|window| {
trace!("Locked shared state in `window_will_enter_fullscreen`");
let mut shared_state = window.shared_state.lock().unwrap();
shared_state.maximized = window.is_zoomed();
match shared_state.fullscreen {
Some(Fullscreen::Exclusive(_)) => (),
Some(Fullscreen::Borderless(_)) => (),
None => {
let current_monitor = window.current_monitor_inner();
shared_state.fullscreen = Some(Fullscreen::Borderless(current_monitor))
}
}
shared_state.in_fullscreen_transition = true;
trace!("Unlocked shared state in `window_will_enter_fullscreen`");
})
});
trace!("Completed `windowWillEnterFullscreen:`");
}
extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) {
trace!("Triggered `windowWillExitFullScreen:`");
with_state(this, |state| {
state.with_window(|window| {
trace!("Locked shared state in `window_will_exit_fullscreen`");
let mut shared_state = window.shared_state.lock().unwrap();
shared_state.in_fullscreen_transition = true;
trace!("Unlocked shared state in `window_will_exit_fullscreen`");
});
});
trace!("Completed `windowWillExitFullScreen:`");
}
extern "C" fn window_will_use_fullscreen_presentation_options(
this: &Object,
_: Sel,
_: id,
proposed_options: NSUInteger,
) -> NSUInteger {
let mut options: NSUInteger = proposed_options;
with_state(this, |state| {
state.with_window(|window| {
trace!("Locked shared state in `window_will_use_fullscreen_presentation_options`");
let shared_state = window.shared_state.lock().unwrap();
if let Some(Fullscreen::Exclusive(_)) = shared_state.fullscreen {
options = (NSApplicationPresentationOptions::FullScreen
| NSApplicationPresentationOptions::HideDock
| NSApplicationPresentationOptions::HideMenuBar)
.bits();
}
trace!("Unlocked shared state in `window_will_use_fullscreen_presentation_options`");
})
});
options
}
extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) {
trace!("Triggered `windowDidEnterFullscreen:`");
with_state(this, |state| {
state.initial_fullscreen = false;
state.with_window(|window| {
trace!("Locked shared state in `window_did_enter_fullscreen`");
let mut shared_state = window.shared_state.lock().unwrap();
shared_state.in_fullscreen_transition = false;
let target_fullscreen = shared_state.target_fullscreen.take();
trace!("Unlocked shared state in `window_did_enter_fullscreen`");
drop(shared_state);
if let Some(target_fullscreen) = target_fullscreen {
window.set_fullscreen(target_fullscreen);
}
});
state.emit_resize_event();
state.emit_move_event();
});
trace!("Completed `windowDidEnterFullscreen:`");
}
extern "C" fn window_did_exit_fullscreen(this: &Object, _: Sel, _: id) {
trace!("Triggered `windowDidExitFullscreen:`");
with_state(this, |state| {
state.with_window(|window| {
window.restore_state_from_fullscreen();
trace!("Locked shared state in `window_did_exit_fullscreen`");
let mut shared_state = window.shared_state.lock().unwrap();
shared_state.in_fullscreen_transition = false;
let target_fullscreen = shared_state.target_fullscreen.take();
trace!("Unlocked shared state in `window_did_exit_fullscreen`");
drop(shared_state);
if let Some(target_fullscreen) = target_fullscreen {
window.set_fullscreen(target_fullscreen);
}
});
state.emit_resize_event();
state.emit_move_event();
});
trace!("Completed `windowDidExitFullscreen:`");
}
extern "C" fn window_did_fail_to_enter_fullscreen(this: &Object, _: Sel, _: id) {
trace!("Triggered `windowDidFailToEnterFullscreen:`");
with_state(this, |state| {
state.with_window(|window| {
trace!("Locked shared state in `window_did_fail_to_enter_fullscreen`");
let mut shared_state = window.shared_state.lock().unwrap();
shared_state.in_fullscreen_transition = false;
shared_state.target_fullscreen = None;
trace!("Unlocked shared state in `window_did_fail_to_enter_fullscreen`");
});
if state.initial_fullscreen {
let _: () = unsafe {
msg_send![
&state.ns_window,
performSelector:sel!(toggleFullScreen:),
withObject:nil,
afterDelay: 0.5,
]
};
} else {
state.with_window(|window| window.restore_state_from_fullscreen());
}
});
trace!("Completed `windowDidFailToEnterFullscreen:`");
}
extern "C" fn effective_appearance_did_change(this: &Object, _: Sel, _: id) {
trace!("Triggered `effectiveAppearDidChange:`");
unsafe {
let _: () = msg_send![this, performSelectorOnMainThread: sel!(effectiveAppearanceDidChangedOnMainThread:), withObject:nil, waitUntilDone:false];
}
}
extern "C" fn effective_appearance_did_changed_on_main_thread(this: &Object, _: Sel, _: id) {
with_state(this, |state| {
let theme = get_ns_theme();
let current_theme = state.window.upgrade().map(|w| {
let mut state = w.shared_state.lock().unwrap();
let current_theme = state.current_theme;
state.current_theme = theme;
current_theme
});
if current_theme != Some(theme) {
state.emit_event(WindowEvent::ThemeChanged(theme));
}
});
trace!("Completed `effectiveAppearDidChange:`");
}