7
44
Fork
You've already forked nice-plug
22

Implement IPlugView::onKeyDown with Editor::on_virtual_key_from_host hook #9

Merged
BillyDM merged 1 commit from paravozz/nih-plug:fix/vst3-on-key-down into main 2026年04月28日 19:46:46 +02:00
Contributor
Copy link

Summary

Adds an Editor::on_vst3_virtual_key_down trait hook the VST3 wrapper calls from IPlugView::onKeyDown, so plugin editors can route virtual-key events (Space, Backspace, arrows, F-keys, etc.) that the host would otherwise intercept as accelerators before they reach the plugin's native view.

API

implEditorforMyEditor{fn on_virtual_key_from_host(&self,key_code: KeyCode,is_down: bool,modifiers: Modifiers)-> bool {// return true if the editor consumed the key
}}

KeyCode mirrors the VST3 SDK VirtualKeyCodes enum (pluginterfaces/base/keycodes.h), with named variants for Backspace, Tab, Return, arrows, Numpad, F1–F24, etc. Modifiers wraps the VST3 KeyModifier bitfield with .shift(), .alt(), .command(), .control(), .any(), .none() accessors.

VST3 wrapper behaviour

The wrapper forwards events only when the raw key_code lies in [VKEY_FIRST_CODE, VKEY_LAST_CODE] (1..=77 in the SDK enum). Values at VKEY_FIRST_ASCII = 128 and above encode printable ASCII characters and are not routed here; those flow through the plugin window's native keyboard path (on macOS, AppKit keyDown: + NSTextInputContext), which is the correct place for text input.

Motivation

REAPER (and some other hosts) capture certain keys (Space, Cmd-shortcuts, Backspace with some key-maps) as transport / accelerator commands before they reach the plugin's NSView. Before this hook, those keys were silently eaten even when a textbox in the plugin's editor had focus. Plugins can now consult the hook and claim the key only while editing text.

Usage

Wrapper-side, the GUI framework (or plugin) implements the hook and claims the key only when it has something focused that should consume it. A minimal pattern:

fn on_vst3_virtual_key_down(&self,key_code: KeyCode,modifiers: Modifiers)-> bool {if!self.has_text_focus()||modifiers.any(){returnfalse;}matchkey_code{KeyCode::Backspace=>{self.delete_back();true}KeyCode::Return=>{self.submit();true}KeyCode::Space=>{self.insert(' ');true}_=>false,}}

Returning true translates to kResultTrue so the host skips its own accelerator handling; returning false yields kResultFalse and the host proceeds as normal.

Tested

  • macOS / REAPER — Space, Backspace, Enter, arrows, Numpad in a focused textbox; Space outside textbox reaches REAPER transport (when Send-all-keyboard-input is off).
  • macOS / Ableton Live — basic type-in-textbox flow.

CLAP

Left unimplemented for now; CLAP plugins manage keyboard focus differently (clap_plugin_gui::set_keyboard_focus) and don't need an equivalent trait hook.

## Summary Adds an `Editor::on_vst3_virtual_key_down` trait hook the VST3 wrapper calls from `IPlugView::onKeyDown`, so plugin editors can route virtual-key events (Space, Backspace, arrows, F-keys, etc.) that the host would otherwise intercept as accelerators before they reach the plugin's native view. ## API ```rust impl Editor for MyEditor { fn on_virtual_key_from_host(&self, key_code: KeyCode, is_down: bool, modifiers: Modifiers) -> bool { // return true if the editor consumed the key } } ``` `KeyCode` mirrors the VST3 SDK `VirtualKeyCodes` enum (`pluginterfaces/base/keycodes.h`), with named variants for Backspace, Tab, Return, arrows, Numpad, F1–F24, etc. `Modifiers` wraps the VST3 `KeyModifier` bitfield with `.shift()`, `.alt()`, `.command()`, `.control()`, `.any()`, `.none()` accessors. ## VST3 wrapper behaviour The wrapper forwards events only when the raw `key_code` lies in `[VKEY_FIRST_CODE, VKEY_LAST_CODE]` (1..=77 in the SDK enum). Values at `VKEY_FIRST_ASCII = 128` and above encode printable ASCII characters and are not routed here; those flow through the plugin window's native keyboard path (on macOS, AppKit `keyDown:` + NSTextInputContext), which is the correct place for text input. ## Motivation REAPER (and some other hosts) capture certain keys (Space, Cmd-shortcuts, Backspace with some key-maps) as transport / accelerator commands before they reach the plugin's NSView. Before this hook, those keys were silently eaten even when a textbox in the plugin's editor had focus. Plugins can now consult the hook and claim the key only while editing text. ## Usage Wrapper-side, the GUI framework (or plugin) implements the hook and claims the key only when it has something focused that should consume it. A minimal pattern: ```rust fn on_vst3_virtual_key_down(&self, key_code: KeyCode, modifiers: Modifiers) -> bool { if !self.has_text_focus() || modifiers.any() { return false; } match key_code { KeyCode::Backspace => { self.delete_back(); true } KeyCode::Return => { self.submit(); true } KeyCode::Space => { self.insert(' '); true } _ => false, } } ``` Returning `true` translates to `kResultTrue` so the host skips its own accelerator handling; returning `false` yields `kResultFalse` and the host proceeds as normal. ## Tested - macOS / REAPER — Space, Backspace, Enter, arrows, Numpad in a focused textbox; Space outside textbox reaches REAPER transport (when Send-all-keyboard-input is off). - macOS / Ableton Live — basic type-in-textbox flow. ## CLAP Left unimplemented for now; CLAP plugins manage keyboard focus differently (`clap_plugin_gui::set_keyboard_focus`) and don't need an equivalent trait hook.
paravozz force-pushed fix/vst3-on-key-down from bc03e2d80d to a3bd4bf8d3 2026年04月24日 21:56:49 +02:00 Compare
@ -72,0 +79,4 @@
/// intercept certain keys (space, Cmd-shortcuts) before they reach the
/// plugin's native view. The editor should only return `true` if a text
/// input in the editor currently has focus and can consume the character.
fn on_key_down(&self,_character: Option<char>)-> bool {
Owner
Copy link

If we are only forwarding virtual non-character keys to the plugin editor, then it doesn't make sense to have a character: Option<char> argument since it will always be None. Instead, the argument should just be the raw i16 key_code value (or an enum of the key_code).

Also, this trait method should be renamed to something like on_vst3_virtual_key_down to make it clear that it is only for virtual keys sent from the VST3 API.

If we are only forwarding virtual non-character keys to the plugin editor, then it doesn't make sense to have a `character: Option<char>` argument since it will always be `None`. Instead, the argument should just be the raw `i16` key_code value (or an enum of the key_code). Also, this trait method should be renamed to something like `on_vst3_virtual_key_down` to make it clear that it is *only* for virtual keys sent from the VST3 API.
Author
Contributor
Copy link

Thanks, done. Renamed to on_vst3_virtual_key_down and dropped the character arg. With the proper range gate (see other reply) the event is always a virtual key, so any character associated with it is recoverable from the KeyCode variant itself (Space' ', Numpad0'0', Equals'=', etc.).

Thanks, done. Renamed to `on_vst3_virtual_key_down` and dropped the `character` arg. With the proper range gate (see other reply) the event is always a virtual key, so any character associated with it is recoverable from the `KeyCode` variant itself (`Space` → `' '`, `Numpad0` → `'0'`, `Equals` → `'='`, etc.).
BillyDM marked this conversation as resolved
@ -346,0 +348,4 @@
// etc. Plain printable characters arrive with `key_code = 0` and
// flow through the normal AppKit `keyDown:` path, which already
// handles text input via NSTextInputContext. Consuming them here
// would break letter input.
Owner
Copy link

Where in the VST3 docs does it say that plain printable characters arrive with key_code = 0?

All I can find is this, which doesn't really say much. https://steinbergmedia.github.io/vst3_doc/base/classSteinberg_1_1IPlugView.html#a759b576f046e699c84dc07d579600b1b

It appears that ascii characters may actually have a key_code value >= 128: https://steinbergmedia.github.io/vst3_doc/base/keycodes_8h.html

So maybe instead of if key_code == 0 {, it should be if key_code < VKEY_FIRST_CODE || key_code > VKEY_LAST_CODE {?

Where in the VST3 docs does it say that plain printable characters arrive with `key_code = 0`? All I can find is this, which doesn't really say much. https://steinbergmedia.github.io/vst3_doc/base/classSteinberg_1_1IPlugView.html#a759b576f046e699c84dc07d579600b1b It appears that ascii characters may actually have a `key_code` value >= 128: https://steinbergmedia.github.io/vst3_doc/base/keycodes_8h.html So maybe instead of `if key_code == 0 {`, it should be `if key_code < VKEY_FIRST_CODE || key_code > VKEY_LAST_CODE {`?
Author
Contributor
Copy link

Good catch, thanks. I'd missed the VKEY_FIRST_ASCII side of the header when I first looked at it. Swapped the gate to VKEY_FIRST_CODE..=VKEY_LAST_CODE so ASCII-offset codes (128+) fall through to the native keyboard path too.

Good catch, thanks. I'd missed the `VKEY_FIRST_ASCII` side of the header when I first looked at it. Swapped the gate to `VKEY_FIRST_CODE..=VKEY_LAST_CODE` so ASCII-offset codes (128+) fall through to the native keyboard path too.
BillyDM marked this conversation as resolved
paravozz force-pushed fix/vst3-on-key-down from a3bd4bf8d3 to 38223f456e 2026年04月24日 22:22:47 +02:00 Compare
paravozz force-pushed fix/vst3-on-key-down from 38223f456e to 4cbf5ab76c 2026年04月24日 23:02:16 +02:00 Compare
paravozz changed title from (削除) Implement IPlugView::onKeyDown with Editor::on_key_down hook (削除ここまで) to Implement IPlugView::onKeyDown with Editor::on_vst3_virtual_key_down hook 2026年04月24日 23:05:27 +02:00
Author
Contributor
Copy link

@BillyDM Thanks for the review!

Quick note on why the API ended up slightly richer than strictly necessary: I went with a named KeyCode enum (mirroring the SDK's VirtualKeyCodes) and a Modifiers bitfield with named accessors instead of raw i16s primarily to avoid every wrapper/plugin duplicating the same 77-arm match against SDK magic numbers and the same kShiftKey/kAlternateKey/kCommandKey/kControlKey bit-twiddling. Lifting it into the trait means downstream integrations (vizia-plug here, a hypothetical future egui / iced one) can match on KeyCode::Backspace + modifiers.shift() directly.

The same types would map cleanly onto CLAP if an equivalent hook ever makes sense there. CLAP's set_keyboard_focus flow doesn't strictly need it today, but re-using KeyCode / Modifiers across APIs would keep the cross-wrapper surface small. Happy to narrow back to raw i16s if you'd rather keep the trait minimal for now.

@BillyDM Thanks for the review! Quick note on why the API ended up slightly richer than strictly necessary: I went with a named `KeyCode` enum (mirroring the SDK's `VirtualKeyCodes`) and a `Modifiers` bitfield with named accessors instead of raw `i16`s primarily to avoid every wrapper/plugin duplicating the same 77-arm match against SDK magic numbers and the same `kShiftKey`/`kAlternateKey`/`kCommandKey`/`kControlKey` bit-twiddling. Lifting it into the trait means downstream integrations (vizia-plug here, a hypothetical future egui / iced one) can match on `KeyCode::Backspace` + `modifiers.shift()` directly. The same types would map cleanly onto CLAP if an equivalent hook ever makes sense there. CLAP's `set_keyboard_focus` flow doesn't strictly need it today, but re-using `KeyCode` / `Modifiers` across APIs would keep the cross-wrapper surface small. Happy to narrow back to raw `i16`s if you'd rather keep the trait minimal for now.
BillyDM left a comment
Copy link

Sorry for the delay, I was busy this weekend.

Got a new review! Just some minor nitpicks.

Sorry for the delay, I was busy this weekend. Got a new review! Just some minor nitpicks.
@ -72,0 +94,4 @@
/// - `key_code`: the virtual key the host reports.
/// - `modifiers`: which modifier keys were held when the event was
/// generated.
fn on_vst3_virtual_key_down(&self,_key_code: KeyCode,_modifiers: Modifiers)-> bool {
Owner
Copy link

Since we are now abstracting this to potentially allow CLAP plugins to also use this, it might make more sense to rename the method to something like on_virtual_key_down_from_host.

Since we are now abstracting this to potentially allow CLAP plugins to also use this, it might make more sense to rename the method to something like `on_virtual_key_down_from_host`.
Owner
Copy link

Actually, now that I think about it, it might be better to also allow the method to receive key up events like this:

fn on_virtual_key_from_host(&self,_key_code: KeyCode,_is_down: bool,_modifiers: Modifiers)-> bool {

We don't want the host to also intercept key up events if the plugin is using them.

Actually, now that I think about it, it might be better to also allow the method to receive key up events like this: ```rust fn on_virtual_key_from_host(&self, _key_code: KeyCode, _is_down: bool, _modifiers: Modifiers) -> bool { ``` We don't want the host to also intercept key up events if the plugin is using them.
@ -114,0 +144,4 @@
/// Lowest VST3 virtual key code (corresponds to `KEY_BACK` in the VST3
/// SDK `VirtualKeyCodes` enum). Values below this are not virtual keys
/// in the VST3 sense.
pub(crate)constVKEY_FIRST_CODE: i16 =1;
Owner
Copy link

VST3-specific constants should live in the vst3 wrapper code.

VST3-specific constants should live in the vst3 wrapper code.
@ -114,0 +150,4 @@
/// SDK `VirtualKeyCodes` enum). Values in `VKEY_FIRST_ASCII = 128` and
/// above encode printable ASCII characters rather than virtual keys and
/// are not routed to [`Editor::on_vst3_virtual_key_down`].
pub(crate)constVKEY_LAST_CODE: i16 =77;
Owner
Copy link

Same for this constant.

Same for this constant.
@ -114,0 +157,4 @@
/// SDK `VirtualKeyCodes` enum (`pluginterfaces/base/keycodes.h`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pubenum KeyCode{
Owner
Copy link

This should be renamed to VirtualKeyCode to make it clear that it is only for non-character keys.

This should be renamed to `VirtualKeyCode` to make it clear that it is only for non-character keys.
@ -114,0 +254,4 @@
/// [`VKEY_FIRST_CODE`]`..=`[`VKEY_LAST_CODE`] first; codes outside
/// that range (including `0`, and ASCII offsets at
/// `VKEY_FIRST_ASCII = 128` and above) return `None`.
pub(crate)fn from_vst3(raw: i16)-> Option<Self>{
Owner
Copy link

The function that translates from VST3 to the abstraction should live in the vst3 wrapper.

The function that translates from VST3 to the abstraction should live in the vst3 wrapper.
@ -114,0 +346,4 @@
/// modifier has a named accessor; `any()` / `none()` check whether
/// any modifier is held.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pubstruct Modifiers(u8);
Owner
Copy link

Bit flags should be implemented with the bitflags crate for maximum ergonomics.

Bit flags should be implemented with the `bitflags` crate for maximum ergonomics.
@ -114,0 +358,4 @@
pubconstNONE: Self=Self(0);
/// Construct directly from the raw VST3 modifier bitmask.
pub(crate)fn from_vst3(raw: i16)-> Self{
Owner
Copy link

Again, this function should live in the vst3 wrapper.

Again, this function should live in the vst3 wrapper.
paravozz force-pushed fix/vst3-on-key-down from 4cbf5ab76c to b59ce19b31 2026年04月28日 00:50:53 +02:00 Compare
Author
Contributor
Copy link

@BillyDM thanks for the review! fixed everything

@BillyDM thanks for the review! fixed everything
paravozz changed title from (削除) Implement IPlugView::onKeyDown with Editor::on_vst3_virtual_key_down hook (削除ここまで) to Implement IPlugView::onKeyDown with Editor:: on_virtual_key_from_host hook 2026年04月28日 01:01:52 +02:00
paravozz changed title from (削除) Implement IPlugView::onKeyDown with Editor:: on_virtual_key_from_host hook (削除ここまで) to Implement IPlugView::onKeyDown with Editor::on_virtual_key_from_host hook 2026年04月28日 01:01:59 +02:00

Looks good, thanks!

Looks good, thanks!
Sign in to join this conversation.
No reviewers
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
RustAudio/nice-plug!9
Reference in a new issue
RustAudio/nice-plug
No description provided.
Delete branch "paravozz/nih-plug:fix/vst3-on-key-down"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?