fix(macos): route first click in non-key window to mouseDown: via hitTest: override#228
fix(macos): route first click in non-key window to mouseDown: via hitTest: override#228paravozz wants to merge 1 commit intoRustAudio:masterfrom
Conversation
| unsafe { | ||
| let gl_class = class!(NSOpenGLView); | ||
| let is_gl_subview: BOOL = msg_send![super_result, isKindOfClass: gl_class]; | ||
| if is_gl_subview == YES { | ||
| return this as *const _ as id; | ||
| } | ||
| } |
There was a problem hiding this comment.
This is a really strange way to achieve this. Here is a list of alternative approaches that I think would be better, in order from simple to complex:
- We literally store a pointer to the specific
NSOpenGLViewyou are checking for in theviewfield ofGlContext. Just check ifsuper_resultis equal to thatviewpointer. I don't know why you would feel the need to check the class of theNSView. - Subclass
NSOpenGLViewand overridehitTestto always returnnil. - Get rid of the
NSOpenGLViewentirely and just create anNSOpenGLContextinstead.
| /// hierarchy. `viewWillMoveToWindow:` above also calls | ||
| /// `makeFirstResponder:`, but at that point the view is not yet | ||
| /// attached to the target window, so the request can silently fail. | ||
| /// The retry here runs after the view has been added — the canonical |
There was a problem hiding this comment.
If the makeFirstResponder: call in viewWillMoveToWindow: does nothing, why would you just leave it there instead of removing it? Either this is the correct place for it, in which case the other call should be removed, or there is some reason to keep the other call around, in which case this comment is incorrect. Which is it?
| extern "C" fn view_did_move_to_window(this: &Object, _sel: Sel) { | ||
| unsafe { | ||
| let window: id = msg_send![this, window]; | ||
| if window != nil { | ||
| let _: BOOL = msg_send![window, makeFirstResponder: this]; | ||
| } | ||
|
|
||
| let superclass = msg_send![this, superclass]; | ||
| let _: () = msg_send![super(this, superclass), viewDidMoveToWindow]; | ||
| } | ||
| } |
There was a problem hiding this comment.
I'm pretty skeptical that this part of the change is necessary for the actual bug fix being targeted by this PR (i.e., the first click on the plugin window not registering as an event). Whether Baseview's NSView successfully becomes the first responder or not when it first gets added to the window doesn't seem to have anything to do with the actual problem, which is that mouse events are being routed to the child NSOpenGLView. Have you tested without this change?
If it is actually the case that the call to makeFirstResponder: in viewWillMoveToWindow: does nothing, it does seem like the correct thing would be to move that call to the viewDidMoveToWindow: method (or to just remove it entirely, which also seems fine to me). However, including that change in this PR specifically is misleading, since it makes it seem like it has something to do with the fix for the first click issue.
|
If you change the |
…s mouseDown: 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, RustAudio#202 (filed by Justin Frankel of REAPER), and RustAudio#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 / RustAudio#202 / RustAudio#169.
ce9ff7e to
09a95d3
Compare
|
@micahrj Thanks! Force-pushed with both addressed:
|
|
It looks like the macOS build is failing due to this issue in the I can look into putting up a PR to address this. |
Summary
macOS audio-plugin hosts (Ableton Live, REAPER, Bitwig) show a "first-click dead zone" on every baseview-based plugin: when the plugin window isn't key, the first click inside it is absorbed by AppKit as an activation click and never reaches
mouseDown:. Every subsequent click works. This is #129 / #202 / #169 — same root cause, different reported symptoms.This PR fixes it by overriding
hitTest:on the rootNSViewwith a narrowly-scoped collapse toselfwhen the hit lands on the internalNSOpenGLViewrender subview.Root cause
src/gl/macos.rs:89–99attaches anNSOpenGLViewas a subview of the root to host the GL context. The GL subview covers the root's frame, so[NSView hitTest:]returns the GL subview for every click.NSOpenGLViewinherits the defaultacceptsFirstMouse:which returnsNO— so when the plugin window isn't key, AppKit treats the first click as a pure activation click and never dispatchesmouseDown:to any view. The root'sacceptsFirstMouse:override (which returnsYES) is never consulted because AppKit asks the hit-test target.I verified this with selector-level logging in a real plugin (vizia-plug-based synth) running in both Ableton Live 12.3.7 and REAPER 7.69 on macOS 15.5:
mouseMoved:fires normally (tracking areas hand them to the root regardless of hit depth).hitTest:returns an object other thanselffor every click.acceptsFirstMouse:is never called on any selector I added logging to.mouseDown:is never called on the first click.acceptsFirstMouse:is irrelevant andmouseDown:fires normally.Why existing proposals don't close this
makeFirstResponder:inside themouseDown:macro) — can't help:mouseDown:doesn't fire on the first click, so there's no hook point. Fixes downstream keyboard-focus for clicks barebones x11 window #2+, but the The initial click is ignored on macOS, requires mouse movement before the second click works correctly #129 first-click symptom remains. This also explains the "triggered on every mouse click which is incorrect" objection raised on feat: add focus handling to macOS view for proper key event forwarding #203 — scoping athitTest:is more precise.Window::focus()API) — requires every downstream GUI (vizia, egui, iced) to know when to call it. Even when they do, the underlying AppKit event-routing is still broken — it only papers over the keyboard-focus symptom after the user has already lost their first click.The fix
Two methods added, registered via
add_methodincreate_view_class:hitTest:— collapses in-bounds hits onNSOpenGLViewtoself. AppKit now asks the root view aboutacceptsFirstMouse:, we returnYES, the first click is delivered.viewDidMoveToWindow— callsmakeFirstResponder:after the view is in its target window.viewWillMoveToWindow:already calls it, but that runs before attachment and can silently fail.Scope / safety
isKindOfClass: NSOpenGLViewgate ensures we don't break future legitimate child views (e.g. anNSTextViewoverlay for IME,NSScrollView). Those keep receiving hits normally.NSOpenGLViewchild,[super hitTest:]returns eitherself(in-bounds, no subviews) ornil(out-of-bounds). Neither triggers the collapse.NSOpenGLViewstill exists as a subview and still draws into its GL context. We only redirect event routing, not rendering.draggingEntered:/registeredForDraggedTypes:, nothitTest:.accessibilityHitTest:, which is separate.Testing
Tested end-to-end in Ableton Live 12.3.7 and REAPER 7.69 on macOS 15.5:
WindowEvent::Presssequence) works on first click.Related
onKeyDownreturning the rightTResult).set_focus(bool)interface for getting and losing keyboard focus #152 / Fix keyboard focus for parented Windows on Windows OS #225 (Windows-specific focus, different code path).