From afab3ee77d9238e0406168861a75fbd3cb809cbd Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Tue, 24 Feb 2026 00:17:03 +0100 Subject: [PATCH] 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 --- crates/browser/src/main.rs | 6 + crates/platform/src/appkit.rs | 225 +++++++++++++++++++++++++++++++++- 2 files changed, 230 insertions(+), 1 deletion(-) diff --git a/crates/browser/src/main.rs b/crates/browser/src/main.rs index cb86954..a32972a 100644 --- a/crates/browser/src/main.rs +++ b/crates/browser/src/main.rs @@ -11,6 +11,12 @@ fn main() { let window = appkit::create_standard_window("we"); + // Install a window delegate to handle resize events. + appkit::install_window_delegate(&window); + + // Enable mouse-moved event delivery so mouseMoved: fires on the view. + window.set_accepts_mouse_moved_events(true); + // Create a bitmap context for software rendering. let bitmap = BitmapContext::new(800, 600).expect("failed to create bitmap context"); diff --git a/crates/platform/src/appkit.rs b/crates/platform/src/appkit.rs index ed3c574..e2775d8 100644 --- a/crates/platform/src/appkit.rs +++ b/crates/platform/src/appkit.rs @@ -13,7 +13,7 @@ use crate::cg::{self, BitmapContext, CGRect}; use crate::objc::{Class, Id, Imp, Sel}; use crate::{class, msg_send}; use std::ffi::CStr; -use std::os::raw::c_void; +use std::os::raw::{c_char, c_void}; // --------------------------------------------------------------------------- // AppKit framework link @@ -226,6 +226,18 @@ impl Window { let _: *mut c_void = msg_send![self.window.as_ptr(), setContentView: view.as_ptr()]; } + /// Set the window's delegate. + pub fn set_delegate(&self, delegate: &Id) { + let _: *mut c_void = msg_send![self.window.as_ptr(), setDelegate: delegate.as_ptr()]; + } + + /// Enable or disable mouse-moved event delivery for this window. + /// + /// Must be set to `true` for `mouseMoved:` events to reach the view. + pub fn set_accepts_mouse_moved_events(&self, accepts: bool) { + let _: *mut c_void = msg_send![self.window.as_ptr(), setAcceptsMouseMovedEvents: accepts]; + } + /// Return the underlying Objective-C object. pub fn id(&self) -> Id { self.window @@ -326,6 +338,87 @@ fn register_we_view_class() { c"B@:", ); + // acceptsFirstResponder -> YES (allows view to receive key events) + extern "C" fn accepts_first_responder(_this: *mut c_void, _sel: *mut c_void) -> bool { + true + } + + let sel = Sel::register(c"acceptsFirstResponder"); + view_class.add_method( + sel, + unsafe { std::mem::transmute::<*const (), Imp>(accepts_first_responder as *const ()) }, + c"B@:", + ); + + // keyDown: — log key character and keyCode to stdout + extern "C" fn key_down(_this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { + let chars: *mut c_void = msg_send![event, characters]; + if chars.is_null() { + return; + } + let utf8: *const c_char = msg_send![chars, UTF8String]; + if utf8.is_null() { + return; + } + let c_str = unsafe { CStr::from_ptr(utf8) }; + let key_code: u16 = msg_send![event, keyCode]; + if let Ok(s) = c_str.to_str() { + println!("keyDown: '{}' (keyCode: {})", s, key_code); + } + } + + let sel = Sel::register(c"keyDown:"); + view_class.add_method( + sel, + unsafe { std::mem::transmute::<*const (), Imp>(key_down as *const ()) }, + c"v@:@", + ); + + // mouseDown: — log mouse location to stdout + extern "C" fn mouse_down(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { + let raw_loc: NSPoint = msg_send![event, locationInWindow]; + let loc: NSPoint = + msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::()]; + println!("mouseDown: ({:.1}, {:.1})", loc.x, loc.y); + } + + let sel = Sel::register(c"mouseDown:"); + view_class.add_method( + sel, + unsafe { std::mem::transmute::<*const (), Imp>(mouse_down as *const ()) }, + c"v@:@", + ); + + // mouseUp: — log mouse location to stdout + extern "C" fn mouse_up(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { + let raw_loc: NSPoint = msg_send![event, locationInWindow]; + let loc: NSPoint = + msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::()]; + println!("mouseUp: ({:.1}, {:.1})", loc.x, loc.y); + } + + let sel = Sel::register(c"mouseUp:"); + view_class.add_method( + sel, + unsafe { std::mem::transmute::<*const (), Imp>(mouse_up as *const ()) }, + c"v@:@", + ); + + // mouseMoved: — log mouse location to stdout + extern "C" fn mouse_moved(this: *mut c_void, _sel: *mut c_void, event: *mut c_void) { + let raw_loc: NSPoint = msg_send![event, locationInWindow]; + let loc: NSPoint = + msg_send![this, convertPoint: raw_loc, fromView: std::ptr::null_mut::()]; + println!("mouseMoved: ({:.1}, {:.1})", loc.x, loc.y); + } + + let sel = Sel::register(c"mouseMoved:"); + view_class.add_method( + sel, + unsafe { std::mem::transmute::<*const (), Imp>(mouse_moved as *const ()) }, + c"v@:@", + ); + view_class.register(); } @@ -379,6 +472,83 @@ impl BitmapView { } } +// --------------------------------------------------------------------------- +// Window delegate for handling resize and close events +// --------------------------------------------------------------------------- + +/// Register the `WeWindowDelegate` class if not already registered. +/// +/// The class implements: +/// - `windowDidResize:` — marks the content view as needing display +/// - `windowShouldClose:` — returns YES (allows closing) +fn register_we_window_delegate_class() { + if class!("WeWindowDelegate").is_some() { + return; + } + + let superclass = class!("NSObject").expect("NSObject not found"); + let delegate_class = Class::allocate(superclass, c"WeWindowDelegate", 0) + .expect("failed to allocate WeWindowDelegate class"); + + // windowDidResize: — mark the content view as needing display + extern "C" fn window_did_resize( + _this: *mut c_void, + _sel: *mut c_void, + notification: *mut c_void, + ) { + let win: *mut c_void = msg_send![notification, object]; + if win.is_null() { + return; + } + let content_view: *mut c_void = msg_send![win, contentView]; + if content_view.is_null() { + return; + } + let _: *mut c_void = msg_send![content_view, setNeedsDisplay: true]; + } + + let sel = Sel::register(c"windowDidResize:"); + delegate_class.add_method( + sel, + unsafe { std::mem::transmute::<*const (), Imp>(window_did_resize as *const ()) }, + c"v@:@", + ); + + // windowShouldClose: -> YES + extern "C" fn window_should_close( + _this: *mut c_void, + _sel: *mut c_void, + _sender: *mut c_void, + ) -> bool { + true + } + + let sel = Sel::register(c"windowShouldClose:"); + delegate_class.add_method( + sel, + unsafe { std::mem::transmute::<*const (), Imp>(window_should_close as *const ()) }, + c"B@:@", + ); + + delegate_class.register(); +} + +/// Install a window delegate that handles resize and close events. +/// +/// Creates a `WeWindowDelegate` class (if not already registered) and sets +/// an instance as the window's delegate. The app delegate then terminates +/// the app when the last window closes. +pub fn install_window_delegate(window: &Window) { + register_we_window_delegate_class(); + + let cls = class!("WeWindowDelegate").expect("WeWindowDelegate not found"); + let delegate: *mut c_void = msg_send![cls.as_ptr(), alloc]; + let delegate: *mut c_void = msg_send![delegate, init]; + let delegate = + unsafe { Id::from_raw(delegate as *mut _) }.expect("WeWindowDelegate init failed"); + window.set_delegate(&delegate); +} + // --------------------------------------------------------------------------- // App delegate for handling window close -> app termination // --------------------------------------------------------------------------- @@ -517,4 +687,57 @@ mod tests { let view = BitmapView::new(frame, &bitmap); assert!(!view.id().as_ptr().is_null()); } + + #[test] + fn we_view_accepts_first_responder() { + let _pool = AutoreleasePool::new(); + let bitmap = BitmapContext::new(100, 100).expect("should create bitmap context"); + let frame = NSRect::new(0.0, 0.0, 100.0, 100.0); + let view = BitmapView::new(frame, &bitmap); + let accepts: bool = msg_send![view.id().as_ptr(), acceptsFirstResponder]; + assert!(accepts, "WeView should accept first responder"); + } + + #[test] + fn we_view_responds_to_key_down() { + let _pool = AutoreleasePool::new(); + register_we_view_class(); + let cls = class!("WeView").expect("WeView should be registered"); + let sel = Sel::register(c"keyDown:"); + let instances_respond: bool = + msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()]; + assert!(instances_respond, "WeView should respond to keyDown:"); + } + + #[test] + fn we_view_responds_to_mouse_events() { + let _pool = AutoreleasePool::new(); + register_we_view_class(); + let cls = class!("WeView").expect("WeView should be registered"); + + for sel_name in [c"mouseDown:", c"mouseUp:", c"mouseMoved:"] { + let sel = Sel::register(sel_name); + let responds: bool = msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()]; + assert!(responds, "WeView should respond to {:?}", sel_name); + } + } + + #[test] + fn we_window_delegate_class_registration() { + register_we_window_delegate_class(); + let cls = class!("WeWindowDelegate"); + assert!(cls.is_some(), "WeWindowDelegate class should be registered"); + } + + #[test] + fn we_window_delegate_responds_to_resize() { + register_we_window_delegate_class(); + let cls = class!("WeWindowDelegate").expect("WeWindowDelegate should be registered"); + let sel = Sel::register(c"windowDidResize:"); + let responds: bool = msg_send![cls.as_ptr(), instancesRespondToSelector: sel.as_ptr()]; + assert!( + responds, + "WeWindowDelegate should respond to windowDidResize:" + ); + } } -- 2.51.2