Skip to content

WebView: forward keys to host responder chain by default#25

Merged
jakepenn merged 1 commit into
minimal_masterfrom
webview-keyboard-passthrough
May 22, 2026
Merged

WebView: forward keys to host responder chain by default#25
jakepenn merged 1 commit into
minimal_masterfrom
webview-keyboard-passthrough

Conversation

@jakepenn

Copy link
Copy Markdown
Member

Summary

  • Adds WebBrowserComponent::setEditableFocusActive(bool) + an auto-registered __juceSetEditableFocusActive JS native function so a frontend can flip key routing between the host responder chain and the embedded DOM
  • macOS: extends the existing WebViewKeyEquivalentResponder<WKWebView> with keyDown:/keyUp: overrides and adjusts performKeyEquivalent: so Cmd-shortcuts bubble to the host outside of editable focus
  • Windows: switches to CreateCoreWebView2ControllerWithOptions with AllowHostInputProcessing=TRUE (capability-gated fallback to plain Create) and subscribes AcceleratorKeyPressed

Motivation

The OS web surface (WKWebView on macOS, WebView2 on Windows) becomes first responder on click and otherwise swallows every keystroke. For an audio plugin embedded in a DAW that breaks transport (spacebar), MIDI-typing, save shortcuts, and basically every host keybinding the moment the user touches the WebView. JUCE doesn't currently expose a way to fix this, so plugin authors either eat the regression or write their own NSView/HWND swizzles outside JUCE.

This patch makes JUCE's WebBrowserComponent forward keys to the host responder chain by default and lets the embedded JS opt back into DOM key routing when an editable element has focus.

Public API

/** Controls whether key events received by the underlying OS web surface should be
    forwarded to the host responder chain instead of being consumed by the web view.
    Default: forwards to host. Call with true while an editable DOM element has focus. */
void WebBrowserComponent::setEditableFocusActive (bool editableFocusActive);

The JS side gets a JUCE-injected native function:

import { getNativeFunction } from 'juce-framework-frontend';
const setEditableFocus = getNativeFunction('__juceSetEditableFocusActive');
document.addEventListener('focusin', e => {
    const t = e.target;
    setEditableFocus(t instanceof HTMLInputElement
                     || t instanceof HTMLTextAreaElement
                     || t instanceof HTMLSelectElement
                     || (t instanceof HTMLElement && t.isContentEditable));
});

__juceSetEditableFocusActive is only registered when native integration is already in use (withNativeIntegrationEnabled was set, or any other native function was registered). That keeps the security model intact — components loading untrusted content don't suddenly have native-integration forced on by an internal JUCE function.

Platform notes

macOS — uses the existing WebViewKeyEquivalentResponder<WKWebView> ObjC class subclass machinery; a new editableFocusActiveMemberName ivar mirrors the existing LastFocusChange* ivar pattern, holding a pointer to a std::atomic<bool> owned by WKWebViewImpl (also wired into the legacy WebViewImpl for consistency). The keyDown:/keyUp: overrides forward to nextResponder by default; performKeyEquivalent: returns NO outside editable focus so AppKit walks past the WebView and existing host Cmd-shortcuts still work.

WindowsCreateCoreWebView2ControllerWithOptions is the only way to enable AllowHostInputProcessing, which is a creation-time option not a runtime toggle. The new code path queries ICoreWebView2Environment10 + ICoreWebView2ControllerOptions2; if either is unavailable (older WebView2 SDK / runtime / locked-down enterprise environment), the original CreateCoreWebView2Controller is used and AcceleratorKeyPressed alone covers the accelerator-key subset (Tab, Esc, F-keys, modifier combos). AcceleratorKeyPressed's put_Handled value tracks the same editableFocusActive atomic so editing-mode behavior is consistent across the two code paths.

Test plan

  • Built current_VST3 (macOS Memory Rites) — compiles after replacing the captured [isEditableFocusActive] lambdas with [] lambdas (JUCE's toFnPtr requires captureless lambdas for ObjCClass::addMethod)
  • Manual smoke in standalone — QWERTY MIDI keyboard works, no regression
  • Manual smoke in Logic / Ableton on macOS — confirm spacebar transport reaches host while WebView has focus
  • Manual smoke in Reaper / FL Studio / Studio One on Windows — confirm character pass-through via AllowHostInputProcessing and accelerator-key pass-through via AcceleratorKeyPressed
  • Verify Tab/Esc behavior inside an editable element doesn't get yanked by AllowHostInputProcessing (frontend uses a capture-phase guard for these two keys while editable focus is active)

🤖 Generated with Claude Code

The OS web surface (WKWebView on macOS, WebView2 on Windows) becomes first
responder on click and otherwise swallows every keystroke, which prevents
plugin hosts from receiving transport, MIDI-typing, and DAW shortcut keys
while the WebView has focus. This patch flips the default routing so keys
go to the host responder chain unless the JS layer reports that an editable
DOM element has focus.

Public surface:
- WebBrowserComponent::setEditableFocusActive(bool) — runtime toggle
- Auto-registered native function __juceSetEditableFocusActive — JS entry
  point exposed via JUCE's existing getNativeFunction helper, only
  registered when native integration is already in use (no security
  regression for components loading untrusted content)

macOS: extends the existing WebViewKeyEquivalentResponder<WKWebView> with
keyDown:/keyUp: overrides and adjusts performKeyEquivalent: so Cmd-shortcuts
also bubble to the host when not editing. State is held in a std::atomic<bool>
exposed per-instance through an ivar pointer, mirroring the existing
LastFocusChange* ivar pattern. Wired into both WKWebViewImpl and the legacy
WebViewImpl for consistency.

Windows: switches CreateCoreWebView2Controller to
CreateCoreWebView2ControllerWithOptions with AllowHostInputProcessing=TRUE so
character keys (spacebar, QWERTY) reach the host's window proc, with a
capability-gated fallback to plain Create when the environment or controller
options interface isn't available. AcceleratorKeyPressed is subscribed
unconditionally to cover Tab/Esc/F-keys/modifier combos on older runtimes —
its put_Handled value tracks editableFocusActive too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jakepenn jakepenn merged commit 75b0697 into minimal_master May 22, 2026
1 check failed
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