Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
144 changes: 144 additions & 0 deletions src/win/hook.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use std::{
collections::HashSet,
ffi::c_int,
ptr,
sync::{LazyLock, Mutex, 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;

static HOOK: Mutex<Option<KeyboardHook>> = Mutex::new(None);

// 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 OPEN_WINDOWS: LazyLock<RwLock<HashSet<HWNDWrapper>>> = LazyLock::new(|| RwLock::default());

pub(crate) struct KeyboardHookHandle(HWNDWrapper);

struct KeyboardHook(HHOOK);

#[derive(Hash, PartialEq, Eq, Clone, Copy)]
struct HWNDWrapper(HWND);

// SAFETY: it's a pointer behind a mutex. we'll live
unsafe impl Send for KeyboardHook {}
unsafe impl Sync for KeyboardHook {}

// SAFETY: ditto
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 {
// register hwnd to global window set
OPEN_WINDOWS.write().unwrap().insert(HWNDWrapper(hwnd));

let hook = &mut *HOOK.lock().unwrap();

if 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 = KeyboardHook(unsafe {
SetWindowsHookExW(
WH_GETMESSAGE,
Some(keyboard_hook_callback),
GetModuleHandleW(ptr::null()),
GetCurrentThreadId(),
)
});

*hook = Some(new_hook);

KeyboardHookHandle(HWNDWrapper(hwnd))
}
}

fn deinit_keyboard_hook(hwnd: HWNDWrapper) {
let windows = &mut *OPEN_WINDOWS.write().unwrap();

windows.remove(&hwnd);

if windows.is_empty() {
if let Ok(mut hook) = HOOK.lock() {
if let Some(KeyboardHook(hhook)) = &mut *hook {
unsafe {
UnhookWindowsHookEx(*hhook);
}

*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 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
let Ok(windows) = OPEN_WINDOWS.read() else { return false };

if 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