Skip to content

Commit 101c864

Browse files
Implement keyboard event hook on Windows (#212)
This PR implements **keyboard event hooking** on Windows, a technique also used by JUCE to get around restrictions on keyboard input imposed by various hosts. By intercepting Win32 messages using `SetWindowsHookEx`, we can bypass the host and forward keyboard messages to the `baseview` window directly. This resolves the longstanding issue with keyboard input in Ableton, and likely renders many existing hacks for this problem obsolete (though I haven't tested that part yet.) The meat of the implementation is in the new `src/win/hook.rs` module, which registers a simple event hook that intercepts keyboard-related `WM_` events addressed to `baseview` windows, and forwards them into the relevant window's event handler directly, consuming the message in the process. --------- Co-authored-by: ash taylor?! <estrobiologist@outlook.com> Co-authored-by: Micah Johnston <micahrjohnston@gmail.com>
1 parent d39a848 commit 101c864

File tree

3 files changed

+154
-1
lines changed

3 files changed

+154
-1
lines changed

src/win/hook.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
use std::{
2+
collections::HashSet,
3+
ffi::c_int,
4+
ptr,
5+
sync::{LazyLock, RwLock},
6+
};
7+
8+
use winapi::{
9+
shared::{
10+
minwindef::{LPARAM, WPARAM},
11+
windef::{HHOOK, HWND, POINT},
12+
},
13+
um::{
14+
libloaderapi::GetModuleHandleW,
15+
processthreadsapi::GetCurrentThreadId,
16+
winuser::{
17+
CallNextHookEx, SetWindowsHookExW, UnhookWindowsHookEx, HC_ACTION, MSG, PM_REMOVE,
18+
WH_GETMESSAGE, WM_CHAR, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP,
19+
WM_USER,
20+
},
21+
},
22+
};
23+
24+
use crate::win::wnd_proc;
25+
26+
// track all windows opened by this instance of baseview
27+
// we use an RwLock here since the vast majority of uses (event interceptions)
28+
// will only need to read from the HashSet
29+
static HOOK_STATE: LazyLock<RwLock<KeyboardHookState>> = LazyLock::new(|| RwLock::default());
30+
31+
pub(crate) struct KeyboardHookHandle(HWNDWrapper);
32+
33+
#[derive(Default)]
34+
struct KeyboardHookState {
35+
hook: Option<HHOOK>,
36+
open_windows: HashSet<HWNDWrapper>,
37+
}
38+
39+
#[derive(Hash, PartialEq, Eq, Clone, Copy)]
40+
struct HWNDWrapper(HWND);
41+
42+
// SAFETY: it's a pointer behind an RwLock. we'll live
43+
unsafe impl Send for KeyboardHookState {}
44+
unsafe impl Sync for KeyboardHookState {}
45+
46+
// SAFETY: we never access the underlying HWND ourselves, just use it as a HashSet entry
47+
unsafe impl Send for HWNDWrapper {}
48+
unsafe impl Sync for HWNDWrapper {}
49+
50+
impl Drop for KeyboardHookHandle {
51+
fn drop(&mut self) {
52+
deinit_keyboard_hook(self.0);
53+
}
54+
}
55+
56+
// initialize keyboard hook
57+
// some DAWs (particularly Ableton) intercept incoming keyboard messages,
58+
// but we're naughty so we intercept them right back
59+
pub(crate) fn init_keyboard_hook(hwnd: HWND) -> KeyboardHookHandle {
60+
let state = &mut *HOOK_STATE.write().unwrap();
61+
62+
// register hwnd to global window set
63+
state.open_windows.insert(HWNDWrapper(hwnd));
64+
65+
if state.hook.is_some() {
66+
// keyboard hook already exists, just return handle
67+
KeyboardHookHandle(HWNDWrapper(hwnd))
68+
} else {
69+
// keyboard hook doesn't exist (no windows open before this), create it
70+
let new_hook = unsafe {
71+
SetWindowsHookExW(
72+
WH_GETMESSAGE,
73+
Some(keyboard_hook_callback),
74+
GetModuleHandleW(ptr::null()),
75+
GetCurrentThreadId(),
76+
)
77+
};
78+
79+
state.hook = Some(new_hook);
80+
81+
KeyboardHookHandle(HWNDWrapper(hwnd))
82+
}
83+
}
84+
85+
fn deinit_keyboard_hook(hwnd: HWNDWrapper) {
86+
let state = &mut *HOOK_STATE.write().unwrap();
87+
88+
state.open_windows.remove(&hwnd);
89+
90+
if state.open_windows.is_empty() {
91+
if let Some(hhook) = state.hook {
92+
unsafe {
93+
UnhookWindowsHookEx(hhook);
94+
}
95+
96+
state.hook = None;
97+
}
98+
}
99+
}
100+
101+
unsafe extern "system" fn keyboard_hook_callback(
102+
n_code: c_int, wparam: WPARAM, lparam: LPARAM,
103+
) -> isize {
104+
let msg = lparam as *mut MSG;
105+
106+
if n_code == HC_ACTION && wparam == PM_REMOVE as usize && offer_message_to_baseview(msg) {
107+
*msg = MSG {
108+
hwnd: ptr::null_mut(),
109+
message: WM_USER,
110+
wParam: 0,
111+
lParam: 0,
112+
time: 0,
113+
pt: POINT { x: 0, y: 0 },
114+
};
115+
116+
0
117+
} else {
118+
CallNextHookEx(ptr::null_mut(), n_code, wparam, lparam)
119+
}
120+
}
121+
122+
// check if `msg` is a keyboard message addressed to a window
123+
// in KeyboardHookState::open_windows, and intercept it if so
124+
unsafe fn offer_message_to_baseview(msg: *mut MSG) -> bool {
125+
let msg = &*msg;
126+
127+
// if this isn't a keyboard message, ignore it
128+
match msg.message {
129+
WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP | WM_CHAR | WM_SYSCHAR => {}
130+
131+
_ => return false,
132+
}
133+
134+
// check if this is one of our windows. if so, intercept it
135+
if HOOK_STATE.read().unwrap().open_windows.contains(&HWNDWrapper(msg.hwnd)) {
136+
let _ = wnd_proc(msg.hwnd, msg.message, msg.wParam, msg.lParam);
137+
138+
return true;
139+
}
140+
141+
false
142+
}

src/win/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod cursor;
22
mod drop_target;
3+
mod hook;
34
mod keyboard;
45
mod window;
56

src/win/window.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use raw_window_handle::{
3333

3434
const BV_WINDOW_MUST_CLOSE: UINT = WM_USER + 1;
3535

36+
use crate::win::hook::{self, KeyboardHookHandle};
3637
use crate::{
3738
Event, MouseButton, MouseCursor, MouseEvent, PhyPoint, PhySize, ScrollDelta, Size, WindowEvent,
3839
WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy,
@@ -118,7 +119,7 @@ impl Drop for ParentHandle {
118119
}
119120
}
120121

121-
unsafe extern "system" fn wnd_proc(
122+
pub(crate) unsafe extern "system" fn wnd_proc(
122123
hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM,
123124
) -> LRESULT {
124125
if msg == WM_CREATE {
@@ -507,6 +508,11 @@ pub(super) struct WindowState {
507508
scale_policy: WindowScalePolicy,
508509
dw_style: u32,
509510

511+
// handle to the win32 keyboard hook
512+
// we don't need to read from this, just carry it around so the Drop impl can run
513+
#[allow(dead_code)]
514+
kb_hook: KeyboardHookHandle,
515+
510516
/// Tasks that should be executed at the end of `wnd_proc`. This is needed to avoid mutably
511517
/// borrowing the fields from `WindowState` more than once. For instance, when the window
512518
/// handler requests a resize in response to a keyboard event, the window state will already be
@@ -686,6 +692,8 @@ impl Window<'_> {
686692
);
687693
// todo: manage error ^
688694

695+
let kb_hook = hook::init_keyboard_hook(hwnd);
696+
689697
#[cfg(feature = "opengl")]
690698
let gl_context: Option<GlContext> = options.gl_config.map(|gl_config| {
691699
let mut handle = Win32WindowHandle::empty();
@@ -716,6 +724,8 @@ impl Window<'_> {
716724

717725
deferred_tasks: RefCell::new(VecDeque::with_capacity(4)),
718726

727+
kb_hook,
728+
719729
#[cfg(feature = "opengl")]
720730
gl_context,
721731
});

0 commit comments

Comments
 (0)