//! AppKit FFI bindings for macOS window creation. //! //! Provides wrappers around NSApplication, NSWindow, NSAutoreleasePool, and //! NSView for opening native macOS windows. //! //! # Safety //! //! This module contains `unsafe` code for FFI with AppKit. //! The `platform` crate is one of the few crates where `unsafe` is permitted. use crate::cf::CfString; use crate::cg::{self, BitmapContext, CGRect}; use crate::objc::{Class, Id, Imp, Sel}; use crate::{class, msg_send}; use std::ffi::CStr; use std::os::raw::{c_char, c_void}; // --------------------------------------------------------------------------- // AppKit framework link // --------------------------------------------------------------------------- #[link(name = "AppKit", kind = "framework")] extern "C" {} // --------------------------------------------------------------------------- // Geometry types matching AppKit's expectations // --------------------------------------------------------------------------- /// `NSRect` / `CGRect` — a rectangle defined by origin and size. #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct NSRect { pub origin: NSPoint, pub size: NSSize, } /// `NSPoint` / `CGPoint` — a point in 2D space. #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct NSPoint { pub x: f64, pub y: f64, } /// `NSSize` / `CGSize` — a 2D size. #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct NSSize { pub width: f64, pub height: f64, } impl NSRect { /// Create a new rectangle. pub fn new(x: f64, y: f64, width: f64, height: f64) -> NSRect { NSRect { origin: NSPoint { x, y }, size: NSSize { width, height }, } } } // --------------------------------------------------------------------------- // NSWindow style mask constants // --------------------------------------------------------------------------- /// Window has a title bar. pub const NS_WINDOW_STYLE_MASK_TITLED: u64 = 1 << 0; /// Window has a close button. pub const NS_WINDOW_STYLE_MASK_CLOSABLE: u64 = 1 << 1; /// Window can be minimized. pub const NS_WINDOW_STYLE_MASK_MINIATURIZABLE: u64 = 1 << 2; /// Window can be resized. pub const NS_WINDOW_STYLE_MASK_RESIZABLE: u64 = 1 << 3; // --------------------------------------------------------------------------- // NSBackingStoreType constants // --------------------------------------------------------------------------- /// Buffered backing store (the standard for modern macOS). pub const NS_BACKING_STORE_BUFFERED: u64 = 2; // --------------------------------------------------------------------------- // NSApplicationActivationPolicy constants // --------------------------------------------------------------------------- /// Regular application that appears in the Dock and may have a menu bar. pub const NS_APPLICATION_ACTIVATION_POLICY_REGULAR: i64 = 0; // --------------------------------------------------------------------------- // NSAutoreleasePool // --------------------------------------------------------------------------- /// RAII wrapper for `NSAutoreleasePool`. /// /// Creates a pool on construction and drains it on drop. Required for any /// Objective-C code that creates autoreleased objects. pub struct AutoreleasePool { pool: Id, } impl Default for AutoreleasePool { fn default() -> Self { Self::new() } } impl AutoreleasePool { /// Create a new autorelease pool. pub fn new() -> AutoreleasePool { let cls = class!("NSAutoreleasePool").expect("NSAutoreleasePool class not found"); let pool: *mut c_void = msg_send![cls.as_ptr(), alloc]; let pool: *mut c_void = msg_send![pool, init]; let pool = unsafe { Id::from_raw(pool as *mut _) }.expect("NSAutoreleasePool init failed"); AutoreleasePool { pool } } } impl Drop for AutoreleasePool { fn drop(&mut self) { let _: *mut c_void = msg_send![self.pool.as_ptr(), drain]; } } // --------------------------------------------------------------------------- // NSApplication wrapper // --------------------------------------------------------------------------- /// Wrapper around `NSApplication`. pub struct App { app: Id, } impl App { /// Get the shared `NSApplication` instance. /// /// Must be called from the main thread. Creates the application object /// if it doesn't already exist. pub fn shared() -> App { let cls = class!("NSApplication").expect("NSApplication class not found"); let app: *mut c_void = msg_send![cls.as_ptr(), sharedApplication]; let app = unsafe { Id::from_raw(app as *mut _) }.expect("sharedApplication returned nil"); App { app } } /// Set the application's activation policy. /// /// Use [`NS_APPLICATION_ACTIVATION_POLICY_REGULAR`] for a normal app that /// appears in the Dock. pub fn set_activation_policy(&self, policy: i64) { let _: bool = msg_send![self.app.as_ptr(), setActivationPolicy: policy]; } /// Activate the application, bringing it to the foreground. pub fn activate(&self) { let _: *mut c_void = msg_send![self.app.as_ptr(), activateIgnoringOtherApps: true]; } /// Start the application's main event loop. /// /// This method does **not** return under normal circumstances. pub fn run(&self) { let _: *mut c_void = msg_send![self.app.as_ptr(), run]; } /// Return the underlying Objective-C object. pub fn id(&self) -> Id { self.app } } // --------------------------------------------------------------------------- // NSWindow wrapper // --------------------------------------------------------------------------- /// Wrapper around `NSWindow`. pub struct Window { window: Id, } impl Window { /// Create a new window with the given content rect, style mask, and backing. /// /// # Arguments /// /// * `rect` — The content rectangle (position and size). /// * `style` — Bitwise OR of `NS_WINDOW_STYLE_MASK_*` constants. /// * `backing` — Backing store type (use [`NS_BACKING_STORE_BUFFERED`]). /// * `defer` — Whether to defer window device creation. pub fn new(rect: NSRect, style: u64, backing: u64, defer: bool) -> Window { let cls = class!("NSWindow").expect("NSWindow class not found"); let window: *mut c_void = msg_send![cls.as_ptr(), alloc]; let window: *mut c_void = msg_send![ window, initWithContentRect: rect, styleMask: style, backing: backing, defer: defer ]; let window = unsafe { Id::from_raw(window as *mut _) }.expect("NSWindow initWithContentRect failed"); Window { window } } /// Set the window's title. pub fn set_title(&self, title: &str) { let cf_title = CfString::new(title).expect("failed to create CFString for title"); // CFStringRef is toll-free bridged to NSString*. let _: *mut c_void = msg_send![self.window.as_ptr(), setTitle: cf_title.as_void_ptr()]; } /// Make the window the key window and bring it to the front. pub fn make_key_and_order_front(&self) { let _: *mut c_void = msg_send![self.window.as_ptr(), makeKeyAndOrderFront: std::ptr::null::()]; } /// Get the window's content view. pub fn content_view(&self) -> Id { let view: *mut c_void = msg_send![self.window.as_ptr(), contentView]; unsafe { Id::from_raw(view as *mut _) }.expect("contentView returned nil") } /// Set the window's content view. pub fn set_content_view(&self, view: &Id) { let _: *mut c_void = msg_send![self.window.as_ptr(), setContentView: view.as_ptr()]; } /// Set the window's delegate. pub fn set_delegate(&self, delegate: &Id) { let _: *mut c_void = msg_send![self.window.as_ptr(), setDelegate: delegate.as_ptr()]; } /// Enable or disable mouse-moved event delivery for this window. /// /// Must be set to `true` for `mouseMoved:` events to reach the view. pub fn set_accepts_mouse_moved_events(&self, accepts: bool) { let _: *mut c_void = msg_send![self.window.as_ptr(), setAcceptsMouseMovedEvents: accepts]; } /// Return the underlying Objective-C object. pub fn id(&self) -> Id { self.window } } // --------------------------------------------------------------------------- // BitmapView — custom NSView subclass for rendering a CG bitmap // --------------------------------------------------------------------------- /// The name of the ivar storing the BitmapContext pointer in WeView. const BITMAP_CTX_IVAR: &CStr = c"_bitmapCtx"; /// Register the `WeView` custom NSView subclass. /// /// The class overrides `drawRect:` to blit a CGImage from the associated /// BitmapContext into the view's graphics context, and `isFlipped` to use a /// top-left coordinate origin. fn register_we_view_class() { if class!("WeView").is_some() { return; } let superclass = class!("NSView").expect("NSView not found"); let view_class = Class::allocate(superclass, c"WeView", 0).expect("failed to allocate WeView class"); // Add an ivar to hold a raw pointer to a BitmapContext. // Size = 8 (pointer), alignment = 3 (log2(8)), type = "^v" (pointer to void). view_class.add_ivar(BITMAP_CTX_IVAR, 8, 3, c"^v"); // drawRect: extern "C" fn draw_rect(this: *mut c_void, _sel: *mut c_void, _dirty_rect: NSRect) { let this_id = unsafe { Id::from_raw(this as *mut _) }; let this_id = match this_id { Some(id) => id, None => return, }; // Get the BitmapContext pointer from our ivar. let ctx_ptr = unsafe { this_id.get_ivar(BITMAP_CTX_IVAR) }; if ctx_ptr.is_null() { return; } let bitmap_ctx = unsafe { &*(ctx_ptr as *const BitmapContext) }; // Create a CGImage from the bitmap context. let image = match bitmap_ctx.create_image() { Some(img) => img, None => return, }; // Get the view's bounds: [self bounds] let bounds: NSRect = msg_send![this, bounds]; // Get the current NSGraphicsContext: // [NSGraphicsContext currentContext] let gfx_ctx_cls = class!("NSGraphicsContext").expect("NSGraphicsContext not found"); let gfx_ctx: *mut c_void = msg_send![gfx_ctx_cls.as_ptr(), currentContext]; if gfx_ctx.is_null() { return; } // Get the CGContextRef: [gfxCtx CGContext] let cg_context: *mut c_void = msg_send![gfx_ctx, CGContext]; if cg_context.is_null() { return; } // Draw the image into the CG context. let rect = CGRect::new( bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height, ); unsafe { cg::draw_image_in_context(cg_context, rect, &image); } } let sel = Sel::register(c"drawRect:"); view_class.add_method( sel, unsafe { std::mem::transmute::<*const (), Imp>(draw_rect as *const ()) }, c"v@:{CGRect={CGPoint=dd}{CGSize=dd}}", ); // isFlipped -> YES (top-left origin) extern "C" fn is_flipped(_this: *mut c_void, _sel: *mut c_void) -> bool { true } let sel = Sel::register(c"isFlipped"); view_class.add_method( sel, unsafe { std::mem::transmute::<*const (), Imp>(is_flipped as *const ()) }, c"B@:", ); // acceptsFirstResponder -> YES (allows view to receive key events) extern "C" fn accepts_first_responder(_this: *mut c_void, _sel: *mut c_void) -> bool { true } let sel = Sel::register(c"acceptsFirstResponder"); view_class.add_method( sel, unsafe { std::mem::transmute::<*const (), Imp>(accepts_first_responder as *const ()) }, c"B@:", ); // keyDown: — log key character and keyCode to stdout extern "C" fn key_down(_this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { let chars: *mut c_void = msg_send![event, characters]; if chars.is_null() { return; } let utf8: *const c_char = msg_send![chars, UTF8String]; if utf8.is_null() { return; } let c_str = unsafe { CStr::from_ptr(utf8) }; let key_code: u16 = msg_send![event, keyCode]; if let Ok(s) = c_str.to_str() { println!("keyDown: '{}' (keyCode: {})", s, key_code); } } let sel = Sel::register(c"keyDown:"); view_class.add_method( sel, unsafe { std::mem::transmute::<*const (), Imp>(key_down as *const ()) }, c"v@:@", ); // mouseDown: — log mouse location to stdout extern "C" fn mouse_down(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { let raw_loc: NSPoint = msg_send![event, locationInWindow]; let loc: NSPoint = msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::()]; println!("mouseDown: ({:.1}, {:.1})", loc.x, loc.y); } let sel = Sel::register(c"mouseDown:"); view_class.add_method( sel, unsafe { std::mem::transmute::<*const (), Imp>(mouse_down as *const ()) }, c"v@:@", ); // mouseUp: — log mouse location to stdout extern "C" fn mouse_up(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { let raw_loc: NSPoint = msg_send![event, locationInWindow]; let loc: NSPoint = msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::()]; println!("mouseUp: ({:.1}, {:.1})", loc.x, loc.y); } let sel = Sel::register(c"mouseUp:"); view_class.add_method( sel, unsafe { std::mem::transmute::<*const (), Imp>(mouse_up as *const ()) }, c"v@:@", ); // mouseMoved: — log mouse location to stdout extern "C" fn mouse_moved(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { let raw_loc: NSPoint = msg_send![event, locationInWindow]; let loc: NSPoint = msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::()]; println!("mouseMoved: ({:.1}, {:.1})", loc.x, loc.y); } let sel = Sel::register(c"mouseMoved:"); view_class.add_method( sel, unsafe { std::mem::transmute::<*const (), Imp>(mouse_moved as *const ()) }, c"v@:@", ); view_class.register(); } /// A custom NSView backed by a [`BitmapContext`]. /// /// When the view needs to draw, it creates a `CGImage` from the bitmap /// context and blits it into the view's graphics context. /// /// The `BitmapContext` must outlive this view. The caller is responsible /// for ensuring this (typically by keeping the context in a `Box` alongside /// the view). pub struct BitmapView { view: Id, } impl BitmapView { /// Create a new `BitmapView` with the given frame and bitmap context. /// /// The `bitmap_ctx` pointer is stored in the view's instance variable. /// The caller must ensure the `BitmapContext` outlives this view. pub fn new(frame: NSRect, bitmap_ctx: &BitmapContext) -> BitmapView { register_we_view_class(); let cls = class!("WeView").expect("WeView class not found"); let view: *mut c_void = msg_send![cls.as_ptr(), alloc]; let view: *mut c_void = msg_send![view, initWithFrame: frame]; let view = unsafe { Id::from_raw(view as *mut _) }.expect("WeView initWithFrame failed"); // Store the BitmapContext pointer in the ivar. unsafe { view.set_ivar( BITMAP_CTX_IVAR, bitmap_ctx as *const BitmapContext as *mut c_void, ); } BitmapView { view } } /// Update the bitmap context pointer stored in the view. /// /// Call this when the bitmap context has been replaced (e.g., on resize). /// The new `BitmapContext` must outlive this view. pub fn update_bitmap(&self, bitmap_ctx: &BitmapContext) { unsafe { self.view.set_ivar( BITMAP_CTX_IVAR, bitmap_ctx as *const BitmapContext as *mut c_void, ); } } /// Request the view to redraw. /// /// Call this after modifying the bitmap context's pixels to /// schedule a redraw. pub fn set_needs_display(&self) { let _: *mut c_void = msg_send![self.view.as_ptr(), setNeedsDisplay: true]; } /// Return the underlying Objective-C view object. pub fn id(&self) -> Id { self.view } } // --------------------------------------------------------------------------- // Global resize handler // --------------------------------------------------------------------------- /// Global resize callback, called from `windowDidResize:` with the new /// content view dimensions (width, height) in points. /// /// # Safety /// /// Accessed only from the main thread (the AppKit event loop). static mut RESIZE_HANDLER: Option = None; /// Register a function to be called when the window is resized. /// /// The handler receives the new content view width and height in points. /// Only one handler can be active at a time; setting a new one replaces /// any previous handler. pub fn set_resize_handler(handler: fn(f64, f64)) { // SAFETY: Called from the main thread before `app.run()`. unsafe { RESIZE_HANDLER = Some(handler); } } // --------------------------------------------------------------------------- // Window delegate for handling resize and close events // --------------------------------------------------------------------------- /// Register the `WeWindowDelegate` class if not already registered. /// /// The class implements: /// - `windowDidResize:` — marks the content view as needing display /// - `windowShouldClose:` — returns YES (allows closing) fn register_we_window_delegate_class() { if class!("WeWindowDelegate").is_some() { return; } let superclass = class!("NSObject").expect("NSObject not found"); let delegate_class = Class::allocate(superclass, c"WeWindowDelegate", 0) .expect("failed to allocate WeWindowDelegate class"); // windowDidResize: — call resize handler and mark view as needing display extern "C" fn window_did_resize( _this: *mut c_void, _sel: *mut c_void, notification: *mut c_void, ) { let win: *mut c_void = msg_send![notification, object]; if win.is_null() { return; } let content_view: *mut c_void = msg_send![win, contentView]; if content_view.is_null() { return; } // Get the content view's bounds to determine new dimensions. let bounds: NSRect = msg_send![content_view, bounds]; // Call the resize handler if one has been registered. // SAFETY: We are on the main thread (AppKit event loop). unsafe { if let Some(handler) = RESIZE_HANDLER { handler(bounds.size.width, bounds.size.height); } } let _: *mut c_void = msg_send![content_view, setNeedsDisplay: true]; } let sel = Sel::register(c"windowDidResize:"); delegate_class.add_method( sel, unsafe { std::mem::transmute::<*const (), Imp>(window_did_resize as *const ()) }, c"v@:@", ); // windowShouldClose: -> YES extern "C" fn window_should_close( _this: *mut c_void, _sel: *mut c_void, _sender: *mut c_void, ) -> bool { true } let sel = Sel::register(c"windowShouldClose:"); delegate_class.add_method( sel, unsafe { std::mem::transmute::<*const (), Imp>(window_should_close as *const ()) }, c"B@:@", ); delegate_class.register(); } /// Install a window delegate that handles resize and close events. /// /// Creates a `WeWindowDelegate` class (if not already registered) and sets /// an instance as the window's delegate. The app delegate then terminates /// the app when the last window closes. pub fn install_window_delegate(window: &Window) { register_we_window_delegate_class(); let cls = class!("WeWindowDelegate").expect("WeWindowDelegate not found"); let delegate: *mut c_void = msg_send![cls.as_ptr(), alloc]; let delegate: *mut c_void = msg_send![delegate, init]; let delegate = unsafe { Id::from_raw(delegate as *mut _) }.expect("WeWindowDelegate init failed"); window.set_delegate(&delegate); } // --------------------------------------------------------------------------- // App delegate for handling window close -> app termination // --------------------------------------------------------------------------- /// Install an application delegate that terminates the app when the last /// window is closed. /// /// This creates a custom Objective-C class `WeAppDelegate` that implements /// `applicationShouldTerminateAfterLastWindowClosed:` returning `YES`. pub fn install_app_delegate(app: &App) { // Only register the delegate class once. if class!("WeAppDelegate").is_some() { // Already registered, just create an instance and set it. set_delegate(app); return; } let superclass = class!("NSObject").expect("NSObject not found"); let delegate_class = Class::allocate(superclass, c"WeAppDelegate", 0) .expect("failed to allocate WeAppDelegate class"); // applicationShouldTerminateAfterLastWindowClosed: extern "C" fn should_terminate_after_last_window_closed( _this: *mut c_void, _sel: *mut c_void, _app: *mut c_void, ) -> bool { true } let sel = Sel::register(c"applicationShouldTerminateAfterLastWindowClosed:"); delegate_class.add_method( sel, unsafe { std::mem::transmute::<*const (), Imp>( should_terminate_after_last_window_closed as *const (), ) }, c"B@:@", ); delegate_class.register(); set_delegate(app); } fn set_delegate(app: &App) { let cls = class!("WeAppDelegate").expect("WeAppDelegate not found"); let delegate: *mut c_void = msg_send![cls.as_ptr(), alloc]; let delegate: *mut c_void = msg_send![delegate, init]; let _: *mut c_void = msg_send![app.id().as_ptr(), setDelegate: delegate]; } // --------------------------------------------------------------------------- // Convenience: create a standard browser window // --------------------------------------------------------------------------- /// Create a standard window suitable for a browser. /// /// Returns a window with title bar, close, minimize, and resize controls, /// centered at (200, 200), sized 800x600. pub fn create_standard_window(title: &str) -> Window { let style = NS_WINDOW_STYLE_MASK_TITLED | NS_WINDOW_STYLE_MASK_CLOSABLE | NS_WINDOW_STYLE_MASK_MINIATURIZABLE | NS_WINDOW_STYLE_MASK_RESIZABLE; let rect = NSRect::new(200.0, 200.0, 800.0, 600.0); let window = Window::new(rect, style, NS_BACKING_STORE_BUFFERED, false); window.set_title(title); window } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; #[test] fn nsrect_new() { let rect = NSRect::new(10.0, 20.0, 300.0, 400.0); assert_eq!(rect.origin.x, 10.0); assert_eq!(rect.origin.y, 20.0); assert_eq!(rect.size.width, 300.0); assert_eq!(rect.size.height, 400.0); } #[test] fn style_mask_constants() { // Verify the constants match AppKit's expected values. assert_eq!(NS_WINDOW_STYLE_MASK_TITLED, 1); assert_eq!(NS_WINDOW_STYLE_MASK_CLOSABLE, 2); assert_eq!(NS_WINDOW_STYLE_MASK_MINIATURIZABLE, 4); assert_eq!(NS_WINDOW_STYLE_MASK_RESIZABLE, 8); } #[test] fn backing_store_constant() { assert_eq!(NS_BACKING_STORE_BUFFERED, 2); } #[test] fn activation_policy_constant() { assert_eq!(NS_APPLICATION_ACTIVATION_POLICY_REGULAR, 0); } #[test] fn autorelease_pool_create_and_drop() { // Creating and dropping an autorelease pool should not crash. let _pool = AutoreleasePool::new(); } #[test] fn combined_style_mask() { let style = NS_WINDOW_STYLE_MASK_TITLED | NS_WINDOW_STYLE_MASK_CLOSABLE | NS_WINDOW_STYLE_MASK_MINIATURIZABLE | NS_WINDOW_STYLE_MASK_RESIZABLE; assert_eq!(style, 0b1111); } #[test] fn we_view_class_registration() { register_we_view_class(); let cls = class!("WeView"); assert!(cls.is_some(), "WeView class should be registered"); } #[test] fn bitmap_view_create() { let _pool = AutoreleasePool::new(); let bitmap = BitmapContext::new(100, 100).expect("should create bitmap context"); let frame = NSRect::new(0.0, 0.0, 100.0, 100.0); let view = BitmapView::new(frame, &bitmap); assert!(!view.id().as_ptr().is_null()); } #[test] fn we_view_accepts_first_responder() { let _pool = AutoreleasePool::new(); let bitmap = BitmapContext::new(100, 100).expect("should create bitmap context"); let frame = NSRect::new(0.0, 0.0, 100.0, 100.0); let view = BitmapView::new(frame, &bitmap); let accepts: bool = msg_send![view.id().as_ptr(), acceptsFirstResponder]; assert!(accepts, "WeView should accept first responder"); } #[test] fn we_view_responds_to_key_down() { let _pool = AutoreleasePool::new(); register_we_view_class(); let cls = class!("WeView").expect("WeView should be registered"); let sel = Sel::register(c"keyDown:"); let instances_respond: bool = msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()]; assert!(instances_respond, "WeView should respond to keyDown:"); } #[test] fn we_view_responds_to_mouse_events() { let _pool = AutoreleasePool::new(); register_we_view_class(); let cls = class!("WeView").expect("WeView should be registered"); for sel_name in [c"mouseDown:", c"mouseUp:", c"mouseMoved:"] { let sel = Sel::register(sel_name); let responds: bool = msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()]; assert!(responds, "WeView should respond to {:?}", sel_name); } } #[test] fn we_window_delegate_class_registration() { register_we_window_delegate_class(); let cls = class!("WeWindowDelegate"); assert!(cls.is_some(), "WeWindowDelegate class should be registered"); } #[test] fn we_window_delegate_responds_to_resize() { register_we_window_delegate_class(); let cls = class!("WeWindowDelegate").expect("WeWindowDelegate should be registered"); let sel = Sel::register(c"windowDidResize:"); let responds: bool = msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()]; assert!( responds, "WeWindowDelegate should respond to windowDidResize:" ); } }