web engine - experimental web browser

Implement AppKit window creation

Add platform/src/appkit.rs with:
- NSRect/NSPoint/NSSize geometry types matching AppKit's C layout
- NSWindow style mask and backing store constants
- NSApplicationActivationPolicy constants
- AutoreleasePool RAII wrapper (creates on new, drains on drop)
- App wrapper: sharedApplication, setActivationPolicy:,
activateIgnoringOtherApps:, run
- Window wrapper: initWithContentRect:styleMask:backing:defer:,
setTitle: (via toll-free bridged CfString), makeKeyAndOrderFront:,
contentView
- WeAppDelegate custom ObjC class implementing
applicationShouldTerminateAfterLastWindowClosed: -> YES
- create_standard_window() convenience (800x600, titled/closable/
miniaturizable/resizable)
- 6 unit tests covering geometry, constants, and autorelease pool

Update browser main.rs to open a native macOS window titled "we".
Closing the window terminates the application.

Also fix pre-existing clippy macro_metavars_in_unsafe lint in msg_send!
macro by binding metavariables to locals outside unsafe blocks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+395 -16
+14 -1
crates/browser/src/main.rs
··· 1 + use we_platform::appkit; 2 + 1 3 fn main() { 2 - println!("we: a web browser"); 4 + let _pool = appkit::AutoreleasePool::new(); 5 + 6 + let app = appkit::App::shared(); 7 + app.set_activation_policy(appkit::NS_APPLICATION_ACTIVATION_POLICY_REGULAR); 8 + 9 + appkit::install_app_delegate(&app); 10 + 11 + let window = appkit::create_standard_window("we"); 12 + window.make_key_and_order_front(); 13 + 14 + app.activate(); 15 + app.run(); 3 16 }
+350
crates/platform/src/appkit.rs
··· 1 + //! AppKit FFI bindings for macOS window creation. 2 + //! 3 + //! Provides wrappers around NSApplication, NSWindow, NSAutoreleasePool, and 4 + //! NSView for opening native macOS windows. 5 + //! 6 + //! # Safety 7 + //! 8 + //! This module contains `unsafe` code for FFI with AppKit. 9 + //! The `platform` crate is one of the few crates where `unsafe` is permitted. 10 + 11 + use crate::cf::CfString; 12 + use crate::objc::{Class, Id, Imp, Sel}; 13 + use crate::{class, msg_send}; 14 + use std::os::raw::c_void; 15 + 16 + // --------------------------------------------------------------------------- 17 + // AppKit framework link 18 + // --------------------------------------------------------------------------- 19 + 20 + #[link(name = "AppKit", kind = "framework")] 21 + extern "C" {} 22 + 23 + // --------------------------------------------------------------------------- 24 + // Geometry types matching AppKit's expectations 25 + // --------------------------------------------------------------------------- 26 + 27 + /// `NSRect` / `CGRect` — a rectangle defined by origin and size. 28 + #[repr(C)] 29 + #[derive(Debug, Clone, Copy)] 30 + pub struct NSRect { 31 + pub origin: NSPoint, 32 + pub size: NSSize, 33 + } 34 + 35 + /// `NSPoint` / `CGPoint` — a point in 2D space. 36 + #[repr(C)] 37 + #[derive(Debug, Clone, Copy)] 38 + pub struct NSPoint { 39 + pub x: f64, 40 + pub y: f64, 41 + } 42 + 43 + /// `NSSize` / `CGSize` — a 2D size. 44 + #[repr(C)] 45 + #[derive(Debug, Clone, Copy)] 46 + pub struct NSSize { 47 + pub width: f64, 48 + pub height: f64, 49 + } 50 + 51 + impl NSRect { 52 + /// Create a new rectangle. 53 + pub fn new(x: f64, y: f64, width: f64, height: f64) -> NSRect { 54 + NSRect { 55 + origin: NSPoint { x, y }, 56 + size: NSSize { width, height }, 57 + } 58 + } 59 + } 60 + 61 + // --------------------------------------------------------------------------- 62 + // NSWindow style mask constants 63 + // --------------------------------------------------------------------------- 64 + 65 + /// Window has a title bar. 66 + pub const NS_WINDOW_STYLE_MASK_TITLED: u64 = 1 << 0; 67 + /// Window has a close button. 68 + pub const NS_WINDOW_STYLE_MASK_CLOSABLE: u64 = 1 << 1; 69 + /// Window can be minimized. 70 + pub const NS_WINDOW_STYLE_MASK_MINIATURIZABLE: u64 = 1 << 2; 71 + /// Window can be resized. 72 + pub const NS_WINDOW_STYLE_MASK_RESIZABLE: u64 = 1 << 3; 73 + 74 + // --------------------------------------------------------------------------- 75 + // NSBackingStoreType constants 76 + // --------------------------------------------------------------------------- 77 + 78 + /// Buffered backing store (the standard for modern macOS). 79 + pub const NS_BACKING_STORE_BUFFERED: u64 = 2; 80 + 81 + // --------------------------------------------------------------------------- 82 + // NSApplicationActivationPolicy constants 83 + // --------------------------------------------------------------------------- 84 + 85 + /// Regular application that appears in the Dock and may have a menu bar. 86 + pub const NS_APPLICATION_ACTIVATION_POLICY_REGULAR: i64 = 0; 87 + 88 + // --------------------------------------------------------------------------- 89 + // NSAutoreleasePool 90 + // --------------------------------------------------------------------------- 91 + 92 + /// RAII wrapper for `NSAutoreleasePool`. 93 + /// 94 + /// Creates a pool on construction and drains it on drop. Required for any 95 + /// Objective-C code that creates autoreleased objects. 96 + pub struct AutoreleasePool { 97 + pool: Id, 98 + } 99 + 100 + impl Default for AutoreleasePool { 101 + fn default() -> Self { 102 + Self::new() 103 + } 104 + } 105 + 106 + impl AutoreleasePool { 107 + /// Create a new autorelease pool. 108 + pub fn new() -> AutoreleasePool { 109 + let cls = class!("NSAutoreleasePool").expect("NSAutoreleasePool class not found"); 110 + let pool: *mut c_void = msg_send![cls.as_ptr(), alloc]; 111 + let pool: *mut c_void = msg_send![pool, init]; 112 + let pool = unsafe { Id::from_raw(pool as *mut _) }.expect("NSAutoreleasePool init failed"); 113 + AutoreleasePool { pool } 114 + } 115 + } 116 + 117 + impl Drop for AutoreleasePool { 118 + fn drop(&mut self) { 119 + let _: *mut c_void = msg_send![self.pool.as_ptr(), drain]; 120 + } 121 + } 122 + 123 + // --------------------------------------------------------------------------- 124 + // NSApplication wrapper 125 + // --------------------------------------------------------------------------- 126 + 127 + /// Wrapper around `NSApplication`. 128 + pub struct App { 129 + app: Id, 130 + } 131 + 132 + impl App { 133 + /// Get the shared `NSApplication` instance. 134 + /// 135 + /// Must be called from the main thread. Creates the application object 136 + /// if it doesn't already exist. 137 + pub fn shared() -> App { 138 + let cls = class!("NSApplication").expect("NSApplication class not found"); 139 + let app: *mut c_void = msg_send![cls.as_ptr(), sharedApplication]; 140 + let app = unsafe { Id::from_raw(app as *mut _) }.expect("sharedApplication returned nil"); 141 + App { app } 142 + } 143 + 144 + /// Set the application's activation policy. 145 + /// 146 + /// Use [`NS_APPLICATION_ACTIVATION_POLICY_REGULAR`] for a normal app that 147 + /// appears in the Dock. 148 + pub fn set_activation_policy(&self, policy: i64) { 149 + let _: bool = msg_send![self.app.as_ptr(), setActivationPolicy: policy]; 150 + } 151 + 152 + /// Activate the application, bringing it to the foreground. 153 + pub fn activate(&self) { 154 + let _: *mut c_void = msg_send![self.app.as_ptr(), activateIgnoringOtherApps: true]; 155 + } 156 + 157 + /// Start the application's main event loop. 158 + /// 159 + /// This method does **not** return under normal circumstances. 160 + pub fn run(&self) { 161 + let _: *mut c_void = msg_send![self.app.as_ptr(), run]; 162 + } 163 + 164 + /// Return the underlying Objective-C object. 165 + pub fn id(&self) -> Id { 166 + self.app 167 + } 168 + } 169 + 170 + // --------------------------------------------------------------------------- 171 + // NSWindow wrapper 172 + // --------------------------------------------------------------------------- 173 + 174 + /// Wrapper around `NSWindow`. 175 + pub struct Window { 176 + window: Id, 177 + } 178 + 179 + impl Window { 180 + /// Create a new window with the given content rect, style mask, and backing. 181 + /// 182 + /// # Arguments 183 + /// 184 + /// * `rect` — The content rectangle (position and size). 185 + /// * `style` — Bitwise OR of `NS_WINDOW_STYLE_MASK_*` constants. 186 + /// * `backing` — Backing store type (use [`NS_BACKING_STORE_BUFFERED`]). 187 + /// * `defer` — Whether to defer window device creation. 188 + pub fn new(rect: NSRect, style: u64, backing: u64, defer: bool) -> Window { 189 + let cls = class!("NSWindow").expect("NSWindow class not found"); 190 + let window: *mut c_void = msg_send![cls.as_ptr(), alloc]; 191 + let window: *mut c_void = msg_send![ 192 + window, 193 + initWithContentRect: rect, 194 + styleMask: style, 195 + backing: backing, 196 + defer: defer 197 + ]; 198 + let window = 199 + unsafe { Id::from_raw(window as *mut _) }.expect("NSWindow initWithContentRect failed"); 200 + Window { window } 201 + } 202 + 203 + /// Set the window's title. 204 + pub fn set_title(&self, title: &str) { 205 + let cf_title = CfString::new(title).expect("failed to create CFString for title"); 206 + // CFStringRef is toll-free bridged to NSString*. 207 + let _: *mut c_void = msg_send![self.window.as_ptr(), setTitle: cf_title.as_void_ptr()]; 208 + } 209 + 210 + /// Make the window the key window and bring it to the front. 211 + pub fn make_key_and_order_front(&self) { 212 + let _: *mut c_void = 213 + msg_send![self.window.as_ptr(), makeKeyAndOrderFront: std::ptr::null::<c_void>()]; 214 + } 215 + 216 + /// Get the window's content view. 217 + pub fn content_view(&self) -> Id { 218 + let view: *mut c_void = msg_send![self.window.as_ptr(), contentView]; 219 + unsafe { Id::from_raw(view as *mut _) }.expect("contentView returned nil") 220 + } 221 + 222 + /// Return the underlying Objective-C object. 223 + pub fn id(&self) -> Id { 224 + self.window 225 + } 226 + } 227 + 228 + // --------------------------------------------------------------------------- 229 + // App delegate for handling window close -> app termination 230 + // --------------------------------------------------------------------------- 231 + 232 + /// Install an application delegate that terminates the app when the last 233 + /// window is closed. 234 + /// 235 + /// This creates a custom Objective-C class `WeAppDelegate` that implements 236 + /// `applicationShouldTerminateAfterLastWindowClosed:` returning `YES`. 237 + pub fn install_app_delegate(app: &App) { 238 + // Only register the delegate class once. 239 + if class!("WeAppDelegate").is_some() { 240 + // Already registered, just create an instance and set it. 241 + set_delegate(app); 242 + return; 243 + } 244 + 245 + let superclass = class!("NSObject").expect("NSObject not found"); 246 + let delegate_class = Class::allocate(superclass, c"WeAppDelegate", 0) 247 + .expect("failed to allocate WeAppDelegate class"); 248 + 249 + // applicationShouldTerminateAfterLastWindowClosed: 250 + extern "C" fn should_terminate_after_last_window_closed( 251 + _this: *mut c_void, 252 + _sel: *mut c_void, 253 + _app: *mut c_void, 254 + ) -> bool { 255 + true 256 + } 257 + 258 + let sel = Sel::register(c"applicationShouldTerminateAfterLastWindowClosed:"); 259 + delegate_class.add_method( 260 + sel, 261 + unsafe { 262 + std::mem::transmute::<*const (), Imp>( 263 + should_terminate_after_last_window_closed as *const (), 264 + ) 265 + }, 266 + c"B@:@", 267 + ); 268 + 269 + delegate_class.register(); 270 + set_delegate(app); 271 + } 272 + 273 + fn set_delegate(app: &App) { 274 + let cls = class!("WeAppDelegate").expect("WeAppDelegate not found"); 275 + let delegate: *mut c_void = msg_send![cls.as_ptr(), alloc]; 276 + let delegate: *mut c_void = msg_send![delegate, init]; 277 + let _: *mut c_void = msg_send![app.id().as_ptr(), setDelegate: delegate]; 278 + } 279 + 280 + // --------------------------------------------------------------------------- 281 + // Convenience: create a standard browser window 282 + // --------------------------------------------------------------------------- 283 + 284 + /// Create a standard window suitable for a browser. 285 + /// 286 + /// Returns a window with title bar, close, minimize, and resize controls, 287 + /// centered at (200, 200), sized 800x600. 288 + pub fn create_standard_window(title: &str) -> Window { 289 + let style = NS_WINDOW_STYLE_MASK_TITLED 290 + | NS_WINDOW_STYLE_MASK_CLOSABLE 291 + | NS_WINDOW_STYLE_MASK_MINIATURIZABLE 292 + | NS_WINDOW_STYLE_MASK_RESIZABLE; 293 + 294 + let rect = NSRect::new(200.0, 200.0, 800.0, 600.0); 295 + let window = Window::new(rect, style, NS_BACKING_STORE_BUFFERED, false); 296 + window.set_title(title); 297 + window 298 + } 299 + 300 + // --------------------------------------------------------------------------- 301 + // Tests 302 + // --------------------------------------------------------------------------- 303 + 304 + #[cfg(test)] 305 + mod tests { 306 + use super::*; 307 + 308 + #[test] 309 + fn nsrect_new() { 310 + let rect = NSRect::new(10.0, 20.0, 300.0, 400.0); 311 + assert_eq!(rect.origin.x, 10.0); 312 + assert_eq!(rect.origin.y, 20.0); 313 + assert_eq!(rect.size.width, 300.0); 314 + assert_eq!(rect.size.height, 400.0); 315 + } 316 + 317 + #[test] 318 + fn style_mask_constants() { 319 + // Verify the constants match AppKit's expected values. 320 + assert_eq!(NS_WINDOW_STYLE_MASK_TITLED, 1); 321 + assert_eq!(NS_WINDOW_STYLE_MASK_CLOSABLE, 2); 322 + assert_eq!(NS_WINDOW_STYLE_MASK_MINIATURIZABLE, 4); 323 + assert_eq!(NS_WINDOW_STYLE_MASK_RESIZABLE, 8); 324 + } 325 + 326 + #[test] 327 + fn backing_store_constant() { 328 + assert_eq!(NS_BACKING_STORE_BUFFERED, 2); 329 + } 330 + 331 + #[test] 332 + fn activation_policy_constant() { 333 + assert_eq!(NS_APPLICATION_ACTIVATION_POLICY_REGULAR, 0); 334 + } 335 + 336 + #[test] 337 + fn autorelease_pool_create_and_drop() { 338 + // Creating and dropping an autorelease pool should not crash. 339 + let _pool = AutoreleasePool::new(); 340 + } 341 + 342 + #[test] 343 + fn combined_style_mask() { 344 + let style = NS_WINDOW_STYLE_MASK_TITLED 345 + | NS_WINDOW_STYLE_MASK_CLOSABLE 346 + | NS_WINDOW_STYLE_MASK_MINIATURIZABLE 347 + | NS_WINDOW_STYLE_MASK_RESIZABLE; 348 + assert_eq!(style, 0b1111); 349 + } 350 + }
+1
crates/platform/src/lib.rs
··· 1 1 //! Minimal macOS platform layer — Obj-C FFI, AppKit, CoreGraphics, Metal. 2 2 3 + pub mod appkit; 3 4 pub mod cf; 4 5 pub mod objc;
+30 -15
crates/platform/src/objc.rs
··· 306 306 }); 307 307 let func: unsafe extern "C" fn(*mut std::os::raw::c_void, *mut std::os::raw::c_void) -> _ 308 308 = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; 309 - unsafe { func($receiver as *mut std::os::raw::c_void, sel.as_ptr() as *mut std::os::raw::c_void) } 309 + let receiver = $receiver as *mut std::os::raw::c_void; 310 + unsafe { func(receiver, sel.as_ptr() as *mut std::os::raw::c_void) } 310 311 }}; 311 312 312 313 // One argument: msg_send![receiver, selector: arg] ··· 320 321 *mut std::os::raw::c_void, 321 322 _, 322 323 ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; 324 + let receiver = $receiver as *mut std::os::raw::c_void; 325 + let arg = $arg; 323 326 unsafe { 324 327 func( 325 - $receiver as *mut std::os::raw::c_void, 328 + receiver, 326 329 sel.as_ptr() as *mut std::os::raw::c_void, 327 - $arg, 330 + arg, 328 331 ) 329 332 } 330 333 }}; ··· 341 344 _, 342 345 _, 343 346 ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; 347 + let receiver = $receiver as *mut std::os::raw::c_void; 348 + let arg1 = $arg1; 349 + let arg2 = $arg2; 344 350 unsafe { 345 351 func( 346 - $receiver as *mut std::os::raw::c_void, 352 + receiver, 347 353 sel.as_ptr() as *mut std::os::raw::c_void, 348 - $arg1, 349 - $arg2, 354 + arg1, 355 + arg2, 350 356 ) 351 357 } 352 358 }}; ··· 368 374 _, 369 375 _, 370 376 ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; 377 + let receiver = $receiver as *mut std::os::raw::c_void; 378 + let arg1 = $arg1; 379 + let arg2 = $arg2; 380 + let arg3 = $arg3; 371 381 unsafe { 372 382 func( 373 - $receiver as *mut std::os::raw::c_void, 383 + receiver, 374 384 sel.as_ptr() as *mut std::os::raw::c_void, 375 - $arg1, 376 - $arg2, 377 - $arg3, 385 + arg1, 386 + arg2, 387 + arg3, 378 388 ) 379 389 } 380 390 }}; ··· 398 408 _, 399 409 _, 400 410 ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; 411 + let receiver = $receiver as *mut std::os::raw::c_void; 412 + let arg1 = $arg1; 413 + let arg2 = $arg2; 414 + let arg3 = $arg3; 415 + let arg4 = $arg4; 401 416 unsafe { 402 417 func( 403 - $receiver as *mut std::os::raw::c_void, 418 + receiver, 404 419 sel.as_ptr() as *mut std::os::raw::c_void, 405 - $arg1, 406 - $arg2, 407 - $arg3, 408 - $arg4, 420 + arg1, 421 + arg2, 422 + arg3, 423 + arg4, 409 424 ) 410 425 } 411 426 }};