diff --git a/src/win/hook.rs b/src/win/hook.rs new file mode 100644 index 00000000..bdb49dfd --- /dev/null +++ b/src/win/hook.rs @@ -0,0 +1,141 @@ +use std::{ + collections::HashSet, + ffi::c_int, + ptr, + sync::{LazyLock, RwLock}, +}; + +use winapi::{ + shared::{ + minwindef::{LPARAM, WPARAM}, + windef::{HHOOK, HWND, POINT}, + }, + um::{ + libloaderapi::GetModuleHandleW, + processthreadsapi::GetCurrentThreadId, + winuser::{ + CallNextHookEx, SetWindowsHookExW, UnhookWindowsHookEx, HC_ACTION, MSG, PM_REMOVE, + WH_GETMESSAGE, WM_CHAR, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, + }, + }, +}; + +use crate::win::wnd_proc; + +// track all windows opened by this instance of baseview +// we use an RwLock here since the vast majority of uses (event interceptions) +// will only need to read from the HashSet +static HOOK_STATE: LazyLock> = LazyLock::new(|| RwLock::default()); + +pub(crate) struct KeyboardHookHandle(HWNDWrapper); + +#[derive(Default)] +struct KeyboardHookState { + hook: Option, + open_windows: HashSet, +} + +#[derive(Hash, PartialEq, Eq, Clone, Copy)] +struct HWNDWrapper(HWND); + +// SAFETY: it's a pointer behind an RwLock. we'll live +unsafe impl Send for KeyboardHookState {} +unsafe impl Sync for KeyboardHookState {} + +// SAFETY: we never access the underlying HWND ourselves, just use it as a HashSet entry +unsafe impl Send for HWNDWrapper {} +unsafe impl Sync for HWNDWrapper {} + +impl Drop for KeyboardHookHandle { + fn drop(&mut self) { + deinit_keyboard_hook(self.0); + } +} + +// initialize keyboard hook +// some DAWs (particularly Ableton) intercept incoming keyboard messages, +// but we're naughty so we intercept them right back +pub(crate) fn init_keyboard_hook(hwnd: HWND) -> KeyboardHookHandle { + let state = &mut *HOOK_STATE.write().unwrap(); + + // register hwnd to global window set + state.open_windows.insert(HWNDWrapper(hwnd)); + + if state.hook.is_some() { + // keyboard hook already exists, just return handle + KeyboardHookHandle(HWNDWrapper(hwnd)) + } else { + // keyboard hook doesn't exist (no windows open before this), create it + let new_hook = unsafe { + SetWindowsHookExW( + WH_GETMESSAGE, + Some(keyboard_hook_callback), + GetModuleHandleW(ptr::null()), + GetCurrentThreadId(), + ) + }; + + state.hook = Some(new_hook); + + KeyboardHookHandle(HWNDWrapper(hwnd)) + } +} + +fn deinit_keyboard_hook(hwnd: HWNDWrapper) { + let state = &mut *HOOK_STATE.write().unwrap(); + + state.open_windows.remove(&hwnd); + + if state.open_windows.is_empty() { + if let Some(hhook) = state.hook { + unsafe { + UnhookWindowsHookEx(hhook); + } + + state.hook = None; + } + } +} + +unsafe extern "system" fn keyboard_hook_callback( + n_code: c_int, wparam: WPARAM, lparam: LPARAM, +) -> isize { + let msg = lparam as *mut MSG; + + if n_code == HC_ACTION && wparam == PM_REMOVE as usize && offer_message_to_baseview(msg) { + *msg = MSG { + hwnd: ptr::null_mut(), + message: WM_USER, + wParam: 0, + lParam: 0, + time: 0, + pt: POINT { x: 0, y: 0 }, + }; + + 0 + } else { + CallNextHookEx(ptr::null_mut(), n_code, wparam, lparam) + } +} + +// check if `msg` is a keyboard message addressed to a window +// in KeyboardHookState::open_windows, and intercept it if so +unsafe fn offer_message_to_baseview(msg: *mut MSG) -> bool { + let msg = &*msg; + + // if this isn't a keyboard message, ignore it + match msg.message { + WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP | WM_CHAR | WM_SYSCHAR => {} + + _ => return false, + } + + // check if this is one of our windows. if so, intercept it + if HOOK_STATE.read().unwrap().open_windows.contains(&HWNDWrapper(msg.hwnd)) { + let _ = wnd_proc(msg.hwnd, msg.message, msg.wParam, msg.lParam); + + return true; + } + + false +} diff --git a/src/win/mod.rs b/src/win/mod.rs index 00effa43..b914b08c 100644 --- a/src/win/mod.rs +++ b/src/win/mod.rs @@ -1,5 +1,6 @@ mod cursor; mod drop_target; +mod hook; mod keyboard; mod window; diff --git a/src/win/window.rs b/src/win/window.rs index ac7824b9..e2caa490 100644 --- a/src/win/window.rs +++ b/src/win/window.rs @@ -33,6 +33,7 @@ use raw_window_handle::{ const BV_WINDOW_MUST_CLOSE: UINT = WM_USER + 1; +use crate::win::hook::{self, KeyboardHookHandle}; use crate::{ Event, MouseButton, MouseCursor, MouseEvent, PhyPoint, PhySize, ScrollDelta, Size, WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy, @@ -118,7 +119,7 @@ impl Drop for ParentHandle { } } -unsafe extern "system" fn wnd_proc( +pub(crate) unsafe extern "system" fn wnd_proc( hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { if msg == WM_CREATE { @@ -507,6 +508,11 @@ pub(super) struct WindowState { scale_policy: WindowScalePolicy, dw_style: u32, + // handle to the win32 keyboard hook + // we don't need to read from this, just carry it around so the Drop impl can run + #[allow(dead_code)] + kb_hook: KeyboardHookHandle, + /// Tasks that should be executed at the end of `wnd_proc`. This is needed to avoid mutably /// borrowing the fields from `WindowState` more than once. For instance, when the window /// handler requests a resize in response to a keyboard event, the window state will already be @@ -686,6 +692,8 @@ impl Window<'_> { ); // todo: manage error ^ + let kb_hook = hook::init_keyboard_hook(hwnd); + #[cfg(feature = "opengl")] let gl_context: Option = options.gl_config.map(|gl_config| { let mut handle = Win32WindowHandle::empty(); @@ -716,6 +724,8 @@ impl Window<'_> { deferred_tasks: RefCell::new(VecDeque::with_capacity(4)), + kb_hook, + #[cfg(feature = "opengl")] gl_context, });