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 from 2126dc3 to 2ebd0f7 Compare April 19, 2026 00:15
…ut has focus

Adopts NSTextInputClient on the custom NSView and routes `keyDown:`
through `[[self inputContext] handleEvent:]` when the application
declares a text input has focus (new `WindowHandler::has_text_focus`
trait method, default `false`). Adds a matching `performKeyEquivalent:`
override, and activates / deactivates the input context on
`becomeFirstResponder` / `resignFirstResponder`.

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 2ebd0f7 to 421bbfa Compare April 19, 2026 00:21
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