//! Implements an `NSWindow` wrapper for MacOS, backed by //! Cocoa and associated widgets. This also handles looping back //! lifecycle events, such as window resizing or close events. use std::sync::{Once, ONCE_INIT}; use cocoa::base::{id, nil, YES, NO}; use cocoa::appkit::{NSWindow, NSWindowStyleMask, NSBackingStoreType}; use cocoa::foundation::{NSRect, NSPoint, NSSize, NSString, NSAutoreleasePool}; use objc_id::ShareId; use objc::declare::ClassDecl; use objc::runtime::{Class, Object, Sel}; use objc::{msg_send, sel, sel_impl}; use alchemy_lifecycle::traits::{AppDelegate, Component}; static APP_PTR: &str = "alchemyAppPtr"; static WINDOW_MANAGER_ID: &str = "alchemyWindowManagerID"; /// A wrapper for `NSWindow`. Holds (retains) pointers for the Objective-C runtime /// where our `NSWindow` and associated delegate live. pub struct Window { pub inner: ShareId, pub delegate: ShareId } impl Window { /// Creates a new `NSWindow` instance, configures it appropriately (e.g, titlebar appearance), /// injects an `NSObject` delegate wrapper, and retains the necessary Objective-C runtime /// pointers. pub fn new(window_id: usize, title: &str, dimensions: (f64, f64, f64, f64), content_view: &Component, app_ptr: *const T) -> Window { let (top, left, width, height) = dimensions; let dimensions = NSRect::new(NSPoint::new(top, left), NSSize::new(width, height)); let style = NSWindowStyleMask::NSResizableWindowMask | NSWindowStyleMask::NSUnifiedTitleAndToolbarWindowMask | NSWindowStyleMask::NSMiniaturizableWindowMask | NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSTitledWindowMask; let inner = unsafe { let window = NSWindow::alloc(nil).initWithContentRect_styleMask_backing_defer_( dimensions, style, NSBackingStoreType::NSBackingStoreBuffered, NO ).autorelease(); let title = NSString::alloc(nil).init_str(title); window.setTitle_(title); msg_send![window, setTitlebarAppearsTransparent:YES]; msg_send![window, setTitleVisibility:1]; // This is very important! NSWindow is an old class and has some behavior that we need // to disable, like... this. If we don't set this, we'll segfault entirely because the // Objective-C runtime gets out of sync. msg_send![window, setReleasedWhenClosed:NO]; if let Some(view_ptr) = content_view.borrow_native_backing_node() { msg_send![window, setContentView:view_ptr]; } ShareId::from_ptr(window) }; let delegate = unsafe { let delegate_class = register_window_class::(); let delegate: id = msg_send![delegate_class, new]; (&mut *delegate).set_ivar(APP_PTR, app_ptr as usize); (&mut *delegate).set_ivar(WINDOW_MANAGER_ID, window_id); msg_send![inner, setDelegate:delegate]; ShareId::from_ptr(delegate) }; Window { inner: inner, delegate: delegate } } /// On macOS, calling `show()` is equivalent to calling `makeKeyAndOrderFront`. This is the /// most common use case, hence why this method was chosen - if you want or need something /// else, feel free to open an issue to discuss. /// /// You should never be calling this yourself, mind you - Alchemy core handles this for you. pub fn show(&self) { unsafe { msg_send![&*self.inner, makeKeyAndOrderFront:nil]; } } /// On macOS, calling `close()` is equivalent to calling... well, `close`. It closes the /// window. /// /// I dunno what else to say here, lol. /// /// You should never be calling this yourself, mind you - Alchemy core handles this for you. pub fn close(&self) { unsafe { msg_send![&*self.inner, close]; } } } impl Drop for Window { /// When a Window is dropped on the Rust side, we want to ensure that we break the delegate /// link on the Objective-C side. While this shouldn't actually be an issue, I'd rather be /// safer than sorry. fn drop(&mut self) { // This bridging link needs to be broken on Drop. unsafe { msg_send![&*self.inner, setDelegate:nil]; } } } /// Called when a Window receives a `windowWillClose:` event. Loops back to the shared /// Alchemy app instance, so that our window manager can act appropriately. extern fn will_close(this: &Object, _: Sel, _: id) { unsafe { let app_ptr: usize = *this.get_ivar(APP_PTR); let window_id: usize = *this.get_ivar(WINDOW_MANAGER_ID); let app = app_ptr as *mut T; (*app)._window_will_close(window_id); }; } /// Injects an `NSObject` delegate subclass, with some callback and pointer ivars for what we /// need to do. fn register_window_class() -> *const Class { static mut DELEGATE_CLASS: *const Class = 0 as *const Class; static INIT: Once = ONCE_INIT; INIT.call_once(|| unsafe { let superclass = Class::get("NSObject").unwrap(); let mut decl = ClassDecl::new("alchemyWindowDelegateShim", superclass).unwrap(); decl.add_ivar::(APP_PTR); decl.add_ivar::(WINDOW_MANAGER_ID); decl.add_method(sel!(windowWillClose:), will_close:: as extern fn(&Object, _, _)); DELEGATE_CLASS = decl.register(); }); unsafe { DELEGATE_CLASS } }