Devised a better (safer) way to handle UI/Controller/Delegate setup

This commit is contained in:
Ryan McGrath 2020-03-11 17:56:17 -07:00
parent 80ba209413
commit f49eff24f9
No known key found for this signature in database
GPG key ID: 811674B62B666830
12 changed files with 385 additions and 374 deletions

View file

@ -17,14 +17,15 @@ use crate::menu::Menu;
mod events; mod events;
use events::register_app_class; use events::register_app_class;
pub trait Dispatcher {
pub trait AppDelegate {
type Message: Send + Sync; type Message: Send + Sync;
fn on_message(&self, _message: Self::Message) {}
}
pub trait AppDelegate {
fn did_finish_launching(&self) {} fn did_finish_launching(&self) {}
fn did_become_active(&self) {} fn did_become_active(&self) {}
fn on_message(&self, _message: Self::Message) {}
} }
/// A wrapper for `NSApplication`. It holds (retains) pointers for the Objective-C runtime, /// A wrapper for `NSApplication`. It holds (retains) pointers for the Objective-C runtime,
@ -60,7 +61,7 @@ impl App {
} }
} }
impl<T, M> App<T, M> where M: Send + Sync + 'static, T: AppDelegate<Message = M> { impl<T, M> App<T, M> where M: Send + Sync + 'static, T: AppDelegate + Dispatcher<Message = M> {
/// Dispatches a message by grabbing the `sharedApplication`, getting ahold of the delegate, /// Dispatches a message by grabbing the `sharedApplication`, getting ahold of the delegate,
/// and passing back through there. All messages are currently dispatched on the main thread. /// and passing back through there. All messages are currently dispatched on the main thread.
pub fn dispatch(message: M) { pub fn dispatch(message: M) {

View file

@ -8,3 +8,4 @@ pub(crate) static VIEW_CONTROLLER_PTR: &str = "rstViewControllerPtr";
pub(crate) static WEBVIEW_CONFIG_VAR: &str = "rstWebViewConfig"; pub(crate) static WEBVIEW_CONFIG_VAR: &str = "rstWebViewConfig";
pub(crate) static WEBVIEW_VAR: &str = "rstWebView"; pub(crate) static WEBVIEW_VAR: &str = "rstWebView";
pub(crate) static WEBVIEW_CONTROLLER_PTR: &str = "rstWebViewControllerPtr"; pub(crate) static WEBVIEW_CONTROLLER_PTR: &str = "rstWebViewControllerPtr";
pub(crate) static WINDOW_CONTROLLER_PTR: &str = "rstWindowController";

View file

@ -45,7 +45,7 @@ pub mod window;
pub use url; pub use url;
pub mod prelude { pub mod prelude {
pub use crate::app::{App, AppDelegate}; pub use crate::app::{App, AppDelegate, Dispatcher};
pub use crate::menu::{Menu, MenuItem}; pub use crate::menu::{Menu, MenuItem};
pub use crate::notifications::{Notification, NotificationCenter, NotificationAuthOption}; pub use crate::notifications::{Notification, NotificationCenter, NotificationAuthOption};
@ -54,14 +54,14 @@ pub mod prelude {
pub use crate::networking::URLRequest; pub use crate::networking::URLRequest;
pub use crate::window::{ pub use crate::window::{
Window, WindowWrapper as WinWrapper, WindowController Window, WindowController, WindowHandle
}; };
pub use crate::webview::{ pub use crate::webview::{
WebView, WebViewConfig, WebViewController WebView, WebViewConfig, WebViewController
}; };
pub use crate::view::{View, ViewController, ViewWrapper}; pub use crate::view::{View, ViewHandle, ViewController};
pub use appkit_derive::{ pub use appkit_derive::{
WindowWrapper, ViewWrapper WindowWrapper, ViewWrapper

View file

@ -7,6 +7,8 @@
//! for in the modern era. It also implements a few helpers for things like setting a background //! for in the modern era. It also implements a few helpers for things like setting a background
//! color, and enforcing layer backing by default. //! color, and enforcing layer backing by default.
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Once; use std::sync::Once;
use cocoa::base::{id, nil, YES, NO}; use cocoa::base::{id, nil, YES, NO};
@ -42,10 +44,19 @@ extern fn update_layer(this: &Object, _: Sel) {
extern fn dragging_entered<T: ViewController>(this: &mut Object, _: Sel, info: id) -> NSUInteger { extern fn dragging_entered<T: ViewController>(this: &mut Object, _: Sel, info: id) -> NSUInteger {
unsafe { unsafe {
let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR); let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR);
let view = ptr as *const T; let view_ptr = ptr as *const RefCell<T>;
(*view).dragging_entered(DragInfo { let view = Rc::from_raw(view_ptr);
let response = {
let v = view.borrow();
(*v).dragging_entered(DragInfo {
info: Id::from_ptr(info) info: Id::from_ptr(info)
}).into() }).into()
};
Rc::into_raw(view);
response
} }
} }
@ -53,14 +64,22 @@ extern fn dragging_entered<T: ViewController>(this: &mut Object, _: Sel, info: i
extern fn prepare_for_drag_operation<T: ViewController>(this: &mut Object, _: Sel, info: id) -> BOOL { extern fn prepare_for_drag_operation<T: ViewController>(this: &mut Object, _: Sel, info: id) -> BOOL {
unsafe { unsafe {
let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR); let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR);
let view = ptr as *const T; let view_ptr = ptr as *const RefCell<T>;
let view = Rc::from_raw(view_ptr);
match (*view).prepare_for_drag_operation(DragInfo { let response = {
let v = view.borrow();
match (*v).prepare_for_drag_operation(DragInfo {
info: Id::from_ptr(info) info: Id::from_ptr(info)
}) { }) {
true => YES, true => YES,
false => NO false => NO
} }
};
Rc::into_raw(view);
response
} }
} }
@ -68,14 +87,22 @@ extern fn prepare_for_drag_operation<T: ViewController>(this: &mut Object, _: Se
extern fn perform_drag_operation<T: ViewController>(this: &mut Object, _: Sel, info: id) -> BOOL { extern fn perform_drag_operation<T: ViewController>(this: &mut Object, _: Sel, info: id) -> BOOL {
unsafe { unsafe {
let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR); let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR);
let view = ptr as *const T; let view_ptr = ptr as *const RefCell<T>;
let view = Rc::from_raw(view_ptr);
match (*view).perform_drag_operation(DragInfo { let response = {
let v = view.borrow();
match (*v).perform_drag_operation(DragInfo {
info: Id::from_ptr(info) info: Id::from_ptr(info)
}) { }) {
true => YES, true => YES,
false => NO false => NO
} }
};
Rc::into_raw(view);
response
} }
} }
@ -83,11 +110,18 @@ extern fn perform_drag_operation<T: ViewController>(this: &mut Object, _: Sel, i
extern fn conclude_drag_operation<T: ViewController>(this: &mut Object, _: Sel, info: id) { extern fn conclude_drag_operation<T: ViewController>(this: &mut Object, _: Sel, info: id) {
unsafe { unsafe {
let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR); let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR);
let view = ptr as *const T; let view_ptr = ptr as *const RefCell<T>;
let view = Rc::from_raw(view_ptr);
(*view).conclude_drag_operation(DragInfo { let response = {
let v = view.borrow();
(*v).conclude_drag_operation(DragInfo {
info: Id::from_ptr(info) info: Id::from_ptr(info)
}); });
};
Rc::into_raw(view);
response
} }
} }
@ -95,10 +129,18 @@ extern fn conclude_drag_operation<T: ViewController>(this: &mut Object, _: Sel,
extern fn dragging_exited<T: ViewController>(this: &mut Object, _: Sel, info: id) { extern fn dragging_exited<T: ViewController>(this: &mut Object, _: Sel, info: id) {
unsafe { unsafe {
let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR); let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR);
let view = ptr as *const T; let view_ptr = ptr as *const RefCell<T>;
(*view).dragging_exited(DragInfo { let view = Rc::from_raw(view_ptr);
let response = {
let v = view.borrow();
(*v).dragging_exited(DragInfo {
info: Id::from_ptr(info) info: Id::from_ptr(info)
}); });
};
Rc::into_raw(view);
response
} }
} }

View file

@ -6,4 +6,4 @@ pub mod traits;
pub use traits::*; pub use traits::*;
pub mod view; pub mod view;
pub use view::View; pub use view::{View, ViewHandle};

View file

@ -5,13 +5,20 @@ use objc::runtime::Object;
use objc_id::ShareId; use objc_id::ShareId;
use crate::dragdrop::{DragInfo, DragOperation}; use crate::dragdrop::{DragInfo, DragOperation};
use crate::view::ViewHandle;
pub trait ViewWrapper { pub trait Node {
fn get_handle(&self) -> Option<ShareId<Object>>; fn get_backing_node(&self) -> Option<ShareId<Object>>;
} }
pub trait ViewController { pub trait ViewController {
fn did_load(&self); /// Where possible, we try to respect the lazy-ish-loading of macOS/iOS systems. This hook
/// notifies you when the `View` has actually loaded into memory, and you're free to do things
/// with it.
///
/// Note that you can trigger the view to load earlier, if need be... and in many cases it's
/// fine. This is a platform-specific detail you may want to read up on. :)
fn did_load(&mut self, _view: ViewHandle) {}
/// Invoked when the dragged image enters destination bounds or frame; returns dragging operation to perform. /// Invoked when the dragged image enters destination bounds or frame; returns dragging operation to perform.
fn dragging_entered(&self, _info: DragInfo) -> DragOperation { DragOperation::None } fn dragging_entered(&self, _info: DragInfo) -> DragOperation { DragOperation::None }

View file

@ -1,7 +1,8 @@
//! A wrapper for `NSViewController`. Uses interior mutability to //! A wrapper for `NSViewController`. Uses interior mutability to
use std::rc::Rc;
use std::cell::RefCell; use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
use cocoa::base::{id, nil, YES}; use cocoa::base::{id, nil, YES};
use cocoa::foundation::NSArray; use cocoa::foundation::NSArray;
@ -13,29 +14,30 @@ use objc::{msg_send, sel, sel_impl};
use crate::color::Color; use crate::color::Color;
use crate::constants::{BACKGROUND_COLOR, VIEW_CONTROLLER_PTR}; use crate::constants::{BACKGROUND_COLOR, VIEW_CONTROLLER_PTR};
use crate::pasteboard::PasteboardType; use crate::pasteboard::PasteboardType;
use crate::view::traits::ViewController; use crate::view::traits::{Node, ViewController};
use crate::view::controller::register_controller_class; use crate::view::controller::register_controller_class;
#[derive(Default)] /// A clone-able handler to a `ViewController` reference in the Objective C runtime. We use this
pub struct ViewInner { /// instead of a stock `View` for easier recordkeeping, since it'll need to hold the `View` on that
pub controller: Option<ShareId<Object>> /// side anyway.
} #[derive(Debug, Default, Clone)]
pub struct ViewHandle(Option<ShareId<Object>>);
impl ViewInner {
pub fn configure<T: ViewController + 'static>(&mut self, controller: &T) { impl ViewHandle {
self.controller = Some(unsafe { /// Call this to set the background color for the backing layer.
let view_controller: id = msg_send![register_controller_class::<T>(), new]; pub fn set_background_color(&self, color: Color) {
(&mut *view_controller).set_ivar(VIEW_CONTROLLER_PTR, controller as *const T as usize); if let Some(controller) = &self.0 {
unsafe {
let view: id = msg_send![view_controller, view]; let view: id = msg_send![*controller, view];
(&mut *view).set_ivar(VIEW_CONTROLLER_PTR, controller as *const T as usize); (*view).set_ivar(BACKGROUND_COLOR, color.into_platform_specific_color());
let _: () = msg_send![view, setNeedsDisplay:YES];
ShareId::from_ptr(view_controller) }
}); }
} }
/// Register this view for drag and drop operations.
pub fn register_for_dragged_types(&self, types: &[PasteboardType]) { pub fn register_for_dragged_types(&self, types: &[PasteboardType]) {
if let Some(controller) = &self.controller { if let Some(controller) = &self.0 {
unsafe { unsafe {
let types = NSArray::arrayWithObjects(nil, &types.iter().map(|t| { let types = NSArray::arrayWithObjects(nil, &types.iter().map(|t| {
t.to_nsstring() t.to_nsstring()
@ -46,49 +48,78 @@ impl ViewInner {
} }
} }
} }
}
/// A `View` wraps two different controllers - one on the Objective-C/Cocoa side, which forwards
/// calls into your supplied `ViewController` trait object. This involves heap allocation, but all
/// of Cocoa is essentially Heap'd, so... well, enjoy.
#[derive(Clone)]
pub struct View<T> {
internal_callback_ptr: *const RefCell<T>,
pub objc_controller: ViewHandle,
pub controller: Rc<RefCell<T>>
}
impl<T> View<T> where T: ViewController + 'static {
/// Allocates and configures a `ViewController` in the Objective-C/Cocoa runtime that maps over
/// to your supplied view controller.
pub fn new(controller: T) -> Self {
let controller = Rc::new(RefCell::new(controller));
let internal_callback_ptr = {
let cloned = Rc::clone(&controller);
Rc::into_raw(cloned)
};
let inner = unsafe {
let view_controller: id = msg_send![register_controller_class::<T>(), new];
(&mut *view_controller).set_ivar(VIEW_CONTROLLER_PTR, internal_callback_ptr as usize);
let view: id = msg_send![view_controller, view];
(&mut *view).set_ivar(VIEW_CONTROLLER_PTR, internal_callback_ptr as usize);
ShareId::from_ptr(view_controller)
};
{
let mut vc = controller.borrow_mut();
(*vc).did_load(ViewHandle(Some(inner.clone())));
}
View {
internal_callback_ptr: internal_callback_ptr,
objc_controller: ViewHandle(Some(inner)),
controller: controller
}
}
pub fn set_background_color(&self, color: Color) { pub fn set_background_color(&self, color: Color) {
if let Some(controller) = &self.controller { self.objc_controller.set_background_color(color);
unsafe {
let view: id = msg_send![*controller, view];
(*view).set_ivar(BACKGROUND_COLOR, color.into_platform_specific_color());
let _: () = msg_send![view, setNeedsDisplay:YES];
}
}
}
}
#[derive(Default)]
pub struct View(Rc<RefCell<ViewInner>>);
impl View {
pub fn configure<T: ViewController + 'static>(&self, controller: &T) {
{
let mut view = self.0.borrow_mut();
view.configure(controller);
}
controller.did_load();
}
pub fn get_handle(&self) -> Option<ShareId<Object>> {
let view = self.0.borrow();
view.controller.clone()
} }
pub fn register_for_dragged_types(&self, types: &[PasteboardType]) { pub fn register_for_dragged_types(&self, types: &[PasteboardType]) {
let view = self.0.borrow(); self.objc_controller.register_for_dragged_types(types);
view.register_for_dragged_types(types);
}
pub fn set_background_color(&self, color: Color) {
let view = self.0.borrow();
view.set_background_color(color);
} }
} }
impl std::fmt::Debug for View { impl<T> Node for View<T> {
/// Returns the Objective-C object used for handling the view heirarchy.
fn get_backing_node(&self) -> Option<ShareId<Object>> {
self.objc_controller.0.clone()
}
}
impl<T> std::fmt::Debug for View<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "View ({:p})", self) write!(f, "View ({:p})", self)
} }
} }
impl<T> Drop for View<T> {
/// A bit of extra cleanup for delegate callback pointers.
fn drop(&mut self) {
unsafe {
let _ = Rc::from_raw(self.internal_callback_ptr);
}
}
}

View file

@ -103,7 +103,7 @@ impl WebView {
webview.configure(controller); webview.configure(controller);
} }
controller.did_load(); //controller.did_load();
} }
pub fn get_handle(&self) -> Option<ShareId<Object>> { pub fn get_handle(&self) -> Option<ShareId<Object>> {

147
appkit/src/window/handle.rs Normal file
View file

@ -0,0 +1,147 @@
//! Implements `WindowHandle`, which wraps a lower-level `NSWindowController` and handles method
//! shuffling to call through to the window it holds.
//!
//! We use `NSWindowController` as it has lifecycle methods that are useful, in addition to the
//! standard `NSWindowDelegate` methods.
use cocoa::base::{id, nil, YES, NO};
use cocoa::foundation::{NSSize, NSString};
use objc::{msg_send, sel, sel_impl};
use objc::runtime::Object;
use objc_id::ShareId;
use crate::view::traits::Node;
//use crate::toolbar::traits::ToolbarDelegate;
#[derive(Debug, Default, Clone)]
pub struct WindowHandle(pub Option<ShareId<Object>>);
impl WindowHandle {
/// Handles setting the title on the underlying window. Allocates and passes an `NSString` over
/// to the Objective C runtime.
pub fn set_title(&self, title: &str) {
if let Some(controller) = &self.0 {
unsafe {
let title = NSString::alloc(nil).init_str(title);
let window: id = msg_send![*controller, window];
let _: () = msg_send![window, setTitle:title];
}
}
}
/// Sets the title visibility for the underlying window.
pub fn set_title_visibility(&self, visibility: usize) {
if let Some(controller) = &self.0 {
unsafe {
let window: id = msg_send![*controller, window];
let _: () = msg_send![window, setTitleVisibility:visibility];
}
}
}
/// Used for configuring whether the window is movable via the background.
pub fn set_movable_by_background(&self, movable: bool) {
if let Some(controller) = &self.0 {
unsafe {
let window: id = msg_send![*controller, window];
let _: () = msg_send![window, setMovableByWindowBackground:match movable {
true => YES,
false => NO
}];
}
}
}
/// Used for setting whether this titlebar appears transparent.
pub fn set_titlebar_appears_transparent(&self, transparent: bool) {
if let Some(controller) = &self.0 {
unsafe {
let window: id = msg_send![*controller, window];
let _: () = msg_send![window, setTitlebarAppearsTransparent:match transparent {
true => YES,
false => NO
}];
}
}
}
/// Used for setting this Window autosave name.
pub fn set_autosave_name(&mut self, name: &str) {
if let Some(controller) = &self.0 {
unsafe {
let window: id = msg_send![*controller, window];
let autosave = NSString::alloc(nil).init_str(name);
let _: () = msg_send![window, setFrameAutosaveName:autosave];
}
}
}
/// Sets the minimum size this window can shrink to.
pub fn set_minimum_content_size<F: Into<f64>>(&self, width: F, height: F) {
if let Some(controller) = &self.0 {
unsafe {
let size = NSSize::new(width.into(), height.into());
let window: id = msg_send![*controller, window];
let _: () = msg_send![window, setMinSize:size];
}
}
}
/// Used for setting a toolbar on this window. Note that this takes ownership of whatever
/// `ToolbarDelegate` you pass! The underlying `NSToolbar` is a bit... old, and it's just
/// easier to do things this way.
///
/// If you find yourself in a position where you need your toolbar after the fact, you
/// probably have bigger issues.
//pub fn set_toolbar<T: ToolbarDelegate + 'static>(&mut self, identifier: &str, toolbar: T) {
/*let toolbar = Toolbar::new(identifier, toolbar);
if let Some(controller) = &self.0 {
unsafe {
let window: id = msg_send![*controller, window];
let _: () = msg_send![window, setToolbar:&*toolbar.inner];
}
}
self.toolbar = Some(toolbar);*/
//}
/// Used for setting the content view controller for this window.
pub fn set_content_view_controller<T: Node + 'static>(&self, view_controller: &T) {
if let Some(controller) = &self.0 {
unsafe {
if let Some(vc) = view_controller.get_backing_node() {
let _: () = msg_send![*controller, setContentViewController:&*vc];
}
}
}
}
/// 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) {
if let Some(controller) = &self.0 {
unsafe {
let _: () = msg_send![*controller, showWindow:nil];
}
}
}
/// On macOS, calling `close()` is equivalent to calling... well, `close`. It closes the
/// window.
///
/// I dunno what else to say here, lol.
pub fn close(&self) {
if let Some(controller) = &self.0 {
unsafe {
let _: () = msg_send![*controller, close];
}
}
}
}

View file

@ -1,12 +1,15 @@
//! Implements wrappers and traits for `NSWindowController` and associated types. //! Implements wrappers and traits for `NSWindowController` and associated types.
pub mod traits; pub mod traits;
pub use traits::{WindowController, WindowWrapper}; pub use traits::WindowController;
mod controller; mod controller;
pub mod config; pub mod config;
pub use config::{WindowConfig, WindowStyle}; pub use config::{WindowConfig, WindowStyle};
pub mod handle;
pub use handle::WindowHandle;
pub mod window; pub mod window;
pub use window::{Window, WindowTitleVisibility}; pub use window::{Window, WindowTitleVisibility};

View file

@ -2,59 +2,9 @@
//! module. There's a few different ones, and it's just... cleaner, if //! module. There's a few different ones, and it's just... cleaner, if
//! it's organized here. //! it's organized here.
use crate::window::handle::WindowHandle;
use crate::window::WindowConfig; use crate::window::WindowConfig;
/// `WindowController` is a trait that handles providing higher level methods
/// that map into platform specific methods. Typically, you won't want to (or at least, won't need
/// to) implement this yourself - simply derive `WindowWrapper` and it'll work for you.
///
/// By deriving or implementing this, you get usable methods on your struct - for example:
///
/// ```
/// use appkit::{AppDelegate, Window, WindowController, WindowWrapper};
///
/// #[derive(Default, WindowWrapper)]
/// struct MyWindow {
/// window: Window
/// }
///
/// impl WindowController for MyWindow {
/// // The default implementation is actually okay!
/// }
///
/// #[derive(Default)]
/// struct MyApp {
/// window: MyWindow
/// }
///
/// impl AppDelegate for MyApp {
/// fn did_finish_launching(&mut self) {
/// window.show();
/// }
/// }
///
/// fn main() {
/// let app = App::new("com.myapp.lol", MyApp::default());
/// app.run();
/// }
/// ```
pub trait WindowWrapper {
/// Sets the title for the underlying window.
fn set_title(&self, title: &str);
/// Calls through to the NSWindow show method (technically, `[NSWindowController showWindow:]`.
/// Notable, this handles passing the implementing entity as the delegate, ensuring that
/// callbacks work appropriately.
///
/// We're technically setting the delegate later than is ideal, but in practice it works fine
/// in most cases due to the underlying implementation of `NSWindow` deferring things until
/// needed.
fn show(&self);
/// Calls through to the native NSwindow close implementation.
fn close(&self);
}
/// Lifecycle events for anything that `impl Window`'s. These map to the standard Cocoa /// Lifecycle events for anything that `impl Window`'s. These map to the standard Cocoa
/// lifecycle methods, but mix in a few extra things to handle offering configuration tools /// lifecycle methods, but mix in a few extra things to handle offering configuration tools
/// in lieu of subclasses. /// in lieu of subclasses.
@ -67,7 +17,7 @@ pub trait WindowController {
/// to set up your views and what not. /// to set up your views and what not.
/// ///
/// If you're coming from the web, you can think of this as `DOMContentLoaded`. /// If you're coming from the web, you can think of this as `DOMContentLoaded`.
fn did_load(&self) {} fn did_load(&mut self, _window: WindowHandle) {}
/// Fires when a window is going to close. You might opt to, say, clean up things here - /// Fires when a window is going to close. You might opt to, say, clean up things here -
/// perhaps you have a long running task, or something that should be removed. /// perhaps you have a long running task, or something that should be removed.

View file

@ -7,24 +7,15 @@ use std::cell::RefCell;
use cocoa::base::{id, nil, YES, NO}; use cocoa::base::{id, nil, YES, NO};
use cocoa::foundation::{NSSize, NSString}; use cocoa::foundation::{NSSize, NSString};
use objc_id::Id;
use objc::runtime::Object;
use objc::{msg_send, sel, sel_impl}; use objc::{msg_send, sel, sel_impl};
use objc_id::ShareId;
use crate::view::{ViewController, ViewWrapper}; use crate::constants::WINDOW_CONTROLLER_PTR;
use crate::toolbar::{Toolbar, ToolbarDelegate}; use crate::toolbar::{Toolbar, ToolbarDelegate};
use crate::window::WindowController; use crate::view::traits::Node;
use crate::window::controller::{register_window_controller_class}; use crate::window::handle::WindowHandle;
use crate::window::traits::WindowController;
static WINDOW_CONTROLLER_PTR: &str = "rstWindowController"; use crate::window::controller::register_window_controller_class;
/// A wrapper for `NSWindow`. Holds (retains) pointers for the Objective-C runtime
/// where our `NSWindow` and associated delegate live.
#[derive(Default)]
pub struct WindowInner {
pub controller: Option<Id<Object>>,
pub toolbar: Option<Toolbar>
}
pub enum WindowTitleVisibility { pub enum WindowTitleVisibility {
Visible, Visible,
@ -40,8 +31,18 @@ impl From<WindowTitleVisibility> for usize {
} }
} }
impl WindowInner { /// A `Window` represents your way of interacting with an `NSWindow`. It wraps the various moving
/// Configures the `NSWindow` to know about our delegate. /// pieces to enable you to focus on reacting to lifecycle methods and doing your thing.
#[derive(Clone)]
pub struct Window<T> {
internal_callback_ptr: *const RefCell<T>,
pub objc_controller: WindowHandle,
pub controller: Rc<RefCell<T>>
}
impl<T> Window<T> where T: WindowController + 'static {
/// Allocates and configures a `WindowController` in the Objective-C/Cocoa runtime that maps over
/// to your supplied controller.
/// ///
/// Now, you may look at this and go "hey, the hell is going on here - why don't you make the /// Now, you may look at this and go "hey, the hell is going on here - why don't you make the
/// `NSWindow` in `[NSWindowController loadWindow]`? /// `NSWindow` in `[NSWindowController loadWindow]`?
@ -54,246 +55,74 @@ impl WindowInner {
/// the route of implementing `loadView`. /// the route of implementing `loadView`.
/// ///
/// APPKIT! /// APPKIT!
pub fn configure<T: WindowController + 'static>(&mut self, window_controller: &T) { pub fn new(controller: T) -> Self {
let window = window_controller.config().0; let window = controller.config().0;
let controller = Rc::new(RefCell::new(controller));
self.controller = Some(unsafe { let internal_callback_ptr = {
let cloned = Rc::clone(&controller);
Rc::into_raw(cloned)
};
let inner = unsafe {
let window_controller_class = register_window_controller_class::<T>(); let window_controller_class = register_window_controller_class::<T>();
let controller_alloc: id = msg_send![window_controller_class, alloc]; let controller_alloc: id = msg_send![window_controller_class, alloc];
let controller: id = msg_send![controller_alloc, initWithWindow:window]; let controller: id = msg_send![controller_alloc, initWithWindow:window];
(&mut *controller).set_ivar(WINDOW_CONTROLLER_PTR, window_controller as *const T as usize); (&mut *controller).set_ivar(WINDOW_CONTROLLER_PTR, internal_callback_ptr as usize);
let window: id = msg_send![controller, window]; let window: id = msg_send![controller, window];
let _: () = msg_send![window, setDelegate:controller]; let _: () = msg_send![window, setDelegate:controller];
Id::from_ptr(controller) ShareId::from_ptr(controller)
}); };
{
let mut vc = controller.borrow_mut();
(*vc).did_load(WindowHandle(Some(inner.clone())));
} }
/// Handles setting the title on the underlying window. Allocates and passes an `NSString` over Window {
/// to the Objective C runtime. internal_callback_ptr: internal_callback_ptr,
pub fn set_title(&mut self, title: &str) { objc_controller: WindowHandle(Some(inner)),
if let Some(controller) = &self.controller { controller: controller
unsafe {
let title = NSString::alloc(nil).init_str(title);
let window: id = msg_send![*controller, window];
let _: () = msg_send![window, setTitle:title];
}
} }
} }
/// Sets the title visibility for the underlying window. /// Sets the title for this window.
pub fn set_title_visibility(&mut self, visibility: usize) {
if let Some(controller) = &self.controller {
unsafe {
let window: id = msg_send![*controller, window];
let _: () = msg_send![window, setTitleVisibility:visibility];
}
}
}
/// Used for configuring whether the window is movable via the background.
pub fn set_movable_by_background(&self, movable: bool) {
if let Some(controller) = &self.controller {
unsafe {
let window: id = msg_send![*controller, window];
let _: () = msg_send![window, setMovableByWindowBackground:match movable {
true => YES,
false => NO
}];
}
}
}
/// Used for setting whether this titlebar appears transparent.
pub fn set_titlebar_appears_transparent(&self, transparent: bool) {
if let Some(controller) = &self.controller {
unsafe {
let window: id = msg_send![*controller, window];
let _: () = msg_send![window, setTitlebarAppearsTransparent:match transparent {
true => YES,
false => NO
}];
}
}
}
/// Used for setting this Window autosave name.
pub fn set_autosave_name(&mut self, name: &str) {
if let Some(controller) = &self.controller {
unsafe {
let window: id = msg_send![*controller, window];
// Now we need to make sure to re-apply the NSAutoSaveName, as initWithWindow
// strips it... for some reason. We want it applied as it does nice things like
// save the window position in the Defaults database, which is what users expect.
let autosave = NSString::alloc(nil).init_str(name);
let _: () = msg_send![window, setFrameAutosaveName:autosave];
}
}
}
pub fn set_minimum_content_size<F: Into<f64>>(&self, width: F, height: F) {
if let Some(controller) = &self.controller {
unsafe {
let size = NSSize::new(width.into(), height.into());
let window: id = msg_send![*controller, window];
let _: () = msg_send![window, setMinSize:size];
}
}
}
/// Used for setting a toolbar on this window. Note that this takes ownership of whatever
/// `ToolbarDelegate` you pass! The underlying `NSToolbar` is a bit... old, and it's just
/// easier to do things this way.
///
/// If you find yourself in a position where you need your toolbar after the fact, you
/// probably have bigger issues.
pub fn set_toolbar<T: ToolbarDelegate + 'static>(&mut self, identifier: &str, toolbar: T) {
let toolbar = Toolbar::new(identifier, toolbar);
if let Some(controller) = &self.controller {
unsafe {
let window: id = msg_send![*controller, window];
let _: () = msg_send![window, setToolbar:&*toolbar.inner];
}
}
self.toolbar = Some(toolbar);
}
/// Used for setting the content view controller for this window.
pub fn set_content_view<T: ViewController + ViewWrapper + 'static>(&mut self, view_controller: &T) {
if let Some(controller) = &self.controller {
unsafe {
if let Some(vc) = view_controller.get_handle() {
let _: () = msg_send![*controller, setContentViewController:&*vc];
}
}
}
}
/// 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) {
if let Some(controller) = &self.controller {
unsafe {
let _: () = msg_send![*controller, showWindow:nil];
}
}
}
/// On macOS, calling `close()` is equivalent to calling... well, `close`. It closes the
/// window.
///
/// I dunno what else to say here, lol.
pub fn close(&self) {
if let Some(controller) = &self.controller {
unsafe {
let _: () = msg_send![*controller, close];
}
}
}
}
impl Drop for WindowInner {
/// 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) {
if let Some(controller) = &self.controller {
unsafe {
let window: id = msg_send![*controller, window];
let _: () = msg_send![window, setDelegate:nil];
}
}
}
}
/// A Window wraps `NSWindowController`, using interior mutability to handle configuration and calling
/// through to it.
///
/// Why `NSWindowController` and not `NSWindow`, you ask? The former has lifecycle events we're
/// interested in, the latter is... well, just the window.
#[derive(Default)]
pub struct Window(Rc<RefCell<WindowInner>>);
impl Window {
/// Sets the window title.
pub fn set_title(&self, title: &str) { pub fn set_title(&self, title: &str) {
let mut window = self.0.borrow_mut(); self.objc_controller.set_title(title.into());
window.set_title(title);
}
/// Sets the window title visibility.
pub fn set_title_visibility(&self, visibility: usize) {
let mut window = self.0.borrow_mut();
window.set_title_visibility(visibility);
}
/// Sets whether the window is movable by the background or not.
pub fn set_movable_by_background(&self, movable: bool) {
let window = self.0.borrow();
window.set_movable_by_background(movable);
}
/// Sets whether the titlebar appears transparent or not.
pub fn set_titlebar_appears_transparent(&self, transparent: bool) {
let window = self.0.borrow();
window.set_titlebar_appears_transparent(transparent);
}
/// Set the window autosave name, which preserves things like position across restarts.
pub fn set_autosave_name(&self, name: &str) {
let mut window = self.0.borrow_mut();
window.set_autosave_name(name);
}
/// Sets the window's smallest size it can shrink to.
pub fn set_minimum_content_size<F: Into<f64>>(&self, width: F, height: F) {
let window = self.0.borrow_mut();
window.set_minimum_content_size(width, height);
}
/// Sets the Toolbar for this window. Note that this takes ownership of the toolbar!
pub fn set_toolbar<T: ToolbarDelegate + 'static>(&self, identifier: &str, toolbar: T) {
let mut window = self.0.borrow_mut();
window.set_toolbar(identifier, toolbar);
} }
/// Sets the content view controller for the window. /// Sets the content view controller for the window.
pub fn set_content_view<T: ViewController + ViewWrapper + 'static>(&self, view: &T) { pub fn set_content_view_controller<VC: Node + 'static>(&self, view_controller: &VC) {
let mut window = self.0.borrow_mut(); self.objc_controller.set_content_view_controller(view_controller);
window.set_content_view(view);
} }
/// Shows the window, running a configuration pass if necessary. /// Shows the window, running a configuration pass if necessary.
pub fn show<T: WindowController + 'static>(&self, controller: &T) { pub fn show(&self) {
let did_load = { self.objc_controller.show();
let mut window = self.0.borrow_mut();
if window.controller.is_none() {
window.configure(controller);
true
} else {
false
}
};
if did_load {
controller.did_load();
}
let window = self.0.borrow();
window.show();
} }
/// Closes the window. /// Closes the window.
pub fn close(&self) { pub fn close(&self) {
let window = self.0.borrow(); self.objc_controller.close();
window.close(); }
}
impl<T> Drop for Window<T> {
/// 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.
///
/// We also clean up our loopback pointer that we use for callbacks.
fn drop(&mut self) {
unsafe {
if let Some(objc_controller) = &self.objc_controller.0 {
let window: id = msg_send![*objc_controller, window];
let _: () = msg_send![window, setDelegate:nil];
}
let _ = Rc::from_raw(self.internal_callback_ptr);
}
} }
} }