1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
//! Window-procedure hook: turns Win32 messages into egui events.
//!
//! While the menu is open we swallow input instead of forwarding it, so the
//! game never sees the clicks and keystrokes meant for the menu — no camera
//! spin while you drag a slider. Everything else is passed straight through.
use std::sync::atomic::{AtomicIsize, Ordering};
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
use windows_sys::Win32::UI::WindowsAndMessaging::{
CallWindowProcW, GetWindowLongPtrW, SetWindowLongPtrW, GWLP_WNDPROC, WHEEL_DELTA, WM_CHAR, WM_KEYDOWN, WM_KEYUP,
WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL,
WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SYSKEYDOWN, WM_SYSKEYUP,
};
use crate::state;
/// The window procedure GLFW installed, which we chain to.
static ORIGINAL: AtomicIsize = AtomicIsize::new(0);
pub const VK_INSERT: usize = 0x2D;
/// Subclass the game's window.
///
/// # Safety
/// `hwnd` must be the game's top-level window.
pub unsafe fn install(hwnd: HWND) -> bool {
// Installing twice would store our own procedure as "the original", and
// restoring that later leaves a dangling pointer on the window.
if ORIGINAL.load(Ordering::SeqCst) != 0 {
return true;
}
let prev = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, wnd_proc as isize);
if prev == 0 {
return false;
}
ORIGINAL.store(prev, Ordering::SeqCst);
true
}
/// Put GLFW's window procedure back.
///
/// # Safety
/// `hwnd` must be the window `install` was called with.
pub unsafe fn remove(hwnd: HWND) -> bool {
let prev = ORIGINAL.swap(0, Ordering::SeqCst);
if prev == 0 {
return true;
}
// If something subclassed the window after us, putting the old procedure
// back would cut that other hook out of the chain. Leave ours in place and
// say so; the caller keeps the module resident rather than unmapping code
// the window still points at.
if GetWindowLongPtrW(hwnd, GWLP_WNDPROC) != wnd_proc as isize {
ORIGINAL.store(prev, Ordering::SeqCst);
return false;
}
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, prev);
true
}
fn chain(hwnd: HWND, msg: u32, w: WPARAM, l: LPARAM) -> LRESULT {
let prev = ORIGINAL.load(Ordering::SeqCst);
if prev == 0 {
return 0;
}
// SAFETY: `prev` is the procedure GLFW registered for this window.
unsafe {
CallWindowProcW(
Some(std::mem::transmute::<
isize,
unsafe extern "system" fn(HWND, u32, WPARAM, LPARAM) -> LRESULT,
>(prev)),
hwnd,
msg,
w,
l,
)
}
}
unsafe extern "system" fn wnd_proc(hwnd: HWND, msg: u32, w: WPARAM, l: LPARAM) -> LRESULT {
// Insert toggles the menu, and is never forwarded to the game.
if msg == WM_KEYDOWN && w == VK_INSERT {
state::with(|s| s.toggle_menu());
return 0;
}
// Keybinds: always while waiting for one to be set, and otherwise only
// when the menu is shut, so typing in the menu cannot fire modules.
if msg == WM_KEYDOWN {
let handled = state::with(|s| {
if s.binding.is_some() || !s.menu_open {
s.handle_key(w as u32)
} else {
false
}
})
.unwrap_or(false);
if handled {
return 0;
}
}
let open = state::with(|s| s.menu_open).unwrap_or(false);
if !open {
return chain(hwnd, msg, w, l);
}
let consumed = feed(msg, w, l);
if consumed {
return 0;
}
chain(hwnd, msg, w, l)
}
/// Translate one message into egui events. Returns true if the game should not
/// see it.
fn feed(msg: u32, w: WPARAM, l: LPARAM) -> bool {
use egui::{Event, PointerButton, Pos2, Vec2};
let mods = current_modifiers();
match msg {
WM_MOUSEMOVE => {
let x = (l & 0xFFFF) as i16 as f32;
let y = ((l >> 16) & 0xFFFF) as i16 as f32;
state::with(|s| {
s.pointer = Pos2::new(x / s.scale, y / s.scale);
let p = s.pointer;
s.events.push(Event::PointerMoved(p));
});
true
}
WM_LBUTTONDOWN | WM_LBUTTONUP | WM_RBUTTONDOWN | WM_RBUTTONUP | WM_MBUTTONDOWN
| WM_MBUTTONUP => {
let (button, pressed) = match msg {
WM_LBUTTONDOWN => (PointerButton::Primary, true),
WM_LBUTTONUP => (PointerButton::Primary, false),
WM_RBUTTONDOWN => (PointerButton::Secondary, true),
WM_RBUTTONUP => (PointerButton::Secondary, false),
WM_MBUTTONDOWN => (PointerButton::Middle, true),
_ => (PointerButton::Middle, false),
};
state::with(|s| {
let pos = s.pointer;
s.events.push(Event::PointerButton { pos, button, pressed, modifiers: mods });
});
true
}
WM_MOUSEWHEEL => {
let delta = ((w >> 16) & 0xFFFF) as i16 as f32 / WHEEL_DELTA as f32;
state::with(|s| {
s.events.push(Event::MouseWheel {
unit: egui::MouseWheelUnit::Line,
delta: Vec2::new(0.0, delta),
modifiers: mods,
});
});
true
}
WM_CHAR => {
if let Some(c) = char::from_u32(w as u32) {
// Control characters are handled as key events, not text.
if !c.is_control() {
state::with(|s| s.events.push(Event::Text(c.to_string())));
}
}
true
}
WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP => {
let pressed = msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN;
if let Some(key) = vk_to_key(w) {
state::with(|s| {
s.events.push(Event::Key {
key,
physical_key: None,
pressed,
repeat: false,
modifiers: mods,
});
});
}
true
}
_ => false,
}
}
fn current_modifiers() -> egui::Modifiers {
use windows_sys::Win32::UI::Input::KeyboardAndMouse::{
GetAsyncKeyState, VK_CONTROL, VK_MENU, VK_SHIFT,
};
// SAFETY: GetAsyncKeyState has no preconditions.
let down = |vk: i32| unsafe { (GetAsyncKeyState(vk) as u16 & 0x8000) != 0 };
let ctrl = down(VK_CONTROL as i32);
egui::Modifiers {
alt: down(VK_MENU as i32),
ctrl,
shift: down(VK_SHIFT as i32),
mac_cmd: false,
command: ctrl,
}
}
fn vk_to_key(vk: WPARAM) -> Option<egui::Key> {
use egui::Key;
Some(match vk as u32 {
0x08 => Key::Backspace,
0x09 => Key::Tab,
0x0D => Key::Enter,
0x1B => Key::Escape,
0x20 => Key::Space,
0x21 => Key::PageUp,
0x22 => Key::PageDown,
0x23 => Key::End,
0x24 => Key::Home,
0x25 => Key::ArrowLeft,
0x26 => Key::ArrowUp,
0x27 => Key::ArrowRight,
0x28 => Key::ArrowDown,
0x2E => Key::Delete,
0x30..=0x39 => match vk as u32 - 0x30 {
0 => Key::Num0,
1 => Key::Num1,
2 => Key::Num2,
3 => Key::Num3,
4 => Key::Num4,
5 => Key::Num5,
6 => Key::Num6,
7 => Key::Num7,
8 => Key::Num8,
_ => Key::Num9,
},
0x41..=0x5A => Key::from_name(&((b'A' + (vk as u8 - 0x41)) as char).to_string())?,
0x70..=0x7B => Key::from_name(&format!("F{}", vk as u32 - 0x70 + 1))?,
_ => return None,
})
}