diff --git a/crates/browser/src/main.rs b/crates/browser/src/main.rs index 7ad856c..1bdd079 100644 --- a/crates/browser/src/main.rs +++ b/crates/browser/src/main.rs @@ -1,3 +1,16 @@ +use we_platform::appkit; + fn main() { - println!("we: a web browser"); + let _pool = appkit::AutoreleasePool::new(); + + let app = appkit::App::shared(); + app.set_activation_policy(appkit::NS_APPLICATION_ACTIVATION_POLICY_REGULAR); + + appkit::install_app_delegate(&app); + + let window = appkit::create_standard_window("we"); + window.make_key_and_order_front(); + + app.activate(); + app.run(); } diff --git a/crates/platform/src/appkit.rs b/crates/platform/src/appkit.rs new file mode 100644 index 0000000..667b759 --- /dev/null +++ b/crates/platform/src/appkit.rs @@ -0,0 +1,350 @@ +//! 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::objc::{Class, Id, Imp, Sel}; +use crate::{class, msg_send}; +use std::os::raw::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") + } + + /// Return the underlying Objective-C object. + pub fn id(&self) -> Id { + self.window + } +} + +// --------------------------------------------------------------------------- +// 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); + } +} diff --git a/crates/platform/src/lib.rs b/crates/platform/src/lib.rs index 1139cc8..ddd494c 100644 --- a/crates/platform/src/lib.rs +++ b/crates/platform/src/lib.rs @@ -1,4 +1,5 @@ //! Minimal macOS platform layer — Obj-C FFI, AppKit, CoreGraphics, Metal. +pub mod appkit; pub mod cf; pub mod objc; diff --git a/crates/platform/src/objc.rs b/crates/platform/src/objc.rs index 916d739..9732241 100644 --- a/crates/platform/src/objc.rs +++ b/crates/platform/src/objc.rs @@ -306,7 +306,8 @@ macro_rules! msg_send { }); let func: unsafe extern "C" fn(*mut std::os::raw::c_void, *mut std::os::raw::c_void) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; - unsafe { func($receiver as *mut std::os::raw::c_void, sel.as_ptr() as *mut std::os::raw::c_void) } + let receiver = $receiver as *mut std::os::raw::c_void; + unsafe { func(receiver, sel.as_ptr() as *mut std::os::raw::c_void) } }}; // One argument: msg_send![receiver, selector: arg] @@ -320,11 +321,13 @@ macro_rules! msg_send { *mut std::os::raw::c_void, _, ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; + let receiver = $receiver as *mut std::os::raw::c_void; + let arg = $arg; unsafe { func( - $receiver as *mut std::os::raw::c_void, + receiver, sel.as_ptr() as *mut std::os::raw::c_void, - $arg, + arg, ) } }}; @@ -341,12 +344,15 @@ macro_rules! msg_send { _, _, ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; + let receiver = $receiver as *mut std::os::raw::c_void; + let arg1 = $arg1; + let arg2 = $arg2; unsafe { func( - $receiver as *mut std::os::raw::c_void, + receiver, sel.as_ptr() as *mut std::os::raw::c_void, - $arg1, - $arg2, + arg1, + arg2, ) } }}; @@ -368,13 +374,17 @@ macro_rules! msg_send { _, _, ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; + let receiver = $receiver as *mut std::os::raw::c_void; + let arg1 = $arg1; + let arg2 = $arg2; + let arg3 = $arg3; unsafe { func( - $receiver as *mut std::os::raw::c_void, + receiver, sel.as_ptr() as *mut std::os::raw::c_void, - $arg1, - $arg2, - $arg3, + arg1, + arg2, + arg3, ) } }}; @@ -398,14 +408,19 @@ macro_rules! msg_send { _, _, ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; + let receiver = $receiver as *mut std::os::raw::c_void; + let arg1 = $arg1; + let arg2 = $arg2; + let arg3 = $arg3; + let arg4 = $arg4; unsafe { func( - $receiver as *mut std::os::raw::c_void, + receiver, sel.as_ptr() as *mut std::os::raw::c_void, - $arg1, - $arg2, - $arg3, - $arg4, + arg1, + arg2, + arg3, + arg4, ) } }};