we (web engine): Experimental web browser project to understand the limits of Claude
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}