Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions src/win/hook.rs
Original file line number Diff line number Diff line change
@@ -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<RwLock<KeyboardHookState>> = LazyLock::new(|| RwLock::default());

pub(crate) struct KeyboardHookHandle(HWNDWrapper);

#[derive(Default)]
struct KeyboardHookState {
hook: Option<HHOOK>,
open_windows: HashSet<HWNDWrapper>,
}

#[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
}
1 change: 1 addition & 0 deletions src/win/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod cursor;
mod drop_target;
mod hook;
mod keyboard;
mod window;

Expand Down
12 changes: 11 additions & 1 deletion src/win/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -686,6 +692,8 @@ impl Window<'_> {
);
// todo: manage error ^

let kb_hook = hook::init_keyboard_hook(hwnd);

#[cfg(feature = "opengl")]
let gl_context: Option<GlContext> = options.gl_config.map(|gl_config| {
let mut handle = Win32WindowHandle::empty();
Expand Down Expand Up @@ -716,6 +724,8 @@ impl Window<'_> {

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

kb_hook,

#[cfg(feature = "opengl")]
gl_context,
});
Expand Down
Loading