we (web engine): Experimental web browser project to understand the limits of Claude
at texture-validation 1060 lines 37 kB view raw
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 11use crate::cf::CfString; 12use crate::cg::{self, BitmapContext, CGRect}; 13use crate::metal::{self, ClearColor, CommandQueue, Device, MetalLayer}; 14use crate::objc::{Class, Id, Imp, Sel}; 15use crate::{class, msg_send}; 16use std::ffi::CStr; 17use std::os::raw::{c_char, c_void}; 18 19// --------------------------------------------------------------------------- 20// AppKit framework link 21// --------------------------------------------------------------------------- 22 23#[link(name = "AppKit", kind = "framework")] 24extern "C" {} 25 26// --------------------------------------------------------------------------- 27// Geometry types matching AppKit's expectations 28// --------------------------------------------------------------------------- 29 30/// `NSRect` / `CGRect` — a rectangle defined by origin and size. 31#[repr(C)] 32#[derive(Debug, Clone, Copy)] 33pub struct NSRect { 34 pub origin: NSPoint, 35 pub size: NSSize, 36} 37 38/// `NSPoint` / `CGPoint` — a point in 2D space. 39#[repr(C)] 40#[derive(Debug, Clone, Copy)] 41pub struct NSPoint { 42 pub x: f64, 43 pub y: f64, 44} 45 46/// `NSSize` / `CGSize` — a 2D size. 47#[repr(C)] 48#[derive(Debug, Clone, Copy)] 49pub struct NSSize { 50 pub width: f64, 51 pub height: f64, 52} 53 54impl NSRect { 55 /// Create a new rectangle. 56 pub fn new(x: f64, y: f64, width: f64, height: f64) -> NSRect { 57 NSRect { 58 origin: NSPoint { x, y }, 59 size: NSSize { width, height }, 60 } 61 } 62} 63 64// --------------------------------------------------------------------------- 65// NSWindow style mask constants 66// --------------------------------------------------------------------------- 67 68/// Window has a title bar. 69pub const NS_WINDOW_STYLE_MASK_TITLED: u64 = 1 << 0; 70/// Window has a close button. 71pub const NS_WINDOW_STYLE_MASK_CLOSABLE: u64 = 1 << 1; 72/// Window can be minimized. 73pub const NS_WINDOW_STYLE_MASK_MINIATURIZABLE: u64 = 1 << 2; 74/// Window can be resized. 75pub const NS_WINDOW_STYLE_MASK_RESIZABLE: u64 = 1 << 3; 76 77// --------------------------------------------------------------------------- 78// NSBackingStoreType constants 79// --------------------------------------------------------------------------- 80 81/// Buffered backing store (the standard for modern macOS). 82pub const NS_BACKING_STORE_BUFFERED: u64 = 2; 83 84// --------------------------------------------------------------------------- 85// NSApplicationActivationPolicy constants 86// --------------------------------------------------------------------------- 87 88/// Regular application that appears in the Dock and may have a menu bar. 89pub const NS_APPLICATION_ACTIVATION_POLICY_REGULAR: i64 = 0; 90 91// --------------------------------------------------------------------------- 92// NSAutoreleasePool 93// --------------------------------------------------------------------------- 94 95/// RAII wrapper for `NSAutoreleasePool`. 96/// 97/// Creates a pool on construction and drains it on drop. Required for any 98/// Objective-C code that creates autoreleased objects. 99pub struct AutoreleasePool { 100 pool: Id, 101} 102 103impl Default for AutoreleasePool { 104 fn default() -> Self { 105 Self::new() 106 } 107} 108 109impl AutoreleasePool { 110 /// Create a new autorelease pool. 111 pub fn new() -> AutoreleasePool { 112 let cls = class!("NSAutoreleasePool").expect("NSAutoreleasePool class not found"); 113 let pool: *mut c_void = msg_send![cls.as_ptr(), alloc]; 114 let pool: *mut c_void = msg_send![pool, init]; 115 let pool = unsafe { Id::from_raw(pool as *mut _) }.expect("NSAutoreleasePool init failed"); 116 AutoreleasePool { pool } 117 } 118} 119 120impl Drop for AutoreleasePool { 121 fn drop(&mut self) { 122 let _: *mut c_void = msg_send![self.pool.as_ptr(), drain]; 123 } 124} 125 126// --------------------------------------------------------------------------- 127// NSApplication wrapper 128// --------------------------------------------------------------------------- 129 130/// Wrapper around `NSApplication`. 131pub struct App { 132 app: Id, 133} 134 135impl App { 136 /// Get the shared `NSApplication` instance. 137 /// 138 /// Must be called from the main thread. Creates the application object 139 /// if it doesn't already exist. 140 pub fn shared() -> App { 141 let cls = class!("NSApplication").expect("NSApplication class not found"); 142 let app: *mut c_void = msg_send![cls.as_ptr(), sharedApplication]; 143 let app = unsafe { Id::from_raw(app as *mut _) }.expect("sharedApplication returned nil"); 144 App { app } 145 } 146 147 /// Set the application's activation policy. 148 /// 149 /// Use [`NS_APPLICATION_ACTIVATION_POLICY_REGULAR`] for a normal app that 150 /// appears in the Dock. 151 pub fn set_activation_policy(&self, policy: i64) { 152 let _: bool = msg_send![self.app.as_ptr(), setActivationPolicy: policy]; 153 } 154 155 /// Activate the application, bringing it to the foreground. 156 pub fn activate(&self) { 157 let _: *mut c_void = msg_send![self.app.as_ptr(), activateIgnoringOtherApps: true]; 158 } 159 160 /// Start the application's main event loop. 161 /// 162 /// This method does **not** return under normal circumstances. 163 pub fn run(&self) { 164 let _: *mut c_void = msg_send![self.app.as_ptr(), run]; 165 } 166 167 /// Return the underlying Objective-C object. 168 pub fn id(&self) -> Id { 169 self.app 170 } 171} 172 173// --------------------------------------------------------------------------- 174// NSWindow wrapper 175// --------------------------------------------------------------------------- 176 177/// Wrapper around `NSWindow`. 178pub struct Window { 179 window: Id, 180} 181 182impl Window { 183 /// Create a new window with the given content rect, style mask, and backing. 184 /// 185 /// # Arguments 186 /// 187 /// * `rect` — The content rectangle (position and size). 188 /// * `style` — Bitwise OR of `NS_WINDOW_STYLE_MASK_*` constants. 189 /// * `backing` — Backing store type (use [`NS_BACKING_STORE_BUFFERED`]). 190 /// * `defer` — Whether to defer window device creation. 191 pub fn new(rect: NSRect, style: u64, backing: u64, defer: bool) -> Window { 192 let cls = class!("NSWindow").expect("NSWindow class not found"); 193 let window: *mut c_void = msg_send![cls.as_ptr(), alloc]; 194 let window: *mut c_void = msg_send![ 195 window, 196 initWithContentRect: rect, 197 styleMask: style, 198 backing: backing, 199 defer: defer 200 ]; 201 let window = 202 unsafe { Id::from_raw(window as *mut _) }.expect("NSWindow initWithContentRect failed"); 203 Window { window } 204 } 205 206 /// Set the window's title. 207 pub fn set_title(&self, title: &str) { 208 let cf_title = CfString::new(title).expect("failed to create CFString for title"); 209 // CFStringRef is toll-free bridged to NSString*. 210 let _: *mut c_void = msg_send![self.window.as_ptr(), setTitle: cf_title.as_void_ptr()]; 211 } 212 213 /// Make the window the key window and bring it to the front. 214 pub fn make_key_and_order_front(&self) { 215 let _: *mut c_void = 216 msg_send![self.window.as_ptr(), makeKeyAndOrderFront: std::ptr::null::<c_void>()]; 217 } 218 219 /// Get the window's content view. 220 pub fn content_view(&self) -> Id { 221 let view: *mut c_void = msg_send![self.window.as_ptr(), contentView]; 222 unsafe { Id::from_raw(view as *mut _) }.expect("contentView returned nil") 223 } 224 225 /// Set the window's content view. 226 pub fn set_content_view(&self, view: &Id) { 227 let _: *mut c_void = msg_send![self.window.as_ptr(), setContentView: view.as_ptr()]; 228 } 229 230 /// Set the window's delegate. 231 pub fn set_delegate(&self, delegate: &Id) { 232 let _: *mut c_void = msg_send![self.window.as_ptr(), setDelegate: delegate.as_ptr()]; 233 } 234 235 /// Enable or disable mouse-moved event delivery for this window. 236 /// 237 /// Must be set to `true` for `mouseMoved:` events to reach the view. 238 pub fn set_accepts_mouse_moved_events(&self, accepts: bool) { 239 let _: *mut c_void = msg_send![self.window.as_ptr(), setAcceptsMouseMovedEvents: accepts]; 240 } 241 242 /// Return the underlying Objective-C object. 243 pub fn id(&self) -> Id { 244 self.window 245 } 246} 247 248// --------------------------------------------------------------------------- 249// Shared NSView event handlers — used by both BitmapView and MetalView 250// --------------------------------------------------------------------------- 251 252/// `isFlipped` → YES (top-left origin). 253extern "C" fn view_is_flipped(_this: *mut c_void, _sel: *mut c_void) -> bool { 254 true 255} 256 257/// `acceptsFirstResponder` → YES (allows view to receive key events). 258extern "C" fn view_accepts_first_responder(_this: *mut c_void, _sel: *mut c_void) -> bool { 259 true 260} 261 262/// `keyDown:` — log key character and keyCode to stdout. 263extern "C" fn view_key_down(_this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { 264 let chars: *mut c_void = msg_send![event, characters]; 265 if chars.is_null() { 266 return; 267 } 268 let utf8: *const c_char = msg_send![chars, UTF8String]; 269 if utf8.is_null() { 270 return; 271 } 272 let c_str = unsafe { CStr::from_ptr(utf8) }; 273 let key_code: u16 = msg_send![event, keyCode]; 274 if let Ok(s) = c_str.to_str() { 275 println!("keyDown: '{}' (keyCode: {})", s, key_code); 276 } 277} 278 279/// `mouseDown:` — log mouse location to stdout. 280extern "C" fn view_mouse_down(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { 281 let raw_loc: NSPoint = msg_send![event, locationInWindow]; 282 let loc: NSPoint = 283 msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::<c_void>()]; 284 println!("mouseDown: ({:.1}, {:.1})", loc.x, loc.y); 285} 286 287/// `mouseUp:` — log mouse location to stdout. 288extern "C" fn view_mouse_up(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { 289 let raw_loc: NSPoint = msg_send![event, locationInWindow]; 290 let loc: NSPoint = 291 msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::<c_void>()]; 292 println!("mouseUp: ({:.1}, {:.1})", loc.x, loc.y); 293} 294 295/// `mouseMoved:` — log mouse location to stdout. 296extern "C" fn view_mouse_moved(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { 297 let raw_loc: NSPoint = msg_send![event, locationInWindow]; 298 let loc: NSPoint = 299 msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::<c_void>()]; 300 println!("mouseMoved: ({:.1}, {:.1})", loc.x, loc.y); 301} 302 303/// `scrollWheel:` — call scroll handler with delta and mouse location. 304extern "C" fn view_scroll_wheel(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { 305 let dx: f64 = msg_send![event, scrollingDeltaX]; 306 let dy: f64 = msg_send![event, scrollingDeltaY]; 307 let raw_loc: NSPoint = msg_send![event, locationInWindow]; 308 let loc: NSPoint = 309 msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::<c_void>()]; 310 // SAFETY: We are on the main thread (AppKit event loop). 311 unsafe { 312 if let Some(handler) = SCROLL_HANDLER { 313 handler(dx, dy, loc.x, loc.y); 314 } 315 } 316} 317 318/// Register the shared event handlers on an NSView subclass being built. 319/// 320/// Adds: `isFlipped`, `acceptsFirstResponder`, `keyDown:`, `mouseDown:`, 321/// `mouseUp:`, `mouseMoved:`, `scrollWheel:`. 322fn register_view_event_handlers(view_class: &Class) { 323 let sel = Sel::register(c"isFlipped"); 324 view_class.add_method( 325 sel, 326 unsafe { std::mem::transmute::<*const (), Imp>(view_is_flipped as *const ()) }, 327 c"B@:", 328 ); 329 330 let sel = Sel::register(c"acceptsFirstResponder"); 331 view_class.add_method( 332 sel, 333 unsafe { std::mem::transmute::<*const (), Imp>(view_accepts_first_responder as *const ()) }, 334 c"B@:", 335 ); 336 337 let sel = Sel::register(c"keyDown:"); 338 view_class.add_method( 339 sel, 340 unsafe { std::mem::transmute::<*const (), Imp>(view_key_down as *const ()) }, 341 c"v@:@", 342 ); 343 344 let sel = Sel::register(c"mouseDown:"); 345 view_class.add_method( 346 sel, 347 unsafe { std::mem::transmute::<*const (), Imp>(view_mouse_down as *const ()) }, 348 c"v@:@", 349 ); 350 351 let sel = Sel::register(c"mouseUp:"); 352 view_class.add_method( 353 sel, 354 unsafe { std::mem::transmute::<*const (), Imp>(view_mouse_up as *const ()) }, 355 c"v@:@", 356 ); 357 358 let sel = Sel::register(c"mouseMoved:"); 359 view_class.add_method( 360 sel, 361 unsafe { std::mem::transmute::<*const (), Imp>(view_mouse_moved as *const ()) }, 362 c"v@:@", 363 ); 364 365 let sel = Sel::register(c"scrollWheel:"); 366 view_class.add_method( 367 sel, 368 unsafe { std::mem::transmute::<*const (), Imp>(view_scroll_wheel as *const ()) }, 369 c"v@:@", 370 ); 371} 372 373// --------------------------------------------------------------------------- 374// BitmapView — custom NSView subclass for rendering a CG bitmap 375// --------------------------------------------------------------------------- 376 377/// The name of the ivar storing the BitmapContext pointer in WeView. 378const BITMAP_CTX_IVAR: &CStr = c"_bitmapCtx"; 379 380/// Register the `WeView` custom NSView subclass. 381/// 382/// The class overrides `drawRect:` to blit a CGImage from the associated 383/// BitmapContext into the view's graphics context, and `isFlipped` to use a 384/// top-left coordinate origin. 385fn register_we_view_class() { 386 if class!("WeView").is_some() { 387 return; 388 } 389 390 let superclass = class!("NSView").expect("NSView not found"); 391 let view_class = 392 Class::allocate(superclass, c"WeView", 0).expect("failed to allocate WeView class"); 393 394 // Add an ivar to hold a raw pointer to a BitmapContext. 395 // Size = 8 (pointer), alignment = 3 (log2(8)), type = "^v" (pointer to void). 396 view_class.add_ivar(BITMAP_CTX_IVAR, 8, 3, c"^v"); 397 398 // drawRect: 399 extern "C" fn draw_rect(this: *mut c_void, _sel: *mut c_void, _dirty_rect: NSRect) { 400 let this_id = unsafe { Id::from_raw(this as *mut _) }; 401 let this_id = match this_id { 402 Some(id) => id, 403 None => return, 404 }; 405 406 // Get the BitmapContext pointer from our ivar. 407 let ctx_ptr = unsafe { this_id.get_ivar(BITMAP_CTX_IVAR) }; 408 if ctx_ptr.is_null() { 409 return; 410 } 411 let bitmap_ctx = unsafe { &*(ctx_ptr as *const BitmapContext) }; 412 413 // Create a CGImage from the bitmap context. 414 let image = match bitmap_ctx.create_image() { 415 Some(img) => img, 416 None => return, 417 }; 418 419 // Get the view's bounds: [self bounds] 420 let bounds: NSRect = msg_send![this, bounds]; 421 422 // Get the current NSGraphicsContext: 423 // [NSGraphicsContext currentContext] 424 let gfx_ctx_cls = class!("NSGraphicsContext").expect("NSGraphicsContext not found"); 425 let gfx_ctx: *mut c_void = msg_send![gfx_ctx_cls.as_ptr(), currentContext]; 426 if gfx_ctx.is_null() { 427 return; 428 } 429 430 // Get the CGContextRef: [gfxCtx CGContext] 431 let cg_context: *mut c_void = msg_send![gfx_ctx, CGContext]; 432 if cg_context.is_null() { 433 return; 434 } 435 436 // Draw the image into the CG context. 437 let rect = CGRect::new( 438 bounds.origin.x, 439 bounds.origin.y, 440 bounds.size.width, 441 bounds.size.height, 442 ); 443 unsafe { 444 cg::draw_image_in_context(cg_context, rect, &image); 445 } 446 } 447 448 let sel = Sel::register(c"drawRect:"); 449 view_class.add_method( 450 sel, 451 unsafe { std::mem::transmute::<*const (), Imp>(draw_rect as *const ()) }, 452 c"v@:{CGRect={CGPoint=dd}{CGSize=dd}}", 453 ); 454 455 // Shared event handlers: isFlipped, acceptsFirstResponder, key/mouse/scroll. 456 register_view_event_handlers(&view_class); 457 458 view_class.register(); 459} 460 461/// A custom NSView backed by a [`BitmapContext`]. 462/// 463/// When the view needs to draw, it creates a `CGImage` from the bitmap 464/// context and blits it into the view's graphics context. 465/// 466/// The `BitmapContext` must outlive this view. The caller is responsible 467/// for ensuring this (typically by keeping the context in a `Box` alongside 468/// the view). 469pub struct BitmapView { 470 view: Id, 471} 472 473impl BitmapView { 474 /// Create a new `BitmapView` with the given frame and bitmap context. 475 /// 476 /// The `bitmap_ctx` pointer is stored in the view's instance variable. 477 /// The caller must ensure the `BitmapContext` outlives this view. 478 pub fn new(frame: NSRect, bitmap_ctx: &BitmapContext) -> BitmapView { 479 register_we_view_class(); 480 481 let cls = class!("WeView").expect("WeView class not found"); 482 let view: *mut c_void = msg_send![cls.as_ptr(), alloc]; 483 let view: *mut c_void = msg_send![view, initWithFrame: frame]; 484 let view = unsafe { Id::from_raw(view as *mut _) }.expect("WeView initWithFrame failed"); 485 486 // Store the BitmapContext pointer in the ivar. 487 unsafe { 488 view.set_ivar( 489 BITMAP_CTX_IVAR, 490 bitmap_ctx as *const BitmapContext as *mut c_void, 491 ); 492 } 493 494 BitmapView { view } 495 } 496 497 /// Update the bitmap context pointer stored in the view. 498 /// 499 /// Call this when the bitmap context has been replaced (e.g., on resize). 500 /// The new `BitmapContext` must outlive this view. 501 pub fn update_bitmap(&self, bitmap_ctx: &BitmapContext) { 502 unsafe { 503 self.view.set_ivar( 504 BITMAP_CTX_IVAR, 505 bitmap_ctx as *const BitmapContext as *mut c_void, 506 ); 507 } 508 } 509 510 /// Request the view to redraw. 511 /// 512 /// Call this after modifying the bitmap context's pixels to 513 /// schedule a redraw. 514 pub fn set_needs_display(&self) { 515 let _: *mut c_void = msg_send![self.view.as_ptr(), setNeedsDisplay: true]; 516 } 517 518 /// Return the underlying Objective-C view object. 519 pub fn id(&self) -> Id { 520 self.view 521 } 522} 523 524// --------------------------------------------------------------------------- 525// MetalView — NSView backed by a CAMetalLayer for GPU rendering 526// --------------------------------------------------------------------------- 527 528/// Ivar name for storing the MetalViewState pointer in WeMetalView. 529const METAL_STATE_IVAR: &CStr = c"_metalState"; 530 531/// Internal state for MetalView, stored as an ivar pointer. 532struct MetalViewState { 533 _device: Device, 534 queue: CommandQueue, 535 layer: MetalLayer, 536 clear_color: ClearColor, 537} 538 539/// Register the `WeMetalView` custom NSView subclass. 540/// 541/// The class overrides `makeBackingLayer` to provide a CAMetalLayer and 542/// `wantsUpdateLayer` to skip drawRect:-based rendering. Event handlers 543/// are shared with WeView. 544fn register_we_metal_view_class() { 545 if class!("WeMetalView").is_some() { 546 return; 547 } 548 549 let superclass = class!("NSView").expect("NSView not found"); 550 let view_class = Class::allocate(superclass, c"WeMetalView", 0) 551 .expect("failed to allocate WeMetalView class"); 552 553 // Ivar for the MetalViewState pointer. 554 view_class.add_ivar(METAL_STATE_IVAR, 8, 3, c"^v"); 555 556 // wantsLayer -> YES 557 extern "C" fn wants_layer(_this: *mut c_void, _sel: *mut c_void) -> bool { 558 true 559 } 560 561 let sel = Sel::register(c"wantsLayer"); 562 view_class.add_method( 563 sel, 564 unsafe { std::mem::transmute::<*const (), Imp>(wants_layer as *const ()) }, 565 c"B@:", 566 ); 567 568 // wantsUpdateLayer -> YES (use updateLayer instead of drawRect:) 569 extern "C" fn wants_update_layer(_this: *mut c_void, _sel: *mut c_void) -> bool { 570 true 571 } 572 573 let sel = Sel::register(c"wantsUpdateLayer"); 574 view_class.add_method( 575 sel, 576 unsafe { std::mem::transmute::<*const (), Imp>(wants_update_layer as *const ()) }, 577 c"B@:", 578 ); 579 580 // makeBackingLayer -> CAMetalLayer 581 extern "C" fn make_backing_layer(this: *mut c_void, _sel: *mut c_void) -> *mut c_void { 582 let this_id = match unsafe { Id::from_raw(this as *mut _) } { 583 Some(id) => id, 584 None => return std::ptr::null_mut(), 585 }; 586 587 let state_ptr = unsafe { this_id.get_ivar(METAL_STATE_IVAR) }; 588 if state_ptr.is_null() { 589 return std::ptr::null_mut(); 590 } 591 let state = unsafe { &*(state_ptr as *const MetalViewState) }; 592 state.layer.id().as_ptr() as *mut c_void 593 } 594 595 let sel = Sel::register(c"makeBackingLayer"); 596 view_class.add_method( 597 sel, 598 unsafe { std::mem::transmute::<*const (), Imp>(make_backing_layer as *const ()) }, 599 c"@@:", 600 ); 601 602 // updateLayer — render a clear-to-color pass via Metal 603 extern "C" fn update_layer(this: *mut c_void, _sel: *mut c_void) { 604 let this_id = match unsafe { Id::from_raw(this as *mut _) } { 605 Some(id) => id, 606 None => return, 607 }; 608 609 let state_ptr = unsafe { this_id.get_ivar(METAL_STATE_IVAR) }; 610 if state_ptr.is_null() { 611 return; 612 } 613 let state = unsafe { &*(state_ptr as *const MetalViewState) }; 614 615 // Get drawable 616 let drawable = match state.layer.next_drawable() { 617 Some(d) => d, 618 None => return, 619 }; 620 621 // Get drawable texture 622 let texture = match metal::drawable_texture(drawable) { 623 Some(t) => t, 624 None => return, 625 }; 626 627 // Create render pass descriptor 628 let desc = match metal::make_clear_pass_descriptor(texture, state.clear_color) { 629 Some(d) => d, 630 None => return, 631 }; 632 633 // Create command buffer 634 let cmd_buf = match state.queue.command_buffer() { 635 Some(b) => b, 636 None => return, 637 }; 638 639 // Create render encoder, immediately end (clear-only pass) 640 if let Some(encoder) = cmd_buf.render_command_encoder(desc) { 641 encoder.end_encoding(); 642 } 643 644 // Present and commit 645 cmd_buf.present_drawable(drawable); 646 cmd_buf.commit(); 647 } 648 649 let sel = Sel::register(c"updateLayer"); 650 view_class.add_method( 651 sel, 652 unsafe { std::mem::transmute::<*const (), Imp>(update_layer as *const ()) }, 653 c"v@:", 654 ); 655 656 // Shared event handlers: isFlipped, acceptsFirstResponder, key/mouse/scroll. 657 register_view_event_handlers(&view_class); 658 659 view_class.register(); 660} 661 662/// A Metal-backed NSView using `CAMetalLayer` for GPU rendering. 663/// 664/// Renders by clearing to a configurable background color. The Metal 665/// device, command queue, and layer are owned by the view's internal state. 666pub struct MetalView { 667 view: Id, 668 /// Boxed state — keeps Metal objects alive and provides `update_drawable_size`. 669 state: Box<MetalViewState>, 670} 671 672impl MetalView { 673 /// Create a new `MetalView` with the given frame and clear color. 674 /// 675 /// Returns `None` if Metal is not available on this system. 676 pub fn new(frame: NSRect, clear_color: ClearColor) -> Option<MetalView> { 677 register_we_metal_view_class(); 678 679 let device = Device::system_default()?; 680 let queue = device.new_command_queue()?; 681 let layer = MetalLayer::new()?; 682 683 layer.set_device(&device); 684 layer.set_pixel_format(metal::MTL_PIXEL_FORMAT_BGRA8_UNORM); 685 layer.set_framebuffer_only(true); 686 687 // Set initial drawable size (points × scale factor will be updated on 688 // first display, but set a sensible default). 689 layer.set_drawable_size(frame.size.width, frame.size.height); 690 691 let state = Box::new(MetalViewState { 692 _device: device, 693 queue, 694 layer, 695 clear_color, 696 }); 697 698 let cls = class!("WeMetalView").expect("WeMetalView class not found"); 699 let view: *mut c_void = msg_send![cls.as_ptr(), alloc]; 700 701 // Set the state ivar BEFORE initWithFrame so makeBackingLayer can read it. 702 let view_id = 703 unsafe { Id::from_raw(view as *mut _) }.expect("WeMetalView alloc returned nil"); 704 unsafe { 705 view_id.set_ivar( 706 METAL_STATE_IVAR, 707 &*state as *const MetalViewState as *mut c_void, 708 ); 709 } 710 711 let view: *mut c_void = msg_send![view_id.as_ptr(), initWithFrame: frame]; 712 let view_id = 713 unsafe { Id::from_raw(view as *mut _) }.expect("WeMetalView initWithFrame failed"); 714 715 Some(MetalView { 716 view: view_id, 717 state, 718 }) 719 } 720 721 /// Update the `CAMetalLayer` drawable size to match the view dimensions. 722 /// 723 /// Call this when the window is resized. The width and height should be 724 /// in pixels (points × backing scale factor). 725 pub fn update_drawable_size(&self, width: f64, height: f64) { 726 self.state.layer.set_drawable_size(width, height); 727 } 728 729 /// Request the view to redraw (triggers `updateLayer`). 730 pub fn set_needs_display(&self) { 731 let _: *mut c_void = msg_send![self.view.as_ptr(), setNeedsDisplay: true]; 732 } 733 734 /// Return the underlying Objective-C view object. 735 pub fn id(&self) -> Id { 736 self.view 737 } 738} 739 740// --------------------------------------------------------------------------- 741// Global resize handler 742// --------------------------------------------------------------------------- 743 744/// Global resize callback, called from `windowDidResize:` with the new 745/// content view dimensions (width, height) in points. 746/// 747/// # Safety 748/// 749/// Accessed only from the main thread (the AppKit event loop). 750static mut RESIZE_HANDLER: Option<fn(f64, f64)> = None; 751 752/// Register a function to be called when the window is resized. 753/// 754/// The handler receives the new content view width and height in points. 755/// Only one handler can be active at a time; setting a new one replaces 756/// any previous handler. 757pub fn set_resize_handler(handler: fn(f64, f64)) { 758 // SAFETY: Called from the main thread before `app.run()`. 759 unsafe { 760 RESIZE_HANDLER = Some(handler); 761 } 762} 763 764/// Global scroll callback, called from `scrollWheel:` with the scroll 765/// deltas (dx, dy) and mouse location (x, y) in view coordinates. 766/// 767/// # Safety 768/// 769/// Accessed only from the main thread (the AppKit event loop). 770static mut SCROLL_HANDLER: Option<fn(f64, f64, f64, f64)> = None; 771 772/// Register a function to be called when a scroll wheel event occurs. 773/// 774/// The handler receives `(delta_x, delta_y, mouse_x, mouse_y)`. 775/// Only one handler can be active at a time. 776pub fn set_scroll_handler(handler: fn(f64, f64, f64, f64)) { 777 // SAFETY: Called from the main thread before `app.run()`. 778 unsafe { 779 SCROLL_HANDLER = Some(handler); 780 } 781} 782 783// --------------------------------------------------------------------------- 784// Window delegate for handling resize and close events 785// --------------------------------------------------------------------------- 786 787/// Register the `WeWindowDelegate` class if not already registered. 788/// 789/// The class implements: 790/// - `windowDidResize:` — marks the content view as needing display 791/// - `windowShouldClose:` — returns YES (allows closing) 792fn register_we_window_delegate_class() { 793 if class!("WeWindowDelegate").is_some() { 794 return; 795 } 796 797 let superclass = class!("NSObject").expect("NSObject not found"); 798 let delegate_class = Class::allocate(superclass, c"WeWindowDelegate", 0) 799 .expect("failed to allocate WeWindowDelegate class"); 800 801 // windowDidResize: — call resize handler and mark view as needing display 802 extern "C" fn window_did_resize( 803 _this: *mut c_void, 804 _sel: *mut c_void, 805 notification: *mut c_void, 806 ) { 807 let win: *mut c_void = msg_send![notification, object]; 808 if win.is_null() { 809 return; 810 } 811 let content_view: *mut c_void = msg_send![win, contentView]; 812 if content_view.is_null() { 813 return; 814 } 815 // Get the content view's bounds to determine new dimensions. 816 let bounds: NSRect = msg_send![content_view, bounds]; 817 // Call the resize handler if one has been registered. 818 // SAFETY: We are on the main thread (AppKit event loop). 819 unsafe { 820 if let Some(handler) = RESIZE_HANDLER { 821 handler(bounds.size.width, bounds.size.height); 822 } 823 } 824 let _: *mut c_void = msg_send![content_view, setNeedsDisplay: true]; 825 } 826 827 let sel = Sel::register(c"windowDidResize:"); 828 delegate_class.add_method( 829 sel, 830 unsafe { std::mem::transmute::<*const (), Imp>(window_did_resize as *const ()) }, 831 c"v@:@", 832 ); 833 834 // windowShouldClose: -> YES 835 extern "C" fn window_should_close( 836 _this: *mut c_void, 837 _sel: *mut c_void, 838 _sender: *mut c_void, 839 ) -> bool { 840 true 841 } 842 843 let sel = Sel::register(c"windowShouldClose:"); 844 delegate_class.add_method( 845 sel, 846 unsafe { std::mem::transmute::<*const (), Imp>(window_should_close as *const ()) }, 847 c"B@:@", 848 ); 849 850 delegate_class.register(); 851} 852 853/// Install a window delegate that handles resize and close events. 854/// 855/// Creates a `WeWindowDelegate` class (if not already registered) and sets 856/// an instance as the window's delegate. The app delegate then terminates 857/// the app when the last window closes. 858pub fn install_window_delegate(window: &Window) { 859 register_we_window_delegate_class(); 860 861 let cls = class!("WeWindowDelegate").expect("WeWindowDelegate not found"); 862 let delegate: *mut c_void = msg_send![cls.as_ptr(), alloc]; 863 let delegate: *mut c_void = msg_send![delegate, init]; 864 let delegate = 865 unsafe { Id::from_raw(delegate as *mut _) }.expect("WeWindowDelegate init failed"); 866 window.set_delegate(&delegate); 867} 868 869// --------------------------------------------------------------------------- 870// App delegate for handling window close -> app termination 871// --------------------------------------------------------------------------- 872 873/// Install an application delegate that terminates the app when the last 874/// window is closed. 875/// 876/// This creates a custom Objective-C class `WeAppDelegate` that implements 877/// `applicationShouldTerminateAfterLastWindowClosed:` returning `YES`. 878pub fn install_app_delegate(app: &App) { 879 // Only register the delegate class once. 880 if class!("WeAppDelegate").is_some() { 881 // Already registered, just create an instance and set it. 882 set_delegate(app); 883 return; 884 } 885 886 let superclass = class!("NSObject").expect("NSObject not found"); 887 let delegate_class = Class::allocate(superclass, c"WeAppDelegate", 0) 888 .expect("failed to allocate WeAppDelegate class"); 889 890 // applicationShouldTerminateAfterLastWindowClosed: 891 extern "C" fn should_terminate_after_last_window_closed( 892 _this: *mut c_void, 893 _sel: *mut c_void, 894 _app: *mut c_void, 895 ) -> bool { 896 true 897 } 898 899 let sel = Sel::register(c"applicationShouldTerminateAfterLastWindowClosed:"); 900 delegate_class.add_method( 901 sel, 902 unsafe { 903 std::mem::transmute::<*const (), Imp>( 904 should_terminate_after_last_window_closed as *const (), 905 ) 906 }, 907 c"B@:@", 908 ); 909 910 delegate_class.register(); 911 set_delegate(app); 912} 913 914fn set_delegate(app: &App) { 915 let cls = class!("WeAppDelegate").expect("WeAppDelegate not found"); 916 let delegate: *mut c_void = msg_send![cls.as_ptr(), alloc]; 917 let delegate: *mut c_void = msg_send![delegate, init]; 918 let _: *mut c_void = msg_send![app.id().as_ptr(), setDelegate: delegate]; 919} 920 921// --------------------------------------------------------------------------- 922// Convenience: create a standard browser window 923// --------------------------------------------------------------------------- 924 925/// Create a standard window suitable for a browser. 926/// 927/// Returns a window with title bar, close, minimize, and resize controls, 928/// centered at (200, 200), sized 800x600. 929pub fn create_standard_window(title: &str) -> Window { 930 let style = NS_WINDOW_STYLE_MASK_TITLED 931 | NS_WINDOW_STYLE_MASK_CLOSABLE 932 | NS_WINDOW_STYLE_MASK_MINIATURIZABLE 933 | NS_WINDOW_STYLE_MASK_RESIZABLE; 934 935 let rect = NSRect::new(200.0, 200.0, 800.0, 600.0); 936 let window = Window::new(rect, style, NS_BACKING_STORE_BUFFERED, false); 937 window.set_title(title); 938 window 939} 940 941// --------------------------------------------------------------------------- 942// Tests 943// --------------------------------------------------------------------------- 944 945#[cfg(test)] 946mod tests { 947 use super::*; 948 949 #[test] 950 fn nsrect_new() { 951 let rect = NSRect::new(10.0, 20.0, 300.0, 400.0); 952 assert_eq!(rect.origin.x, 10.0); 953 assert_eq!(rect.origin.y, 20.0); 954 assert_eq!(rect.size.width, 300.0); 955 assert_eq!(rect.size.height, 400.0); 956 } 957 958 #[test] 959 fn style_mask_constants() { 960 // Verify the constants match AppKit's expected values. 961 assert_eq!(NS_WINDOW_STYLE_MASK_TITLED, 1); 962 assert_eq!(NS_WINDOW_STYLE_MASK_CLOSABLE, 2); 963 assert_eq!(NS_WINDOW_STYLE_MASK_MINIATURIZABLE, 4); 964 assert_eq!(NS_WINDOW_STYLE_MASK_RESIZABLE, 8); 965 } 966 967 #[test] 968 fn backing_store_constant() { 969 assert_eq!(NS_BACKING_STORE_BUFFERED, 2); 970 } 971 972 #[test] 973 fn activation_policy_constant() { 974 assert_eq!(NS_APPLICATION_ACTIVATION_POLICY_REGULAR, 0); 975 } 976 977 #[test] 978 fn autorelease_pool_create_and_drop() { 979 // Creating and dropping an autorelease pool should not crash. 980 let _pool = AutoreleasePool::new(); 981 } 982 983 #[test] 984 fn combined_style_mask() { 985 let style = NS_WINDOW_STYLE_MASK_TITLED 986 | NS_WINDOW_STYLE_MASK_CLOSABLE 987 | NS_WINDOW_STYLE_MASK_MINIATURIZABLE 988 | NS_WINDOW_STYLE_MASK_RESIZABLE; 989 assert_eq!(style, 0b1111); 990 } 991 992 #[test] 993 fn we_view_class_registration() { 994 register_we_view_class(); 995 let cls = class!("WeView"); 996 assert!(cls.is_some(), "WeView class should be registered"); 997 } 998 999 #[test] 1000 fn bitmap_view_create() { 1001 let _pool = AutoreleasePool::new(); 1002 let bitmap = BitmapContext::new(100, 100).expect("should create bitmap context"); 1003 let frame = NSRect::new(0.0, 0.0, 100.0, 100.0); 1004 let view = BitmapView::new(frame, &bitmap); 1005 assert!(!view.id().as_ptr().is_null()); 1006 } 1007 1008 #[test] 1009 fn we_view_accepts_first_responder() { 1010 let _pool = AutoreleasePool::new(); 1011 let bitmap = BitmapContext::new(100, 100).expect("should create bitmap context"); 1012 let frame = NSRect::new(0.0, 0.0, 100.0, 100.0); 1013 let view = BitmapView::new(frame, &bitmap); 1014 let accepts: bool = msg_send![view.id().as_ptr(), acceptsFirstResponder]; 1015 assert!(accepts, "WeView should accept first responder"); 1016 } 1017 1018 #[test] 1019 fn we_view_responds_to_key_down() { 1020 let _pool = AutoreleasePool::new(); 1021 register_we_view_class(); 1022 let cls = class!("WeView").expect("WeView should be registered"); 1023 let sel = Sel::register(c"keyDown:"); 1024 let instances_respond: bool = 1025 msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()]; 1026 assert!(instances_respond, "WeView should respond to keyDown:"); 1027 } 1028 1029 #[test] 1030 fn we_view_responds_to_mouse_events() { 1031 let _pool = AutoreleasePool::new(); 1032 register_we_view_class(); 1033 let cls = class!("WeView").expect("WeView should be registered"); 1034 1035 for sel_name in [c"mouseDown:", c"mouseUp:", c"mouseMoved:"] { 1036 let sel = Sel::register(sel_name); 1037 let responds: bool = msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()]; 1038 assert!(responds, "WeView should respond to {:?}", sel_name); 1039 } 1040 } 1041 1042 #[test] 1043 fn we_window_delegate_class_registration() { 1044 register_we_window_delegate_class(); 1045 let cls = class!("WeWindowDelegate"); 1046 assert!(cls.is_some(), "WeWindowDelegate class should be registered"); 1047 } 1048 1049 #[test] 1050 fn we_window_delegate_responds_to_resize() { 1051 register_we_window_delegate_class(); 1052 let cls = class!("WeWindowDelegate").expect("WeWindowDelegate should be registered"); 1053 let sel = Sel::register(c"windowDidResize:"); 1054 let responds: bool = msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()]; 1055 assert!( 1056 responds, 1057 "WeWindowDelegate should respond to windowDidResize:" 1058 ); 1059 } 1060}