use std::{ f64, os::raw::c_void, sync::{Arc, Weak}, }; use cocoa::{ appkit::{self, NSApplicationPresentationOptions, NSView, NSWindow}, base::{id, nil}, foundation::{NSAutoreleasePool, NSUInteger}, }; use objc::{ declare::ClassDecl, runtime::{Class, Object, Sel, BOOL, NO, YES}, }; use crate::{ dpi::LogicalSize, event::{Event, WindowEvent}, platform_impl::platform::{ app_state::AppState, util::{self, IdRef}, window::{get_window_id, UnownedWindow}, }, window::{Fullscreen, WindowId}, }; pub struct WindowDelegateState { ns_window: IdRef, // never changes ns_view: IdRef, // never changes window: Weak, // TODO: It's possible for delegate methods to be called asynchronously, // causing data races / `RefCell` panics. // This is set when WindowBuilder::with_fullscreen was set, // see comments of `window_did_fail_to_enter_fullscreen` initial_fullscreen: bool, // During `windowDidResize`, we use this to only send Moved if the position changed. previous_position: Option<(f64, f64)>, // Used to prevent redundant events. previous_dpi_factor: f64, } impl WindowDelegateState { pub fn new(window: &Arc, initial_fullscreen: bool) -> Self { let dpi_factor = window.hidpi_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_dpi_factor: dpi_factor, }; if dpi_factor != 1.0 { delegate_state.emit_event(WindowEvent::HiDpiFactorChanged(dpi_factor)); delegate_state.emit_resize_event(); } delegate_state } fn with_window(&mut self, callback: F) -> Option where F: FnOnce(&UnownedWindow) -> T, { self.window.upgrade().map(|ref window| callback(window)) } pub fn emit_event(&mut self, event: WindowEvent) { let event = Event::WindowEvent { window_id: WindowId(get_window_id(*self.ns_window)), event, }; AppState::queue_event(event); } pub fn emit_resize_event(&mut self) { let rect = unsafe { NSView::frame(*self.ns_view) }; let size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); self.emit_event(WindowEvent::Resized(size)); } fn emit_move_event(&mut self) { let rect = unsafe { 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)); self.emit_event(WindowEvent::Moved((x, y).into())); } } } pub fn new_delegate(window: &Arc, initial_fullscreen: bool) -> IdRef { let state = WindowDelegateState::new(window, initial_fullscreen); unsafe { // This is free'd in `dealloc` 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, initWithWinit: state_ptr]) } } struct WindowDelegateClass(*const Class); unsafe impl Send for WindowDelegateClass {} unsafe impl Sync for WindowDelegateClass {} lazy_static! { static ref WINDOW_DELEGATE_CLASS: WindowDelegateClass = unsafe { let superclass = class!(NSResponder); let mut decl = ClassDecl::new("WinitWindowDelegate", superclass).unwrap(); decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel)); decl.add_method( sel!(initWithWinit:), init_with_winit as extern "C" fn(&Object, Sel, *mut c_void) -> id, ); decl.add_method( sel!(windowShouldClose:), window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, ); decl.add_method( sel!(windowWillClose:), window_will_close as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(windowDidResize:), window_did_resize as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(windowDidMove:), window_did_move as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(windowDidChangeScreen:), window_did_change_screen as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(windowDidChangeBackingProperties:), window_did_change_backing_properties as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(windowDidBecomeKey:), window_did_become_key as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(windowDidResignKey:), window_did_resign_key as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(draggingEntered:), dragging_entered as extern "C" fn(&Object, Sel, id) -> BOOL, ); decl.add_method( sel!(prepareForDragOperation:), prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, ); decl.add_method( sel!(performDragOperation:), perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, ); decl.add_method( sel!(concludeDragOperation:), conclude_drag_operation as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(draggingExited:), dragging_exited as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(window:willUseFullScreenPresentationOptions:), window_will_use_fullscreen_presentation_options as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger, ); decl.add_method( sel!(windowDidEnterFullScreen:), window_did_enter_fullscreen as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(windowWillEnterFullScreen:), window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(windowDidExitFullScreen:), window_did_exit_fullscreen as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(windowDidFailToEnterFullScreen:), window_did_fail_to_enter_fullscreen as extern "C" fn(&Object, Sel, id), ); decl.add_ivar::<*mut c_void>("winitState"); WindowDelegateClass(decl.register()) }; } // This function is definitely unsafe, but labeling that would increase // boilerplate and wouldn't really clarify anything... fn with_state T, T>(this: &Object, callback: F) { let state_ptr = unsafe { let state_ptr: *mut c_void = *this.get_ivar("winitState"); &mut *(state_ptr as *mut WindowDelegateState) }; callback(state_ptr); } extern "C" fn dealloc(this: &Object, _sel: Sel) { with_state(this, |state| unsafe { Box::from_raw(state as *mut WindowDelegateState); }); } extern "C" fn init_with_winit(this: &Object, _sel: Sel, state: *mut c_void) -> id { unsafe { let this: id = msg_send![this, init]; if this != nil { (*this).set_ivar("winitState", state); with_state(&*this, |state| { let () = msg_send![*state.ns_window, setDelegate: this]; }); } this } } 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 { // `setDelegate:` retains the previous value and then autoreleases it let pool = NSAutoreleasePool::new(nil); // Since El Capitan, we need to be careful that delegate methods can't // be called after the window closes. let () = msg_send![*state.ns_window, setDelegate: nil]; pool.drain(); 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| { state.emit_resize_event(); state.emit_move_event(); }); trace!("Completed `windowDidResize:`"); } // This won't be triggered if the move was part of a resize. 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_screen(this: &Object, _: Sel, _: id) { trace!("Triggered `windowDidChangeScreen:`"); with_state(this, |state| { let dpi_factor = unsafe { NSWindow::backingScaleFactor(*state.ns_window) } as f64; if state.previous_dpi_factor != dpi_factor { state.previous_dpi_factor = dpi_factor; state.emit_event(WindowEvent::HiDpiFactorChanged(dpi_factor)); state.emit_resize_event(); } }); trace!("Completed `windowDidChangeScreen:`"); } // This will always be called before `window_did_change_screen`. extern "C" fn window_did_change_backing_properties(this: &Object, _: Sel, _: id) { trace!("Triggered `windowDidChangeBackingProperties:`"); with_state(this, |state| { let dpi_factor = unsafe { NSWindow::backingScaleFactor(*state.ns_window) } as f64; if state.previous_dpi_factor != dpi_factor { state.previous_dpi_factor = dpi_factor; state.emit_event(WindowEvent::HiDpiFactorChanged(dpi_factor)); state.emit_resize_event(); } }); trace!("Completed `windowDidChangeBackingProperties:`"); } extern "C" fn window_did_become_key(this: &Object, _: Sel, _: id) { trace!("Triggered `windowDidBecomeKey:`"); with_state(this, |state| { // TODO: center the cursor if the window had mouse grab when it // lost focus 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| { state.emit_event(WindowEvent::Focused(false)); }); trace!("Completed `windowDidResignKey:`"); } /// Invoked when the dragged image enters destination bounds or frame extern "C" fn dragging_entered(this: &Object, _: Sel, sender: id) -> BOOL { trace!("Triggered `draggingEntered:`"); use cocoa::{appkit::NSPasteboard, foundation::NSFastEnumeration}; use std::path::PathBuf; let pb: id = unsafe { msg_send![sender, draggingPasteboard] }; let filenames = unsafe { NSPasteboard::propertyListForType(pb, appkit::NSFilenamesPboardType) }; for file in unsafe { filenames.iter() } { use cocoa::foundation::NSString; use std::ffi::CStr; 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 } /// Invoked when the image is released extern "C" fn prepare_for_drag_operation(_: &Object, _: Sel, _: id) -> BOOL { trace!("Triggered `prepareForDragOperation:`"); trace!("Completed `prepareForDragOperation:`"); YES } /// Invoked after the released image has been removed from the screen extern "C" fn perform_drag_operation(this: &Object, _: Sel, sender: id) -> BOOL { trace!("Triggered `performDragOperation:`"); use cocoa::{appkit::NSPasteboard, foundation::NSFastEnumeration}; use std::path::PathBuf; let pb: id = unsafe { msg_send![sender, draggingPasteboard] }; let filenames = unsafe { NSPasteboard::propertyListForType(pb, appkit::NSFilenamesPboardType) }; for file in unsafe { filenames.iter() } { use cocoa::foundation::NSString; use std::ffi::CStr; 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 } /// Invoked when the dragging operation is complete extern "C" fn conclude_drag_operation(_: &Object, _: Sel, _: id) { trace!("Triggered `concludeDragOperation:`"); trace!("Completed `concludeDragOperation:`"); } /// Invoked when the dragging operation is cancelled extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) { trace!("Triggered `draggingExited:`"); with_state(this, |state| { state.emit_event(WindowEvent::HoveredFileCancelled) }); trace!("Completed `draggingExited:`"); } /// Invoked when before enter fullscreen 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 { // Exclusive mode sets the state in `set_fullscreen` as the user // can't enter exclusive mode by other means (like the // fullscreen button on the window decorations) Some(Fullscreen::Exclusive(_)) => (), // `window_will_enter_fullscreen` was triggered and we're already // in fullscreen, so we must've reached here by `set_fullscreen` // as it updates the state Some(Fullscreen::Borderless(_)) => (), // Otherwise, we must've reached fullscreen by the user clicking // on the green fullscreen button. Update state! None => { shared_state.fullscreen = Some(Fullscreen::Borderless(window.current_monitor())) } } trace!("Unlocked shared state in `window_will_enter_fullscreen`"); }) }); trace!("Completed `windowWillEnterFullscreen:`"); } extern "C" fn window_will_use_fullscreen_presentation_options( _this: &Object, _: Sel, _: id, _proposed_options: NSUInteger, ) -> NSUInteger { // Generally, games will want to disable the menu bar and the dock. Ideally, // this would be configurable by the user. Unfortunately because of our // `CGShieldingWindowLevel() + 1` hack (see `set_fullscreen`), our window is // placed on top of the menu bar in exclusive fullscreen mode. This looks // broken so we always disable the menu bar in exclusive fullscreen. We may // still want to make this configurable for borderless fullscreen. Right now // we don't, for consistency. If we do, it should be documented that the // user-provided options are ignored in exclusive fullscreen. (NSApplicationPresentationOptions::NSApplicationPresentationFullScreen | NSApplicationPresentationOptions::NSApplicationPresentationHideDock | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar) .bits() } /// Invoked when entered fullscreen extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) { trace!("Triggered `windowDidEnterFullscreen:`"); with_state(this, |state| { state.initial_fullscreen = false; }); trace!("Completed `windowDidEnterFullscreen:`"); } /// Invoked when exited fullscreen 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!("Completed `windowDidExitFullscreen:`"); } /// Invoked when fail to enter fullscreen /// /// When this window launch from a fullscreen app (e.g. launch from VS Code /// terminal), it creates a new virtual destkop and a transition animation. /// This animation takes one second and cannot be disable without /// elevated privileges. In this animation time, all toggleFullscreen events /// will be failed. In this implementation, we will try again by using /// performSelector:withObject:afterDelay: until window_did_enter_fullscreen. /// It should be fine as we only do this at initialzation (i.e with_fullscreen /// was set). /// /// From Apple doc: /// In some cases, the transition to enter full-screen mode can fail, /// due to being in the midst of handling some other animation or user gesture. /// This method indicates that there was an error, and you should clean up any /// work you may have done to prepare to enter full-screen mode. extern "C" fn window_did_fail_to_enter_fullscreen(this: &Object, _: Sel, _: id) { trace!("Triggered `windowDidFailToEnterFullscreen:`"); with_state(this, |state| { 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:`"); }