From 09a95d37047647463b16aa52a04613cd8afb7328 Mon Sep 17 00:00:00 2001 From: paravozz Date: Thu, 23 Apr 2026 01:22:06 +0100 Subject: [PATCH] fix(macos): override hitTest: so first click in non-key window reaches mouseDown: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rust-subclass NSView composes with an NSOpenGLView child (src/gl/macos.rs) that hosts the OpenGL render context. The GL subview covers the parent frame, so `[NSView hitTest:]` returns the child for every click. NSOpenGLView inherits the default `acceptsFirstMouse:` which returns `NO`, so AppKit treats the first click in a non-key window as a pure activation click — it never dispatches `mouseDown:` to our view. That's the "first click dead zone" tracked in baseview#129, #202 (filed by Justin Frankel of REAPER), and #169. Override `hitTest:` on the root NSView: respect the superclass bounds/ hidden check (return nil out-of-bounds), then collapse the result to `self` only when the hit lands on our own GL render subview — compared by pointer equality against the NSOpenGLView stored in `GlContext`. The root overrides `acceptsFirstMouse:` → `YES`, so AppKit now asks us, we accept, and `mouseDown:` is dispatched on the first click. Pointer equality (rather than `isKindOfClass: NSOpenGLView`) keeps the redirect scoped exactly to our own render child: any other subview — consumer-added overlays, future IME NSTextViews, NSScrollView — passes through unchanged. Fixes baseview#129 / #202 / #169. --- src/gl/macos.rs | 8 ++++++++ src/gl/mod.rs | 9 +++++++++ src/macos/view.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++ src/macos/window.rs | 2 +- 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/gl/macos.rs b/src/gl/macos.rs index f68b796a..28bb2060 100644 --- a/src/gl/macos.rs +++ b/src/gl/macos.rs @@ -142,6 +142,14 @@ impl GlContext { let _: () = msg_send![self.view, setNeedsDisplay: YES]; } } + + /// Pointer to the `NSOpenGLView` this context renders into. Used by + /// the parent `NSView`'s `hitTest:` override to collapse hits on the + /// render subview to the parent, so AppKit routes `mouseDown:` on + /// first click in non-key windows. + pub(crate) fn ns_view(&self) -> id { + self.view + } } impl Drop for GlContext { diff --git a/src/gl/mod.rs b/src/gl/mod.rs index 488cfd7f..6f277e92 100644 --- a/src/gl/mod.rs +++ b/src/gl/mod.rs @@ -112,4 +112,13 @@ impl GlContext { pub(crate) fn resize(&self, size: cocoa::foundation::NSSize) { self.context.resize(size); } + + /// Pointer to the `NSOpenGLView` this context renders into. Used by + /// the parent `NSView`'s `hitTest:` override to collapse hits on the + /// render subview to the parent, so AppKit routes `mouseDown:` on + /// first click in non-key windows. + #[cfg(target_os = "macos")] + pub(crate) fn ns_view(&self) -> cocoa::base::id { + self.context.ns_view() + } } diff --git a/src/macos/view.rs b/src/macos/view.rs index 063ba24c..ac306548 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -172,6 +172,10 @@ unsafe fn create_view_class() -> &'static Class { sel!(viewWillMoveToWindow:), view_will_move_to_window as extern "C" fn(&Object, Sel, id), ); + class.add_method( + sel!(hitTest:), + hit_test as extern "C" fn(&Object, Sel, NSPoint) -> id, + ); class.add_method( sel!(updateTrackingAreas:), update_tracking_areas as extern "C" fn(&Object, Sel, id), @@ -346,6 +350,51 @@ unsafe fn reinit_tracking_area(this: &Object, tracking_area: *mut Object) { ]; } +/// `hitTest:` override that collapses hits on baseview's internal +/// OpenGL render subview to this NSView. +/// +/// `src/gl/macos.rs` attaches an `NSOpenGLView` as a subview of this +/// view so the GL context is isolated from event handling. The side +/// effect is that `[NSView hitTest:]` returns the GL subview for +/// every click inside our frame — `NSOpenGLView` inherits the +/// default `acceptsFirstMouse:` which returns `NO`, so AppKit treats +/// the first click in a non-key window as an activation click and +/// never dispatches `mouseDown:`. That's the "first click dead zone" +/// symptom reported in baseview#129 / #202 / #169. +/// +/// Fix: if the hit lands on our own GL render subview (pointer +/// equality against the `NSOpenGLView` stored in `GlContext`), +/// collapse the result to `self`. AppKit then asks US about +/// `acceptsFirstMouse:` (we return `YES`), and `mouseDown:` is +/// dispatched on the first click. Hits on any other subview pass +/// through unchanged — we only redirect our own render child, not +/// anything the consumer may add. +/// +/// No-op without the `opengl` feature: there's no GL subview to +/// collapse, so the override pass-through is equivalent to the +/// default implementation. +extern "C" fn hit_test(this: &Object, _sel: Sel, point: NSPoint) -> id { + let super_result: id = unsafe { + let superclass = msg_send![this, superclass]; + msg_send![super(this, superclass), hitTest: point] + }; + if super_result == nil { + return nil; + } + + #[cfg(feature = "opengl")] + unsafe { + let state = WindowState::from_view(this); + if let Some(gl_context) = state.window_inner.gl_context.as_ref() { + if super_result == gl_context.ns_view() { + return this as *const _ as id; + } + } + } + + super_result +} + extern "C" fn view_will_move_to_window(this: &Object, _self: Sel, new_window: id) { unsafe { let tracking_areas: *mut Object = msg_send![this, trackingAreas]; diff --git a/src/macos/window.rs b/src/macos/window.rs index 57bca108..d3d7031f 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -65,7 +65,7 @@ pub(super) struct WindowInner { ns_view: id, #[cfg(feature = "opengl")] - gl_context: Option, + pub(super) gl_context: Option, } impl WindowInner {