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::objc::{Class, Id, Imp, Sel};
14use crate::{class, msg_send};
15use std::ffi::CStr;
16use std::os::raw::{c_char, c_void};
17
18// ---------------------------------------------------------------------------
19// AppKit framework link
20// ---------------------------------------------------------------------------
21
22#[link(name = "AppKit", kind = "framework")]
23extern "C" {}
24
25// ---------------------------------------------------------------------------
26// Geometry types matching AppKit's expectations
27// ---------------------------------------------------------------------------
28
29/// `NSRect` / `CGRect` — a rectangle defined by origin and size.
30#[repr(C)]
31#[derive(Debug, Clone, Copy)]
32pub struct NSRect {
33 pub origin: NSPoint,
34 pub size: NSSize,
35}
36
37/// `NSPoint` / `CGPoint` — a point in 2D space.
38#[repr(C)]
39#[derive(Debug, Clone, Copy)]
40pub struct NSPoint {
41 pub x: f64,
42 pub y: f64,
43}
44
45/// `NSSize` / `CGSize` — a 2D size.
46#[repr(C)]
47#[derive(Debug, Clone, Copy)]
48pub struct NSSize {
49 pub width: f64,
50 pub height: f64,
51}
52
53impl NSRect {
54 /// Create a new rectangle.
55 pub fn new(x: f64, y: f64, width: f64, height: f64) -> NSRect {
56 NSRect {
57 origin: NSPoint { x, y },
58 size: NSSize { width, height },
59 }
60 }
61}
62
63// ---------------------------------------------------------------------------
64// NSWindow style mask constants
65// ---------------------------------------------------------------------------
66
67/// Window has a title bar.
68pub const NS_WINDOW_STYLE_MASK_TITLED: u64 = 1 << 0;
69/// Window has a close button.
70pub const NS_WINDOW_STYLE_MASK_CLOSABLE: u64 = 1 << 1;
71/// Window can be minimized.
72pub const NS_WINDOW_STYLE_MASK_MINIATURIZABLE: u64 = 1 << 2;
73/// Window can be resized.
74pub const NS_WINDOW_STYLE_MASK_RESIZABLE: u64 = 1 << 3;
75
76// ---------------------------------------------------------------------------
77// NSBackingStoreType constants
78// ---------------------------------------------------------------------------
79
80/// Buffered backing store (the standard for modern macOS).
81pub const NS_BACKING_STORE_BUFFERED: u64 = 2;
82
83// ---------------------------------------------------------------------------
84// NSApplicationActivationPolicy constants
85// ---------------------------------------------------------------------------
86
87/// Regular application that appears in the Dock and may have a menu bar.
88pub const NS_APPLICATION_ACTIVATION_POLICY_REGULAR: i64 = 0;
89
90// ---------------------------------------------------------------------------
91// NSAutoreleasePool
92// ---------------------------------------------------------------------------
93
94/// RAII wrapper for `NSAutoreleasePool`.
95///
96/// Creates a pool on construction and drains it on drop. Required for any
97/// Objective-C code that creates autoreleased objects.
98pub struct AutoreleasePool {
99 pool: Id,
100}
101
102impl Default for AutoreleasePool {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl AutoreleasePool {
109 /// Create a new autorelease pool.
110 pub fn new() -> AutoreleasePool {
111 let cls = class!("NSAutoreleasePool").expect("NSAutoreleasePool class not found");
112 let pool: *mut c_void = msg_send![cls.as_ptr(), alloc];
113 let pool: *mut c_void = msg_send![pool, init];
114 let pool = unsafe { Id::from_raw(pool as *mut _) }.expect("NSAutoreleasePool init failed");
115 AutoreleasePool { pool }
116 }
117}
118
119impl Drop for AutoreleasePool {
120 fn drop(&mut self) {
121 let _: *mut c_void = msg_send![self.pool.as_ptr(), drain];
122 }
123}
124
125// ---------------------------------------------------------------------------
126// NSApplication wrapper
127// ---------------------------------------------------------------------------
128
129/// Wrapper around `NSApplication`.
130pub struct App {
131 app: Id,
132}
133
134impl App {
135 /// Get the shared `NSApplication` instance.
136 ///
137 /// Must be called from the main thread. Creates the application object
138 /// if it doesn't already exist.
139 pub fn shared() -> App {
140 let cls = class!("NSApplication").expect("NSApplication class not found");
141 let app: *mut c_void = msg_send![cls.as_ptr(), sharedApplication];
142 let app = unsafe { Id::from_raw(app as *mut _) }.expect("sharedApplication returned nil");
143 App { app }
144 }
145
146 /// Set the application's activation policy.
147 ///
148 /// Use [`NS_APPLICATION_ACTIVATION_POLICY_REGULAR`] for a normal app that
149 /// appears in the Dock.
150 pub fn set_activation_policy(&self, policy: i64) {
151 let _: bool = msg_send![self.app.as_ptr(), setActivationPolicy: policy];
152 }
153
154 /// Activate the application, bringing it to the foreground.
155 pub fn activate(&self) {
156 let _: *mut c_void = msg_send![self.app.as_ptr(), activateIgnoringOtherApps: true];
157 }
158
159 /// Start the application's main event loop.
160 ///
161 /// This method does **not** return under normal circumstances.
162 pub fn run(&self) {
163 let _: *mut c_void = msg_send![self.app.as_ptr(), run];
164 }
165
166 /// Return the underlying Objective-C object.
167 pub fn id(&self) -> Id {
168 self.app
169 }
170}
171
172// ---------------------------------------------------------------------------
173// NSWindow wrapper
174// ---------------------------------------------------------------------------
175
176/// Wrapper around `NSWindow`.
177pub struct Window {
178 window: Id,
179}
180
181impl Window {
182 /// Create a new window with the given content rect, style mask, and backing.
183 ///
184 /// # Arguments
185 ///
186 /// * `rect` — The content rectangle (position and size).
187 /// * `style` — Bitwise OR of `NS_WINDOW_STYLE_MASK_*` constants.
188 /// * `backing` — Backing store type (use [`NS_BACKING_STORE_BUFFERED`]).
189 /// * `defer` — Whether to defer window device creation.
190 pub fn new(rect: NSRect, style: u64, backing: u64, defer: bool) -> Window {
191 let cls = class!("NSWindow").expect("NSWindow class not found");
192 let window: *mut c_void = msg_send![cls.as_ptr(), alloc];
193 let window: *mut c_void = msg_send![
194 window,
195 initWithContentRect: rect,
196 styleMask: style,
197 backing: backing,
198 defer: defer
199 ];
200 let window =
201 unsafe { Id::from_raw(window as *mut _) }.expect("NSWindow initWithContentRect failed");
202 Window { window }
203 }
204
205 /// Set the window's title.
206 pub fn set_title(&self, title: &str) {
207 let cf_title = CfString::new(title).expect("failed to create CFString for title");
208 // CFStringRef is toll-free bridged to NSString*.
209 let _: *mut c_void = msg_send![self.window.as_ptr(), setTitle: cf_title.as_void_ptr()];
210 }
211
212 /// Make the window the key window and bring it to the front.
213 pub fn make_key_and_order_front(&self) {
214 let _: *mut c_void =
215 msg_send![self.window.as_ptr(), makeKeyAndOrderFront: std::ptr::null::<c_void>()];
216 }
217
218 /// Get the window's content view.
219 pub fn content_view(&self) -> Id {
220 let view: *mut c_void = msg_send![self.window.as_ptr(), contentView];
221 unsafe { Id::from_raw(view as *mut _) }.expect("contentView returned nil")
222 }
223
224 /// Set the window's content view.
225 pub fn set_content_view(&self, view: &Id) {
226 let _: *mut c_void = msg_send![self.window.as_ptr(), setContentView: view.as_ptr()];
227 }
228
229 /// Set the window's delegate.
230 pub fn set_delegate(&self, delegate: &Id) {
231 let _: *mut c_void = msg_send![self.window.as_ptr(), setDelegate: delegate.as_ptr()];
232 }
233
234 /// Enable or disable mouse-moved event delivery for this window.
235 ///
236 /// Must be set to `true` for `mouseMoved:` events to reach the view.
237 pub fn set_accepts_mouse_moved_events(&self, accepts: bool) {
238 let _: *mut c_void = msg_send![self.window.as_ptr(), setAcceptsMouseMovedEvents: accepts];
239 }
240
241 /// Return the underlying Objective-C object.
242 pub fn id(&self) -> Id {
243 self.window
244 }
245}
246
247// ---------------------------------------------------------------------------
248// BitmapView — custom NSView subclass for rendering a CG bitmap
249// ---------------------------------------------------------------------------
250
251/// The name of the ivar storing the BitmapContext pointer in WeView.
252const BITMAP_CTX_IVAR: &CStr = c"_bitmapCtx";
253
254/// Register the `WeView` custom NSView subclass.
255///
256/// The class overrides `drawRect:` to blit a CGImage from the associated
257/// BitmapContext into the view's graphics context, and `isFlipped` to use a
258/// top-left coordinate origin.
259fn register_we_view_class() {
260 if class!("WeView").is_some() {
261 return;
262 }
263
264 let superclass = class!("NSView").expect("NSView not found");
265 let view_class =
266 Class::allocate(superclass, c"WeView", 0).expect("failed to allocate WeView class");
267
268 // Add an ivar to hold a raw pointer to a BitmapContext.
269 // Size = 8 (pointer), alignment = 3 (log2(8)), type = "^v" (pointer to void).
270 view_class.add_ivar(BITMAP_CTX_IVAR, 8, 3, c"^v");
271
272 // drawRect:
273 extern "C" fn draw_rect(this: *mut c_void, _sel: *mut c_void, _dirty_rect: NSRect) {
274 let this_id = unsafe { Id::from_raw(this as *mut _) };
275 let this_id = match this_id {
276 Some(id) => id,
277 None => return,
278 };
279
280 // Get the BitmapContext pointer from our ivar.
281 let ctx_ptr = unsafe { this_id.get_ivar(BITMAP_CTX_IVAR) };
282 if ctx_ptr.is_null() {
283 return;
284 }
285 let bitmap_ctx = unsafe { &*(ctx_ptr as *const BitmapContext) };
286
287 // Create a CGImage from the bitmap context.
288 let image = match bitmap_ctx.create_image() {
289 Some(img) => img,
290 None => return,
291 };
292
293 // Get the view's bounds: [self bounds]
294 let bounds: NSRect = msg_send![this, bounds];
295
296 // Get the current NSGraphicsContext:
297 // [NSGraphicsContext currentContext]
298 let gfx_ctx_cls = class!("NSGraphicsContext").expect("NSGraphicsContext not found");
299 let gfx_ctx: *mut c_void = msg_send![gfx_ctx_cls.as_ptr(), currentContext];
300 if gfx_ctx.is_null() {
301 return;
302 }
303
304 // Get the CGContextRef: [gfxCtx CGContext]
305 let cg_context: *mut c_void = msg_send![gfx_ctx, CGContext];
306 if cg_context.is_null() {
307 return;
308 }
309
310 // Draw the image into the CG context.
311 let rect = CGRect::new(
312 bounds.origin.x,
313 bounds.origin.y,
314 bounds.size.width,
315 bounds.size.height,
316 );
317 unsafe {
318 cg::draw_image_in_context(cg_context, rect, &image);
319 }
320 }
321
322 let sel = Sel::register(c"drawRect:");
323 view_class.add_method(
324 sel,
325 unsafe { std::mem::transmute::<*const (), Imp>(draw_rect as *const ()) },
326 c"v@:{CGRect={CGPoint=dd}{CGSize=dd}}",
327 );
328
329 // isFlipped -> YES (top-left origin)
330 extern "C" fn is_flipped(_this: *mut c_void, _sel: *mut c_void) -> bool {
331 true
332 }
333
334 let sel = Sel::register(c"isFlipped");
335 view_class.add_method(
336 sel,
337 unsafe { std::mem::transmute::<*const (), Imp>(is_flipped as *const ()) },
338 c"B@:",
339 );
340
341 // acceptsFirstResponder -> YES (allows view to receive key events)
342 extern "C" fn accepts_first_responder(_this: *mut c_void, _sel: *mut c_void) -> bool {
343 true
344 }
345
346 let sel = Sel::register(c"acceptsFirstResponder");
347 view_class.add_method(
348 sel,
349 unsafe { std::mem::transmute::<*const (), Imp>(accepts_first_responder as *const ()) },
350 c"B@:",
351 );
352
353 // keyDown: — log key character and keyCode to stdout
354 extern "C" fn key_down(_this: *mut c_void, _sel: *mut c_void, event: *mut c_void) {
355 let chars: *mut c_void = msg_send![event, characters];
356 if chars.is_null() {
357 return;
358 }
359 let utf8: *const c_char = msg_send![chars, UTF8String];
360 if utf8.is_null() {
361 return;
362 }
363 let c_str = unsafe { CStr::from_ptr(utf8) };
364 let key_code: u16 = msg_send![event, keyCode];
365 if let Ok(s) = c_str.to_str() {
366 println!("keyDown: '{}' (keyCode: {})", s, key_code);
367 }
368 }
369
370 let sel = Sel::register(c"keyDown:");
371 view_class.add_method(
372 sel,
373 unsafe { std::mem::transmute::<*const (), Imp>(key_down as *const ()) },
374 c"v@:@",
375 );
376
377 // mouseDown: — log mouse location to stdout
378 extern "C" fn mouse_down(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) {
379 let raw_loc: NSPoint = msg_send![event, locationInWindow];
380 let loc: NSPoint =
381 msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::<c_void>()];
382 println!("mouseDown: ({:.1}, {:.1})", loc.x, loc.y);
383 }
384
385 let sel = Sel::register(c"mouseDown:");
386 view_class.add_method(
387 sel,
388 unsafe { std::mem::transmute::<*const (), Imp>(mouse_down as *const ()) },
389 c"v@:@",
390 );
391
392 // mouseUp: — log mouse location to stdout
393 extern "C" fn mouse_up(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) {
394 let raw_loc: NSPoint = msg_send![event, locationInWindow];
395 let loc: NSPoint =
396 msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::<c_void>()];
397 println!("mouseUp: ({:.1}, {:.1})", loc.x, loc.y);
398 }
399
400 let sel = Sel::register(c"mouseUp:");
401 view_class.add_method(
402 sel,
403 unsafe { std::mem::transmute::<*const (), Imp>(mouse_up as *const ()) },
404 c"v@:@",
405 );
406
407 // mouseMoved: — log mouse location to stdout
408 extern "C" fn mouse_moved(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) {
409 let raw_loc: NSPoint = msg_send![event, locationInWindow];
410 let loc: NSPoint =
411 msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::<c_void>()];
412 println!("mouseMoved: ({:.1}, {:.1})", loc.x, loc.y);
413 }
414
415 let sel = Sel::register(c"mouseMoved:");
416 view_class.add_method(
417 sel,
418 unsafe { std::mem::transmute::<*const (), Imp>(mouse_moved as *const ()) },
419 c"v@:@",
420 );
421
422 // scrollWheel: — call scroll handler with delta and mouse location
423 extern "C" fn scroll_wheel(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) {
424 let dx: f64 = msg_send![event, scrollingDeltaX];
425 let dy: f64 = msg_send![event, scrollingDeltaY];
426 let raw_loc: NSPoint = msg_send![event, locationInWindow];
427 let loc: NSPoint =
428 msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::<c_void>()];
429 // SAFETY: We are on the main thread (AppKit event loop).
430 unsafe {
431 if let Some(handler) = SCROLL_HANDLER {
432 handler(dx, dy, loc.x, loc.y);
433 }
434 }
435 }
436
437 let sel = Sel::register(c"scrollWheel:");
438 view_class.add_method(
439 sel,
440 unsafe { std::mem::transmute::<*const (), Imp>(scroll_wheel as *const ()) },
441 c"v@:@",
442 );
443
444 view_class.register();
445}
446
447/// A custom NSView backed by a [`BitmapContext`].
448///
449/// When the view needs to draw, it creates a `CGImage` from the bitmap
450/// context and blits it into the view's graphics context.
451///
452/// The `BitmapContext` must outlive this view. The caller is responsible
453/// for ensuring this (typically by keeping the context in a `Box` alongside
454/// the view).
455pub struct BitmapView {
456 view: Id,
457}
458
459impl BitmapView {
460 /// Create a new `BitmapView` with the given frame and bitmap context.
461 ///
462 /// The `bitmap_ctx` pointer is stored in the view's instance variable.
463 /// The caller must ensure the `BitmapContext` outlives this view.
464 pub fn new(frame: NSRect, bitmap_ctx: &BitmapContext) -> BitmapView {
465 register_we_view_class();
466
467 let cls = class!("WeView").expect("WeView class not found");
468 let view: *mut c_void = msg_send![cls.as_ptr(), alloc];
469 let view: *mut c_void = msg_send![view, initWithFrame: frame];
470 let view = unsafe { Id::from_raw(view as *mut _) }.expect("WeView initWithFrame failed");
471
472 // Store the BitmapContext pointer in the ivar.
473 unsafe {
474 view.set_ivar(
475 BITMAP_CTX_IVAR,
476 bitmap_ctx as *const BitmapContext as *mut c_void,
477 );
478 }
479
480 BitmapView { view }
481 }
482
483 /// Update the bitmap context pointer stored in the view.
484 ///
485 /// Call this when the bitmap context has been replaced (e.g., on resize).
486 /// The new `BitmapContext` must outlive this view.
487 pub fn update_bitmap(&self, bitmap_ctx: &BitmapContext) {
488 unsafe {
489 self.view.set_ivar(
490 BITMAP_CTX_IVAR,
491 bitmap_ctx as *const BitmapContext as *mut c_void,
492 );
493 }
494 }
495
496 /// Request the view to redraw.
497 ///
498 /// Call this after modifying the bitmap context's pixels to
499 /// schedule a redraw.
500 pub fn set_needs_display(&self) {
501 let _: *mut c_void = msg_send![self.view.as_ptr(), setNeedsDisplay: true];
502 }
503
504 /// Return the underlying Objective-C view object.
505 pub fn id(&self) -> Id {
506 self.view
507 }
508}
509
510// ---------------------------------------------------------------------------
511// Global resize handler
512// ---------------------------------------------------------------------------
513
514/// Global resize callback, called from `windowDidResize:` with the new
515/// content view dimensions (width, height) in points.
516///
517/// # Safety
518///
519/// Accessed only from the main thread (the AppKit event loop).
520static mut RESIZE_HANDLER: Option<fn(f64, f64)> = None;
521
522/// Register a function to be called when the window is resized.
523///
524/// The handler receives the new content view width and height in points.
525/// Only one handler can be active at a time; setting a new one replaces
526/// any previous handler.
527pub fn set_resize_handler(handler: fn(f64, f64)) {
528 // SAFETY: Called from the main thread before `app.run()`.
529 unsafe {
530 RESIZE_HANDLER = Some(handler);
531 }
532}
533
534/// Global scroll callback, called from `scrollWheel:` with the scroll
535/// deltas (dx, dy) and mouse location (x, y) in view coordinates.
536///
537/// # Safety
538///
539/// Accessed only from the main thread (the AppKit event loop).
540static mut SCROLL_HANDLER: Option<fn(f64, f64, f64, f64)> = None;
541
542/// Register a function to be called when a scroll wheel event occurs.
543///
544/// The handler receives `(delta_x, delta_y, mouse_x, mouse_y)`.
545/// Only one handler can be active at a time.
546pub fn set_scroll_handler(handler: fn(f64, f64, f64, f64)) {
547 // SAFETY: Called from the main thread before `app.run()`.
548 unsafe {
549 SCROLL_HANDLER = Some(handler);
550 }
551}
552
553// ---------------------------------------------------------------------------
554// Window delegate for handling resize and close events
555// ---------------------------------------------------------------------------
556
557/// Register the `WeWindowDelegate` class if not already registered.
558///
559/// The class implements:
560/// - `windowDidResize:` — marks the content view as needing display
561/// - `windowShouldClose:` — returns YES (allows closing)
562fn register_we_window_delegate_class() {
563 if class!("WeWindowDelegate").is_some() {
564 return;
565 }
566
567 let superclass = class!("NSObject").expect("NSObject not found");
568 let delegate_class = Class::allocate(superclass, c"WeWindowDelegate", 0)
569 .expect("failed to allocate WeWindowDelegate class");
570
571 // windowDidResize: — call resize handler and mark view as needing display
572 extern "C" fn window_did_resize(
573 _this: *mut c_void,
574 _sel: *mut c_void,
575 notification: *mut c_void,
576 ) {
577 let win: *mut c_void = msg_send![notification, object];
578 if win.is_null() {
579 return;
580 }
581 let content_view: *mut c_void = msg_send![win, contentView];
582 if content_view.is_null() {
583 return;
584 }
585 // Get the content view's bounds to determine new dimensions.
586 let bounds: NSRect = msg_send![content_view, bounds];
587 // Call the resize handler if one has been registered.
588 // SAFETY: We are on the main thread (AppKit event loop).
589 unsafe {
590 if let Some(handler) = RESIZE_HANDLER {
591 handler(bounds.size.width, bounds.size.height);
592 }
593 }
594 let _: *mut c_void = msg_send![content_view, setNeedsDisplay: true];
595 }
596
597 let sel = Sel::register(c"windowDidResize:");
598 delegate_class.add_method(
599 sel,
600 unsafe { std::mem::transmute::<*const (), Imp>(window_did_resize as *const ()) },
601 c"v@:@",
602 );
603
604 // windowShouldClose: -> YES
605 extern "C" fn window_should_close(
606 _this: *mut c_void,
607 _sel: *mut c_void,
608 _sender: *mut c_void,
609 ) -> bool {
610 true
611 }
612
613 let sel = Sel::register(c"windowShouldClose:");
614 delegate_class.add_method(
615 sel,
616 unsafe { std::mem::transmute::<*const (), Imp>(window_should_close as *const ()) },
617 c"B@:@",
618 );
619
620 delegate_class.register();
621}
622
623/// Install a window delegate that handles resize and close events.
624///
625/// Creates a `WeWindowDelegate` class (if not already registered) and sets
626/// an instance as the window's delegate. The app delegate then terminates
627/// the app when the last window closes.
628pub fn install_window_delegate(window: &Window) {
629 register_we_window_delegate_class();
630
631 let cls = class!("WeWindowDelegate").expect("WeWindowDelegate not found");
632 let delegate: *mut c_void = msg_send![cls.as_ptr(), alloc];
633 let delegate: *mut c_void = msg_send![delegate, init];
634 let delegate =
635 unsafe { Id::from_raw(delegate as *mut _) }.expect("WeWindowDelegate init failed");
636 window.set_delegate(&delegate);
637}
638
639// ---------------------------------------------------------------------------
640// App delegate for handling window close -> app termination
641// ---------------------------------------------------------------------------
642
643/// Install an application delegate that terminates the app when the last
644/// window is closed.
645///
646/// This creates a custom Objective-C class `WeAppDelegate` that implements
647/// `applicationShouldTerminateAfterLastWindowClosed:` returning `YES`.
648pub fn install_app_delegate(app: &App) {
649 // Only register the delegate class once.
650 if class!("WeAppDelegate").is_some() {
651 // Already registered, just create an instance and set it.
652 set_delegate(app);
653 return;
654 }
655
656 let superclass = class!("NSObject").expect("NSObject not found");
657 let delegate_class = Class::allocate(superclass, c"WeAppDelegate", 0)
658 .expect("failed to allocate WeAppDelegate class");
659
660 // applicationShouldTerminateAfterLastWindowClosed:
661 extern "C" fn should_terminate_after_last_window_closed(
662 _this: *mut c_void,
663 _sel: *mut c_void,
664 _app: *mut c_void,
665 ) -> bool {
666 true
667 }
668
669 let sel = Sel::register(c"applicationShouldTerminateAfterLastWindowClosed:");
670 delegate_class.add_method(
671 sel,
672 unsafe {
673 std::mem::transmute::<*const (), Imp>(
674 should_terminate_after_last_window_closed as *const (),
675 )
676 },
677 c"B@:@",
678 );
679
680 delegate_class.register();
681 set_delegate(app);
682}
683
684fn set_delegate(app: &App) {
685 let cls = class!("WeAppDelegate").expect("WeAppDelegate not found");
686 let delegate: *mut c_void = msg_send![cls.as_ptr(), alloc];
687 let delegate: *mut c_void = msg_send![delegate, init];
688 let _: *mut c_void = msg_send![app.id().as_ptr(), setDelegate: delegate];
689}
690
691// ---------------------------------------------------------------------------
692// Convenience: create a standard browser window
693// ---------------------------------------------------------------------------
694
695/// Create a standard window suitable for a browser.
696///
697/// Returns a window with title bar, close, minimize, and resize controls,
698/// centered at (200, 200), sized 800x600.
699pub fn create_standard_window(title: &str) -> Window {
700 let style = NS_WINDOW_STYLE_MASK_TITLED
701 | NS_WINDOW_STYLE_MASK_CLOSABLE
702 | NS_WINDOW_STYLE_MASK_MINIATURIZABLE
703 | NS_WINDOW_STYLE_MASK_RESIZABLE;
704
705 let rect = NSRect::new(200.0, 200.0, 800.0, 600.0);
706 let window = Window::new(rect, style, NS_BACKING_STORE_BUFFERED, false);
707 window.set_title(title);
708 window
709}
710
711// ---------------------------------------------------------------------------
712// Tests
713// ---------------------------------------------------------------------------
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718
719 #[test]
720 fn nsrect_new() {
721 let rect = NSRect::new(10.0, 20.0, 300.0, 400.0);
722 assert_eq!(rect.origin.x, 10.0);
723 assert_eq!(rect.origin.y, 20.0);
724 assert_eq!(rect.size.width, 300.0);
725 assert_eq!(rect.size.height, 400.0);
726 }
727
728 #[test]
729 fn style_mask_constants() {
730 // Verify the constants match AppKit's expected values.
731 assert_eq!(NS_WINDOW_STYLE_MASK_TITLED, 1);
732 assert_eq!(NS_WINDOW_STYLE_MASK_CLOSABLE, 2);
733 assert_eq!(NS_WINDOW_STYLE_MASK_MINIATURIZABLE, 4);
734 assert_eq!(NS_WINDOW_STYLE_MASK_RESIZABLE, 8);
735 }
736
737 #[test]
738 fn backing_store_constant() {
739 assert_eq!(NS_BACKING_STORE_BUFFERED, 2);
740 }
741
742 #[test]
743 fn activation_policy_constant() {
744 assert_eq!(NS_APPLICATION_ACTIVATION_POLICY_REGULAR, 0);
745 }
746
747 #[test]
748 fn autorelease_pool_create_and_drop() {
749 // Creating and dropping an autorelease pool should not crash.
750 let _pool = AutoreleasePool::new();
751 }
752
753 #[test]
754 fn combined_style_mask() {
755 let style = NS_WINDOW_STYLE_MASK_TITLED
756 | NS_WINDOW_STYLE_MASK_CLOSABLE
757 | NS_WINDOW_STYLE_MASK_MINIATURIZABLE
758 | NS_WINDOW_STYLE_MASK_RESIZABLE;
759 assert_eq!(style, 0b1111);
760 }
761
762 #[test]
763 fn we_view_class_registration() {
764 register_we_view_class();
765 let cls = class!("WeView");
766 assert!(cls.is_some(), "WeView class should be registered");
767 }
768
769 #[test]
770 fn bitmap_view_create() {
771 let _pool = AutoreleasePool::new();
772 let bitmap = BitmapContext::new(100, 100).expect("should create bitmap context");
773 let frame = NSRect::new(0.0, 0.0, 100.0, 100.0);
774 let view = BitmapView::new(frame, &bitmap);
775 assert!(!view.id().as_ptr().is_null());
776 }
777
778 #[test]
779 fn we_view_accepts_first_responder() {
780 let _pool = AutoreleasePool::new();
781 let bitmap = BitmapContext::new(100, 100).expect("should create bitmap context");
782 let frame = NSRect::new(0.0, 0.0, 100.0, 100.0);
783 let view = BitmapView::new(frame, &bitmap);
784 let accepts: bool = msg_send![view.id().as_ptr(), acceptsFirstResponder];
785 assert!(accepts, "WeView should accept first responder");
786 }
787
788 #[test]
789 fn we_view_responds_to_key_down() {
790 let _pool = AutoreleasePool::new();
791 register_we_view_class();
792 let cls = class!("WeView").expect("WeView should be registered");
793 let sel = Sel::register(c"keyDown:");
794 let instances_respond: bool =
795 msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()];
796 assert!(instances_respond, "WeView should respond to keyDown:");
797 }
798
799 #[test]
800 fn we_view_responds_to_mouse_events() {
801 let _pool = AutoreleasePool::new();
802 register_we_view_class();
803 let cls = class!("WeView").expect("WeView should be registered");
804
805 for sel_name in [c"mouseDown:", c"mouseUp:", c"mouseMoved:"] {
806 let sel = Sel::register(sel_name);
807 let responds: bool = msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()];
808 assert!(responds, "WeView should respond to {:?}", sel_name);
809 }
810 }
811
812 #[test]
813 fn we_window_delegate_class_registration() {
814 register_we_window_delegate_class();
815 let cls = class!("WeWindowDelegate");
816 assert!(cls.is_some(), "WeWindowDelegate class should be registered");
817 }
818
819 #[test]
820 fn we_window_delegate_responds_to_resize() {
821 register_we_window_delegate_class();
822 let cls = class!("WeWindowDelegate").expect("WeWindowDelegate should be registered");
823 let sel = Sel::register(c"windowDidResize:");
824 let responds: bool = msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()];
825 assert!(
826 responds,
827 "WeWindowDelegate should respond to windowDidResize:"
828 );
829 }
830}