Skip to content

fix(macos): route keyboard through NSTextInputContext when a text input has focus#226

Open
paravozz wants to merge 1 commit intoRustAudio:masterfrom
paravozz:fix/macos-nstextinputclient-keyboard-routing
Open

fix(macos): route keyboard through NSTextInputContext when a text input has focus#226
paravozz wants to merge 1 commit intoRustAudio:masterfrom
paravozz:fix/macos-nstextinputclient-keyboard-routing

Conversation

@paravozz
Copy link
Copy Markdown

Summary

baseview on macOS delivers keyboard input via keyDown: on its custom NSView subclass, translating the NSEvent's keyCode and characters into a keyboard-types::KeyboardEvent. This works for Latin letters but has several gaps for plugin hosting on macOS:

  • Typing into a plugin's text field produces a system beep per keypress, because the event walks the responder chain up to the host window.
  • IME (Japanese, Chinese, Korean) doesn't work. Dead-key composition (Option+e then e → é on macOS US) doesn't work.
  • In REAPER, the per-plugin "Send all keyboard input to plug-in" toggle forwards events to the NSView, but AppKit drops them because the view's input context isn't marked active.

Standard Cocoa text views don't exhibit any of this because AppKit routes their keys through NSTextInputContext, triggering insertText:replacementRange: / doCommandBySelector: callbacks. JUCE plugin views adopt the same pattern. This patch brings baseview's custom NSView into that pipeline when the app declares a text input is focused.

Change

  1. New WindowHandler::has_text_focus() trait method, default false. Apps override it when a focused widget expects text input.

  2. src/macos/view.rs:

    • NSTextInputClient protocol conformance + the method suite. insertText:replacementRange: and doCommandBySelector: dispatch a KeyboardEvent by re-processing the stored NSEvent; the rest return sentinels.
    • Custom keyDown: that routes through [[self inputContext] handleEvent:] when has_text_focus() is true; falls back to the existing raw-keycode path when false.
    • performKeyEquivalent: override routing through the same input-context path — AppKit calls this before keyDown: for potential menu shortcuts.
    • [inputContext activate] / deactivate in becomeFirstResponder / resignFirstResponder. Marks our input context as globally active while the view has focus; handleEvent: requires this.
  3. src/macos/window.rs:

    • WindowState.current_key_event holds the in-flight NSEvent so NSTextInputClient callbacks can access the original keyCode / modifiers.
    • WindowState::has_text_focus() helper that queries the WindowHandler.

Why this shape

  • has_text_focus() is an app-level opt-in because baseview doesn't know what a "text input" is in the consumer's model. The consumer knows; baseview asks at dispatch time.
  • Gated on text focus rather than always-on because (a) it changes the KeyboardEvent synthesis path, and (b) non-text views (e.g. game controls) benefit from the raw-keycode path.
  • NSTextInputClient callbacks re-process the stored NSEvent rather than synthesize from scratch — process_native_event already does the right keyCode → Code mapping; we just dispatch through trigger_event at the moment AppKit finishes decoding.
  • Pure AppKit change. No VST3 or host-specific code — benefits every baseview consumer with text input in a macOS plugin window.

Test plan

  • macOS 15 (Apple Silicon), REAPER 7.x and Ableton Live 12: typing into a focused vizia::Textbox in a nih-plug + vizia-plug plugin works end-to-end with no system beep.
  • When no text input is focused, the old keyDown: path is preserved — host shortcuts like space = transport continue to reach the DAW.
  • Apps that don't override has_text_focus() see identical behaviour to pre-patch.

Context

@paravozz paravozz force-pushed the fix/macos-nstextinputclient-keyboard-routing branch 2 times, most recently from 2ebd0f7 to 421bbfa Compare April 19, 2026 00:21
@paravozz paravozz marked this pull request as draft April 24, 2026 14:10
@paravozz paravozz force-pushed the fix/macos-nstextinputclient-keyboard-routing branch 3 times, most recently from 421bbfa to 3eef708 Compare April 24, 2026 19:32
…ut has focus

Adopts NSTextInputClient on the custom NSView and routes `keyDown:` /
`performKeyEquivalent:` through `[[self inputContext] handleEvent:]`
when the application declares a text input has focus (new
`WindowHandler::has_text_focus` trait method, default `false`).
`insertText:replacementRange:` uses the NSString `text` parameter
AppKit hands us — already IME / dead-key / keyboard-layout decoded —
as the `Key::Character`; `doCommandBySelector:` re-uses the stored
NSEvent for the command's physical Code.

Before this, plugins hosting text inputs on macOS produced a system
beep per keypress, couldn't use IME or dead-key composition, and lost
keys to the host's accelerator table even when the host was configured
to forward them to the plugin.

Apps that do not override `has_text_focus()` get the old behaviour.
The opt-in avoids changing key semantics for non-text-input views
(game controls etc.) that rely on the raw-keycode path.

Companion to vizia/vizia#630 and #631 on the consumer side.
Addresses vizia/vizia-plug#7.
@paravozz paravozz force-pushed the fix/macos-nstextinputclient-keyboard-routing branch from 3eef708 to 8e5a172 Compare April 24, 2026 19:41
@paravozz paravozz marked this pull request as ready for review April 24, 2026 21:37
paravozz added a commit to paravozz/baseview that referenced this pull request Apr 25, 2026
…bitform)

Applies RustAudio#232 (objc2 macros migration + four latent-bug fixes) on top
of integration/bitform's existing four commits (keyboard-types bump,
RustAudio#226 NSTextInputClient, RustAudio#228 hitTest first-click, RustAudio#230 panic-fix).

Bitform's [patch.baseview] points at this branch via git source for
downstream consumption while RustAudio#232 is in upstream review.

Includes both the macros migration itself and the four latent-bug
fixes (isARepeat gate, keyCode hoist, tracking-areas guard, three
@encode mismatches), translated to apply on top of integration/bitform's
NSTextInputClient additions (which RustAudio#232's upstream-PR base does not
have, since RustAudio#232 is filed against upstream/master).

Replaces 5816b44 (was a fixes-only-no-migration attempt) since
that was the wrong shape; the integration branch needs the full
migration to align with RustAudio#232's eventual landing.

Becomes redundant when RustAudio#232 merges and vizia's baseview pin moves
past it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant