use std::{ ops::Deref, sync::{Mutex, Weak}, }; use cocoa::{ appkit::{CGFloat, NSScreen, NSWindow, NSWindowStyleMask}, base::{id, nil}, foundation::{NSPoint, NSSize, NSString}, }; use dispatch::Queue; use objc::foundation::is_main_thread; use objc::rc::autoreleasepool; use objc::runtime::Bool; use crate::{ dpi::LogicalSize, platform_impl::platform::{ ffi, util::IdRef, window::{SharedState, SharedStateMutexGuard}, }, }; // Unsafe wrapper type that allows us to dispatch things that aren't Send. // This should *only* be used to dispatch to the main queue. // While it is indeed not guaranteed that these types can safely be sent to // other threads, we know that they're safe to use on the main thread. struct MainThreadSafe(T); unsafe impl Send for MainThreadSafe {} impl Deref for MainThreadSafe { type Target = T; fn deref(&self) -> &T { &self.0 } } unsafe fn set_style_mask(ns_window: id, ns_view: id, mask: NSWindowStyleMask) { ns_window.setStyleMask_(mask); // If we don't do this, key handling will break // (at least until the window is clicked again/etc.) ns_window.makeFirstResponder_(ns_view); } // Always use this function instead of trying to modify `styleMask` directly! // `setStyleMask:` isn't thread-safe, so we have to use Grand Central Dispatch. // Otherwise, this would vomit out errors about not being on the main thread // and fail to do anything. pub unsafe fn set_style_mask_async(ns_window: id, ns_view: id, mask: NSWindowStyleMask) { let ns_window = MainThreadSafe(ns_window); let ns_view = MainThreadSafe(ns_view); Queue::main().exec_async(move || { set_style_mask(*ns_window, *ns_view, mask); }); } pub unsafe fn set_style_mask_sync(ns_window: id, ns_view: id, mask: NSWindowStyleMask) { if is_main_thread() { set_style_mask(ns_window, ns_view, mask); } else { let ns_window = MainThreadSafe(ns_window); let ns_view = MainThreadSafe(ns_view); Queue::main().exec_sync(move || { set_style_mask(*ns_window, *ns_view, mask); }) } } // `setContentSize:` isn't thread-safe either, though it doesn't log any errors // and just fails silently. Anyway, GCD to the rescue! pub unsafe fn set_content_size_async(ns_window: id, size: LogicalSize) { let ns_window = MainThreadSafe(ns_window); Queue::main().exec_async(move || { ns_window.setContentSize_(NSSize::new(size.width as CGFloat, size.height as CGFloat)); }); } // `setFrameTopLeftPoint:` isn't thread-safe, but fortunately has the courtesy // to log errors. pub unsafe fn set_frame_top_left_point_async(ns_window: id, point: NSPoint) { let ns_window = MainThreadSafe(ns_window); Queue::main().exec_async(move || { ns_window.setFrameTopLeftPoint_(point); }); } // `setFrameTopLeftPoint:` isn't thread-safe, and fails silently. pub unsafe fn set_level_async(ns_window: id, level: ffi::NSWindowLevel) { let ns_window = MainThreadSafe(ns_window); Queue::main().exec_async(move || { ns_window.setLevel_(level as _); }); } // `setIgnoresMouseEvents_:` isn't thread-safe, and fails silently. pub unsafe fn set_ignore_mouse_events(ns_window: id, ignore: bool) { let ns_window = MainThreadSafe(ns_window); Queue::main().exec_async(move || { ns_window.setIgnoresMouseEvents_(Bool::from(ignore).as_raw()); }); } // `toggleFullScreen` is thread-safe, but our additional logic to account for // window styles isn't. pub unsafe fn toggle_full_screen_async( ns_window: id, ns_view: id, not_fullscreen: bool, shared_state: Weak>, ) { let ns_window = MainThreadSafe(ns_window); let ns_view = MainThreadSafe(ns_view); let shared_state = MainThreadSafe(shared_state); Queue::main().exec_async(move || { // `toggleFullScreen` doesn't work if the `StyleMask` is none, so we // set a normal style temporarily. The previous state will be // restored in `WindowDelegate::window_did_exit_fullscreen`. if not_fullscreen { let curr_mask = ns_window.styleMask(); let required = NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSResizableWindowMask; if !curr_mask.contains(required) { set_style_mask(*ns_window, *ns_view, required); if let Some(shared_state) = shared_state.upgrade() { let mut shared_state_lock = SharedStateMutexGuard::new( shared_state.lock().unwrap(), "toggle_full_screen_callback", ); (*shared_state_lock).saved_style = Some(curr_mask); } } } // Window level must be restored from `CGShieldingWindowLevel() // + 1` back to normal in order for `toggleFullScreen` to do // anything ns_window.setLevel_(0); ns_window.toggleFullScreen_(nil); }); } pub unsafe fn restore_display_mode_async(ns_screen: u32) { Queue::main().exec_async(move || { ffi::CGRestorePermanentDisplayConfiguration(); assert_eq!(ffi::CGDisplayRelease(ns_screen), ffi::kCGErrorSuccess); }); } // `setMaximized` is not thread-safe pub unsafe fn set_maximized_async( ns_window: id, is_zoomed: bool, maximized: bool, shared_state: Weak>, ) { let ns_window = MainThreadSafe(ns_window); let shared_state = MainThreadSafe(shared_state); Queue::main().exec_async(move || { if let Some(shared_state) = shared_state.upgrade() { let mut shared_state_lock = SharedStateMutexGuard::new(shared_state.lock().unwrap(), "set_maximized"); // Save the standard frame sized if it is not zoomed if !is_zoomed { shared_state_lock.standard_frame = Some(NSWindow::frame(*ns_window)); } shared_state_lock.maximized = maximized; if shared_state_lock.fullscreen.is_some() { // Handle it in window_did_exit_fullscreen return; } if ns_window .styleMask() .contains(NSWindowStyleMask::NSResizableWindowMask) { // Just use the native zoom if resizable ns_window.zoom_(nil); } else { // if it's not resizable, we set the frame directly let new_rect = if maximized { let screen = NSScreen::mainScreen(nil); NSScreen::visibleFrame(screen) } else { shared_state_lock.saved_standard_frame() }; ns_window.setFrame_display_(new_rect, Bool::NO.as_raw()); } } }); } // `orderOut:` isn't thread-safe. Calling it from another thread actually works, // but with an odd delay. pub unsafe fn order_out_async(ns_window: id) { let ns_window = MainThreadSafe(ns_window); Queue::main().exec_async(move || { ns_window.orderOut_(nil); }); } // `makeKeyAndOrderFront:` isn't thread-safe. Calling it from another thread // actually works, but with an odd delay. pub unsafe fn make_key_and_order_front_async(ns_window: id) { let ns_window = MainThreadSafe(ns_window); Queue::main().exec_async(move || { ns_window.makeKeyAndOrderFront_(nil); }); } // `setTitle:` isn't thread-safe. Calling it from another thread invalidates the // window drag regions, which throws an exception when not done in the main // thread pub unsafe fn set_title_async(ns_window: id, title: String) { let ns_window = MainThreadSafe(ns_window); Queue::main().exec_async(move || { let title = IdRef::new(NSString::alloc(nil).init_str(&title)); ns_window.setTitle_(*title); }); } // `close:` is thread-safe, but we want the event to be triggered from the main // thread. Though, it's a good idea to look into that more... // // ArturKovacs: It's important that this operation keeps the underlying window alive // through the `IdRef` because otherwise it would dereference free'd memory pub unsafe fn close_async(ns_window: IdRef) { let ns_window = MainThreadSafe(ns_window); Queue::main().exec_async(move || { autoreleasepool(move |_| { ns_window.close(); }); }); }