web engine - experimental web browser

Implement basic event handling for macOS window

Add event handling to WeView (custom NSView subclass):
- acceptsFirstResponder override (returns YES for key event delivery)
- keyDown: logs key character and keyCode to stdout
- mouseDown:/mouseUp:/mouseMoved: log view-local coordinates to stdout

Add WeWindowDelegate (NSWindowDelegate):
- windowDidResize: marks content view as needing display
- windowShouldClose: returns YES (explicit close handling)

Add Window helper methods:
- set_delegate() for installing window delegates
- set_accepts_mouse_moved_events() for mouse move tracking

Update browser main.rs:
- Install window delegate for resize handling
- Enable mouse-moved event delivery

5 new tests for class registration, selector response, and first responder.

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

+230 -1
+6
crates/browser/src/main.rs
··· 11 11 12 12 let window = appkit::create_standard_window("we"); 13 13 14 + // Install a window delegate to handle resize events. 15 + appkit::install_window_delegate(&window); 16 + 17 + // Enable mouse-moved event delivery so mouseMoved: fires on the view. 18 + window.set_accepts_mouse_moved_events(true); 19 + 14 20 // Create a bitmap context for software rendering. 15 21 let bitmap = BitmapContext::new(800, 600).expect("failed to create bitmap context"); 16 22
+224 -1
crates/platform/src/appkit.rs
··· 13 13 use crate::objc::{Class, Id, Imp, Sel}; 14 14 use crate::{class, msg_send}; 15 15 use std::ffi::CStr; 16 - use std::os::raw::c_void; 16 + use std::os::raw::{c_char, c_void}; 17 17 18 18 // --------------------------------------------------------------------------- 19 19 // AppKit framework link ··· 226 226 let _: *mut c_void = msg_send![self.window.as_ptr(), setContentView: view.as_ptr()]; 227 227 } 228 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 + 229 241 /// Return the underlying Objective-C object. 230 242 pub fn id(&self) -> Id { 231 243 self.window ··· 326 338 c"B@:", 327 339 ); 328 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 + 329 422 view_class.register(); 330 423 } 331 424 ··· 380 473 } 381 474 382 475 // --------------------------------------------------------------------------- 476 + // Window delegate for handling resize and close events 477 + // --------------------------------------------------------------------------- 478 + 479 + /// Register the `WeWindowDelegate` class if not already registered. 480 + /// 481 + /// The class implements: 482 + /// - `windowDidResize:` — marks the content view as needing display 483 + /// - `windowShouldClose:` — returns YES (allows closing) 484 + fn register_we_window_delegate_class() { 485 + if class!("WeWindowDelegate").is_some() { 486 + return; 487 + } 488 + 489 + let superclass = class!("NSObject").expect("NSObject not found"); 490 + let delegate_class = Class::allocate(superclass, c"WeWindowDelegate", 0) 491 + .expect("failed to allocate WeWindowDelegate class"); 492 + 493 + // windowDidResize: — mark the content view as needing display 494 + extern "C" fn window_did_resize( 495 + _this: *mut c_void, 496 + _sel: *mut c_void, 497 + notification: *mut c_void, 498 + ) { 499 + let win: *mut c_void = msg_send![notification, object]; 500 + if win.is_null() { 501 + return; 502 + } 503 + let content_view: *mut c_void = msg_send![win, contentView]; 504 + if content_view.is_null() { 505 + return; 506 + } 507 + let _: *mut c_void = msg_send![content_view, setNeedsDisplay: true]; 508 + } 509 + 510 + let sel = Sel::register(c"windowDidResize:"); 511 + delegate_class.add_method( 512 + sel, 513 + unsafe { std::mem::transmute::<*const (), Imp>(window_did_resize as *const ()) }, 514 + c"v@:@", 515 + ); 516 + 517 + // windowShouldClose: -> YES 518 + extern "C" fn window_should_close( 519 + _this: *mut c_void, 520 + _sel: *mut c_void, 521 + _sender: *mut c_void, 522 + ) -> bool { 523 + true 524 + } 525 + 526 + let sel = Sel::register(c"windowShouldClose:"); 527 + delegate_class.add_method( 528 + sel, 529 + unsafe { std::mem::transmute::<*const (), Imp>(window_should_close as *const ()) }, 530 + c"B@:@", 531 + ); 532 + 533 + delegate_class.register(); 534 + } 535 + 536 + /// Install a window delegate that handles resize and close events. 537 + /// 538 + /// Creates a `WeWindowDelegate` class (if not already registered) and sets 539 + /// an instance as the window's delegate. The app delegate then terminates 540 + /// the app when the last window closes. 541 + pub fn install_window_delegate(window: &Window) { 542 + register_we_window_delegate_class(); 543 + 544 + let cls = class!("WeWindowDelegate").expect("WeWindowDelegate not found"); 545 + let delegate: *mut c_void = msg_send![cls.as_ptr(), alloc]; 546 + let delegate: *mut c_void = msg_send![delegate, init]; 547 + let delegate = 548 + unsafe { Id::from_raw(delegate as *mut _) }.expect("WeWindowDelegate init failed"); 549 + window.set_delegate(&delegate); 550 + } 551 + 552 + // --------------------------------------------------------------------------- 383 553 // App delegate for handling window close -> app termination 384 554 // --------------------------------------------------------------------------- 385 555 ··· 516 686 let frame = NSRect::new(0.0, 0.0, 100.0, 100.0); 517 687 let view = BitmapView::new(frame, &bitmap); 518 688 assert!(!view.id().as_ptr().is_null()); 689 + } 690 + 691 + #[test] 692 + fn we_view_accepts_first_responder() { 693 + let _pool = AutoreleasePool::new(); 694 + let bitmap = BitmapContext::new(100, 100).expect("should create bitmap context"); 695 + let frame = NSRect::new(0.0, 0.0, 100.0, 100.0); 696 + let view = BitmapView::new(frame, &bitmap); 697 + let accepts: bool = msg_send![view.id().as_ptr(), acceptsFirstResponder]; 698 + assert!(accepts, "WeView should accept first responder"); 699 + } 700 + 701 + #[test] 702 + fn we_view_responds_to_key_down() { 703 + let _pool = AutoreleasePool::new(); 704 + register_we_view_class(); 705 + let cls = class!("WeView").expect("WeView should be registered"); 706 + let sel = Sel::register(c"keyDown:"); 707 + let instances_respond: bool = 708 + msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()]; 709 + assert!(instances_respond, "WeView should respond to keyDown:"); 710 + } 711 + 712 + #[test] 713 + fn we_view_responds_to_mouse_events() { 714 + let _pool = AutoreleasePool::new(); 715 + register_we_view_class(); 716 + let cls = class!("WeView").expect("WeView should be registered"); 717 + 718 + for sel_name in [c"mouseDown:", c"mouseUp:", c"mouseMoved:"] { 719 + let sel = Sel::register(sel_name); 720 + let responds: bool = msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()]; 721 + assert!(responds, "WeView should respond to {:?}", sel_name); 722 + } 723 + } 724 + 725 + #[test] 726 + fn we_window_delegate_class_registration() { 727 + register_we_window_delegate_class(); 728 + let cls = class!("WeWindowDelegate"); 729 + assert!(cls.is_some(), "WeWindowDelegate class should be registered"); 730 + } 731 + 732 + #[test] 733 + fn we_window_delegate_responds_to_resize() { 734 + register_we_window_delegate_class(); 735 + let cls = class!("WeWindowDelegate").expect("WeWindowDelegate should be registered"); 736 + let sel = Sel::register(c"windowDidResize:"); 737 + let responds: bool = msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()]; 738 + assert!( 739 + responds, 740 + "WeWindowDelegate should respond to windowDidResize:" 741 + ); 519 742 } 520 743 }