mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
windows: Better keyboard input support (#9180)
### Description Currently, there are some issues with input handling on Windows: #### 1. Direct crash when encountering IME input. https://github.com/zed-industries/zed/assets/14981363/598f7272-1948-4a42-99c5-2ef7b9162a1e #### 2. Handling messages every 1/60 seconds in the main thread. Despite being named "immediate_handle," it's not exactly immediate. ```rust // actually halt here let wait_result = unsafe { DCompositionWaitForCompositorClock(Some(&[self.inner.event]), INFINITE) }; // compositor clock ticked so we should draw a frame if wait_result == 1 { unsafe { invalidate_thread_windows(GetCurrentThreadId()) }; while unsafe { PeekMessageW(&mut msg, HWND::default(), 0, 0, PM_REMOVE) }.as_bool() ``` #### 3. According to Windows recommendations, character input should be obtained using `WM_CHAR` instead of `WM_KEYDOWN`. Additionally, there are problems with the handling within `WM_CHAR`. ```rust fn handle_char_msg(&self, wparam: WPARAM) -> LRESULT { let mut callbacks = self.callbacks.borrow_mut(); if let Some(callback) = callbacks.input.as_mut() { let modifiers = self.current_modifiers(); let msg_char = wparam.0 as u8 as char; // these are u16 chars, cant treat them as u8 ``` And, we don't handle `WM_SYSKEYDOWN` properly, which leads to `Alt + F4` not working. Release Notes: - N/A
This commit is contained in:
parent
2abb5aeaf2
commit
36cbfbfb94
28
Cargo.toml
28
Cargo.toml
@ -323,7 +323,13 @@ url = "2.2"
|
|||||||
uuid = { version = "1.1.2", features = ["v4"] }
|
uuid = { version = "1.1.2", features = ["v4"] }
|
||||||
wasmparser = "0.121"
|
wasmparser = "0.121"
|
||||||
wasm-encoder = "0.41"
|
wasm-encoder = "0.41"
|
||||||
wasmtime = { version = "18.0", default-features = false, features = ["async", "demangle", "runtime", "cranelift", "component-model"] }
|
wasmtime = { version = "18.0", default-features = false, features = [
|
||||||
|
"async",
|
||||||
|
"demangle",
|
||||||
|
"runtime",
|
||||||
|
"cranelift",
|
||||||
|
"component-model",
|
||||||
|
] }
|
||||||
wasmtime-wasi = "18.0"
|
wasmtime-wasi = "18.0"
|
||||||
which = "6.0.0"
|
which = "6.0.0"
|
||||||
wit-component = "0.20"
|
wit-component = "0.20"
|
||||||
@ -334,23 +340,21 @@ version = "0.53.0"
|
|||||||
features = [
|
features = [
|
||||||
"implement",
|
"implement",
|
||||||
"Wdk_System_SystemServices",
|
"Wdk_System_SystemServices",
|
||||||
"Win32_Graphics_Gdi",
|
|
||||||
"Win32_Graphics_DirectComposition",
|
"Win32_Graphics_DirectComposition",
|
||||||
"Win32_UI_Controls",
|
"Win32_Graphics_Gdi",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
|
||||||
"Win32_UI_Input_KeyboardAndMouse",
|
|
||||||
"Win32_UI_Shell",
|
|
||||||
"Win32_System_Com",
|
|
||||||
"Win32_System_SystemInformation",
|
|
||||||
"Win32_System_SystemServices",
|
|
||||||
"Win32_System_Time",
|
|
||||||
"Win32_Security",
|
"Win32_Security",
|
||||||
"Win32_System_Com",
|
"Win32_System_Com",
|
||||||
"Win32_System_Com_StructuredStorage",
|
"Win32_System_Com_StructuredStorage",
|
||||||
"Win32_System_Threading",
|
|
||||||
"Win32_System_DataExchange",
|
"Win32_System_DataExchange",
|
||||||
"Win32_System_Ole",
|
"Win32_System_Ole",
|
||||||
"Win32_System_Com",
|
"Win32_System_SystemInformation",
|
||||||
|
"Win32_System_SystemServices",
|
||||||
|
"Win32_System_Time",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
"Win32_UI_Controls",
|
||||||
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
|
"Win32_UI_Shell",
|
||||||
|
"Win32_UI_WindowsAndMessaging",
|
||||||
]
|
]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
collections::HashSet,
|
|
||||||
ffi::{c_uint, c_void, OsString},
|
ffi::{c_uint, c_void, OsString},
|
||||||
os::windows::ffi::{OsStrExt, OsStringExt},
|
os::windows::ffi::{OsStrExt, OsStringExt},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@ -17,19 +16,23 @@ use async_task::Runnable;
|
|||||||
use copypasta::{ClipboardContext, ClipboardProvider};
|
use copypasta::{ClipboardContext, ClipboardProvider};
|
||||||
use futures::channel::oneshot::{self, Receiver};
|
use futures::channel::oneshot::{self, Receiver};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::{Mutex, RwLock};
|
||||||
|
use smallvec::SmallVec;
|
||||||
use time::UtcOffset;
|
use time::UtcOffset;
|
||||||
use util::{ResultExt, SemanticVersion};
|
use util::{ResultExt, SemanticVersion};
|
||||||
use windows::{
|
use windows::{
|
||||||
core::{IUnknown, HRESULT, HSTRING, PCWSTR, PWSTR},
|
core::{IUnknown, HRESULT, HSTRING, PCWSTR, PWSTR},
|
||||||
Wdk::System::SystemServices::RtlGetVersion,
|
Wdk::System::SystemServices::RtlGetVersion,
|
||||||
Win32::{
|
Win32::{
|
||||||
Foundation::{CloseHandle, BOOL, HANDLE, HWND, LPARAM, TRUE},
|
Foundation::{CloseHandle, HANDLE, HWND},
|
||||||
Graphics::DirectComposition::DCompositionWaitForCompositorClock,
|
Graphics::{
|
||||||
|
DirectComposition::DCompositionWaitForCompositorClock,
|
||||||
|
Gdi::{RedrawWindow, HRGN, RDW_INVALIDATE, RDW_UPDATENOW},
|
||||||
|
},
|
||||||
System::{
|
System::{
|
||||||
Com::{CoCreateInstance, CreateBindCtx, CLSCTX_ALL},
|
Com::{CoCreateInstance, CreateBindCtx, CLSCTX_ALL},
|
||||||
Ole::{OleInitialize, OleUninitialize},
|
Ole::{OleInitialize, OleUninitialize},
|
||||||
Threading::{CreateEventW, GetCurrentThreadId, INFINITE},
|
Threading::{CreateEventW, INFINITE},
|
||||||
Time::{GetTimeZoneInformation, TIME_ZONE_ID_INVALID},
|
Time::{GetTimeZoneInformation, TIME_ZONE_ID_INVALID},
|
||||||
},
|
},
|
||||||
UI::{
|
UI::{
|
||||||
@ -40,21 +43,21 @@ use windows::{
|
|||||||
FOS_ALLOWMULTISELECT, FOS_FILEMUSTEXIST, FOS_PICKFOLDERS, SIGDN_FILESYSPATH,
|
FOS_ALLOWMULTISELECT, FOS_FILEMUSTEXIST, FOS_PICKFOLDERS, SIGDN_FILESYSPATH,
|
||||||
},
|
},
|
||||||
WindowsAndMessaging::{
|
WindowsAndMessaging::{
|
||||||
DispatchMessageW, EnumThreadWindows, LoadImageW, PeekMessageW, PostQuitMessage,
|
DispatchMessageW, LoadImageW, PeekMessageW, PostQuitMessage, SetCursor,
|
||||||
SetCursor, SystemParametersInfoW, TranslateMessage, HCURSOR, IDC_ARROW, IDC_CROSS,
|
SystemParametersInfoW, TranslateMessage, HCURSOR, IDC_ARROW, IDC_CROSS, IDC_HAND,
|
||||||
IDC_HAND, IDC_IBEAM, IDC_NO, IDC_SIZENS, IDC_SIZEWE, IMAGE_CURSOR, LR_DEFAULTSIZE,
|
IDC_IBEAM, IDC_NO, IDC_SIZENS, IDC_SIZEWE, IMAGE_CURSOR, LR_DEFAULTSIZE, LR_SHARED,
|
||||||
LR_SHARED, MSG, PM_REMOVE, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES,
|
MSG, PM_REMOVE, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SW_SHOWDEFAULT,
|
||||||
SW_SHOWDEFAULT, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, WM_QUIT, WM_SETTINGCHANGE,
|
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, WM_QUIT, WM_SETTINGCHANGE,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
try_get_window_inner, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle,
|
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor,
|
||||||
ForegroundExecutor, Keymap, Menu, PathPromptOptions, Platform, PlatformDisplay, PlatformInput,
|
Keymap, Menu, PathPromptOptions, Platform, PlatformDisplay, PlatformInput, PlatformTextSystem,
|
||||||
PlatformTextSystem, PlatformWindow, Task, WindowAppearance, WindowOptions, WindowParams,
|
PlatformWindow, Task, WindowAppearance, WindowParams, WindowsDispatcher, WindowsDisplay,
|
||||||
WindowsDispatcher, WindowsDisplay, WindowsTextSystem, WindowsWindow,
|
WindowsTextSystem, WindowsWindow,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) struct WindowsPlatform {
|
pub(crate) struct WindowsPlatform {
|
||||||
@ -72,15 +75,13 @@ pub(crate) struct WindowsPlatformSystemSettings {
|
|||||||
pub(crate) wheel_scroll_lines: u32,
|
pub(crate) wheel_scroll_lines: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
type WindowHandleValues = HashSet<isize>;
|
|
||||||
|
|
||||||
pub(crate) struct WindowsPlatformInner {
|
pub(crate) struct WindowsPlatformInner {
|
||||||
background_executor: BackgroundExecutor,
|
background_executor: BackgroundExecutor,
|
||||||
pub(crate) foreground_executor: ForegroundExecutor,
|
pub(crate) foreground_executor: ForegroundExecutor,
|
||||||
main_receiver: flume::Receiver<Runnable>,
|
main_receiver: flume::Receiver<Runnable>,
|
||||||
text_system: Arc<WindowsTextSystem>,
|
text_system: Arc<WindowsTextSystem>,
|
||||||
callbacks: Mutex<Callbacks>,
|
callbacks: Mutex<Callbacks>,
|
||||||
pub(crate) window_handle_values: RefCell<WindowHandleValues>,
|
pub raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
|
||||||
pub(crate) event: HANDLE,
|
pub(crate) event: HANDLE,
|
||||||
pub(crate) settings: RefCell<WindowsPlatformSystemSettings>,
|
pub(crate) settings: RefCell<WindowsPlatformSystemSettings>,
|
||||||
}
|
}
|
||||||
@ -167,7 +168,7 @@ impl WindowsPlatform {
|
|||||||
let foreground_executor = ForegroundExecutor::new(dispatcher);
|
let foreground_executor = ForegroundExecutor::new(dispatcher);
|
||||||
let text_system = Arc::new(WindowsTextSystem::new());
|
let text_system = Arc::new(WindowsTextSystem::new());
|
||||||
let callbacks = Mutex::new(Callbacks::default());
|
let callbacks = Mutex::new(Callbacks::default());
|
||||||
let window_handle_values = RefCell::new(HashSet::new());
|
let raw_window_handles = RwLock::new(SmallVec::new());
|
||||||
let settings = RefCell::new(WindowsPlatformSystemSettings::new());
|
let settings = RefCell::new(WindowsPlatformSystemSettings::new());
|
||||||
let inner = Rc::new(WindowsPlatformInner {
|
let inner = Rc::new(WindowsPlatformInner {
|
||||||
background_executor,
|
background_executor,
|
||||||
@ -175,64 +176,31 @@ impl WindowsPlatform {
|
|||||||
main_receiver,
|
main_receiver,
|
||||||
text_system,
|
text_system,
|
||||||
callbacks,
|
callbacks,
|
||||||
window_handle_values,
|
raw_window_handles,
|
||||||
event,
|
event,
|
||||||
settings,
|
settings,
|
||||||
});
|
});
|
||||||
Self { inner }
|
Self { inner }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// runs message handlers that should be processed before dispatching to prevent translating unnecessary messages
|
|
||||||
/// returns true if message is handled and should not dispatch
|
|
||||||
fn run_immediate_msg_handlers(&self, msg: &MSG) -> bool {
|
|
||||||
if msg.message == WM_SETTINGCHANGE {
|
|
||||||
self.inner.settings.borrow_mut().update_all();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self
|
|
||||||
.inner
|
|
||||||
.window_handle_values
|
|
||||||
.borrow()
|
|
||||||
.contains(&msg.hwnd.0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(inner) = try_get_window_inner(msg.hwnd) {
|
|
||||||
inner.handle_immediate_msg(msg.message, msg.wParam, msg.lParam)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_foreground_tasks(&self) {
|
fn run_foreground_tasks(&self) {
|
||||||
for runnable in self.inner.main_receiver.drain() {
|
for runnable in self.inner.main_receiver.drain() {
|
||||||
runnable.run();
|
runnable.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
unsafe extern "system" fn invalidate_window_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
fn redraw_all(&self) {
|
||||||
let window_handle_values = unsafe { &*(lparam.0 as *const WindowHandleValues) };
|
for handle in self.inner.raw_window_handles.read().iter() {
|
||||||
if !window_handle_values.contains(&hwnd.0) {
|
unsafe {
|
||||||
return TRUE;
|
RedrawWindow(
|
||||||
|
*handle,
|
||||||
|
None,
|
||||||
|
HRGN::default(),
|
||||||
|
RDW_INVALIDATE | RDW_UPDATENOW,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(inner) = try_get_window_inner(hwnd) {
|
|
||||||
inner.invalidate_client_area();
|
|
||||||
}
|
|
||||||
TRUE
|
|
||||||
}
|
|
||||||
|
|
||||||
/// invalidates all windows belonging to a thread causing a paint message to be scheduled
|
|
||||||
fn invalidate_thread_windows(win32_thread_id: u32, window_handle_values: &WindowHandleValues) {
|
|
||||||
unsafe {
|
|
||||||
EnumThreadWindows(
|
|
||||||
win32_thread_id,
|
|
||||||
Some(invalidate_window_callback),
|
|
||||||
LPARAM(window_handle_values as *const _ as isize),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Platform for WindowsPlatform {
|
impl Platform for WindowsPlatform {
|
||||||
@ -250,35 +218,34 @@ impl Platform for WindowsPlatform {
|
|||||||
|
|
||||||
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
|
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
|
||||||
on_finish_launching();
|
on_finish_launching();
|
||||||
|
let dispatch_event = self.inner.event;
|
||||||
|
|
||||||
'a: loop {
|
'a: loop {
|
||||||
let mut msg = MSG::default();
|
let mut msg = MSG::default();
|
||||||
// will be 0 if woken up by self.inner.event or 1 if the compositor clock ticked
|
// will be 0 if woken up by self.inner.event or 1 if the compositor clock ticked
|
||||||
// SEE: https://learn.microsoft.com/en-us/windows/win32/directcomp/compositor-clock/compositor-clock
|
// SEE: https://learn.microsoft.com/en-us/windows/win32/directcomp/compositor-clock/compositor-clock
|
||||||
let wait_result =
|
let wait_result =
|
||||||
unsafe { DCompositionWaitForCompositorClock(Some(&[self.inner.event]), INFINITE) };
|
unsafe { DCompositionWaitForCompositorClock(Some(&[dispatch_event]), INFINITE) };
|
||||||
|
|
||||||
// compositor clock ticked so we should draw a frame
|
// compositor clock ticked so we should draw a frame
|
||||||
if wait_result == 1 {
|
if wait_result == 1 {
|
||||||
|
self.redraw_all();
|
||||||
unsafe {
|
unsafe {
|
||||||
invalidate_thread_windows(
|
let mut msg = MSG::default();
|
||||||
GetCurrentThreadId(),
|
|
||||||
&self.inner.window_handle_values.borrow(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
while unsafe { PeekMessageW(&mut msg, HWND::default(), 0, 0, PM_REMOVE) }.as_bool()
|
while PeekMessageW(&mut msg, HWND::default(), 0, 0, PM_REMOVE).as_bool() {
|
||||||
{
|
if msg.message == WM_QUIT {
|
||||||
if msg.message == WM_QUIT {
|
break 'a;
|
||||||
break 'a;
|
}
|
||||||
}
|
if msg.message == WM_SETTINGCHANGE {
|
||||||
|
self.inner.settings.borrow_mut().update_all();
|
||||||
if !self.run_immediate_msg_handlers(&msg) {
|
continue;
|
||||||
unsafe { TranslateMessage(&msg) };
|
}
|
||||||
unsafe { DispatchMessageW(&msg) };
|
TranslateMessage(&msg);
|
||||||
|
DispatchMessageW(&msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.run_foreground_tasks();
|
self.run_foreground_tasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,9 +40,9 @@ use windows::{
|
|||||||
TD_INFORMATION_ICON, TD_WARNING_ICON,
|
TD_INFORMATION_ICON, TD_WARNING_ICON,
|
||||||
},
|
},
|
||||||
Input::KeyboardAndMouse::{
|
Input::KeyboardAndMouse::{
|
||||||
GetKeyState, VIRTUAL_KEY, VK_BACK, VK_CONTROL, VK_DOWN, VK_END, VK_ESCAPE, VK_F1,
|
GetKeyState, VIRTUAL_KEY, VK_0, VK_A, VK_BACK, VK_CONTROL, VK_DOWN, VK_END,
|
||||||
VK_F24, VK_HOME, VK_INSERT, VK_LEFT, VK_LWIN, VK_MENU, VK_NEXT, VK_PRIOR,
|
VK_ESCAPE, VK_F1, VK_F24, VK_HOME, VK_INSERT, VK_LEFT, VK_LWIN, VK_MENU, VK_NEXT,
|
||||||
VK_RETURN, VK_RIGHT, VK_RWIN, VK_SHIFT, VK_SPACE, VK_TAB, VK_UP,
|
VK_PRIOR, VK_RETURN, VK_RIGHT, VK_RWIN, VK_SHIFT, VK_TAB, VK_UP,
|
||||||
},
|
},
|
||||||
Shell::{DragQueryFileW, HDROP},
|
Shell::{DragQueryFileW, HDROP},
|
||||||
WindowsAndMessaging::{
|
WindowsAndMessaging::{
|
||||||
@ -52,9 +52,9 @@ use windows::{
|
|||||||
WINDOW_EX_STYLE, WINDOW_LONG_PTR_INDEX, WM_CHAR, WM_CLOSE, WM_DESTROY, WM_KEYDOWN,
|
WINDOW_EX_STYLE, WINDOW_LONG_PTR_INDEX, WM_CHAR, WM_CLOSE, WM_DESTROY, WM_KEYDOWN,
|
||||||
WM_KEYUP, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP,
|
WM_KEYUP, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP,
|
||||||
WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_MOVE, WM_NCCREATE, WM_NCDESTROY,
|
WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_MOVE, WM_NCCREATE, WM_NCDESTROY,
|
||||||
WM_PAINT, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SIZE, WM_SYSCHAR, WM_SYSKEYDOWN,
|
WM_PAINT, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SIZE, WM_SYSKEYDOWN, WM_SYSKEYUP,
|
||||||
WM_SYSKEYUP, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW, WS_OVERLAPPEDWINDOW,
|
WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW, WS_OVERLAPPEDWINDOW, WS_VISIBLE, XBUTTON1,
|
||||||
WS_VISIBLE, XBUTTON1, XBUTTON2,
|
XBUTTON2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -68,25 +68,6 @@ use crate::{
|
|||||||
WindowAppearance, WindowParams, WindowsDisplay, WindowsPlatformInner,
|
WindowAppearance, WindowParams, WindowsDisplay, WindowsPlatformInner,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub(crate) enum CallbackResult {
|
|
||||||
/// handled by system or user callback
|
|
||||||
Handled {
|
|
||||||
/// `true` if user callback handled event
|
|
||||||
by_callback: bool,
|
|
||||||
},
|
|
||||||
Unhandled,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CallbackResult {
|
|
||||||
pub fn is_handled(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Handled { by_callback: _ } => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct WindowsWindowInner {
|
pub(crate) struct WindowsWindowInner {
|
||||||
hwnd: HWND,
|
hwnd: HWND,
|
||||||
origin: Cell<Point<GlobalPixels>>,
|
origin: Cell<Point<GlobalPixels>>,
|
||||||
@ -182,15 +163,6 @@ impl WindowsWindowInner {
|
|||||||
unsafe { InvalidateRect(self.hwnd, None, FALSE) };
|
unsafe { InvalidateRect(self.hwnd, None, FALSE) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns true if message is handled and should not dispatch
|
|
||||||
pub(crate) fn handle_immediate_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> bool {
|
|
||||||
match msg {
|
|
||||||
WM_KEYDOWN | WM_SYSKEYDOWN => self.handle_keydown_msg(wparam).is_handled(),
|
|
||||||
WM_KEYUP | WM_SYSKEYUP => self.handle_keyup_msg(wparam).is_handled(),
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
fn handle_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||||
log::debug!("msg: {msg}, wparam: {}, lparam: {}", wparam.0, lparam.0);
|
log::debug!("msg: {msg}, wparam: {}, lparam: {}", wparam.0, lparam.0);
|
||||||
match msg {
|
match msg {
|
||||||
@ -234,9 +206,11 @@ impl WindowsWindowInner {
|
|||||||
}
|
}
|
||||||
WM_MOUSEWHEEL => self.handle_mouse_wheel_msg(wparam, lparam),
|
WM_MOUSEWHEEL => self.handle_mouse_wheel_msg(wparam, lparam),
|
||||||
WM_MOUSEHWHEEL => self.handle_mouse_horizontal_wheel_msg(wparam, lparam),
|
WM_MOUSEHWHEEL => self.handle_mouse_horizontal_wheel_msg(wparam, lparam),
|
||||||
WM_CHAR | WM_SYSCHAR => self.handle_char_msg(wparam),
|
WM_SYSKEYDOWN => self.handle_syskeydown_msg(msg, wparam, lparam),
|
||||||
// These events are handled by the immediate handler
|
WM_SYSKEYUP => self.handle_syskeyup_msg(msg, wparam, lparam),
|
||||||
WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP => LRESULT(0),
|
WM_KEYDOWN => self.handle_keydown_msg(msg, wparam, lparam),
|
||||||
|
WM_KEYUP => self.handle_keyup_msg(msg, wparam),
|
||||||
|
WM_CHAR => self.handle_char_msg(msg, wparam, lparam),
|
||||||
_ => unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) },
|
_ => unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -302,9 +276,15 @@ impl WindowsWindowInner {
|
|||||||
if let Some(callback) = callbacks.close.take() {
|
if let Some(callback) = callbacks.close.take() {
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
let mut window_handles = self.platform_inner.window_handle_values.borrow_mut();
|
let index = self
|
||||||
window_handles.remove(&self.hwnd.0);
|
.platform_inner
|
||||||
if window_handles.is_empty() {
|
.raw_window_handles
|
||||||
|
.read()
|
||||||
|
.iter()
|
||||||
|
.position(|handle| *handle == self.hwnd)
|
||||||
|
.unwrap();
|
||||||
|
self.platform_inner.raw_window_handles.write().remove(index);
|
||||||
|
if self.platform_inner.raw_window_handles.read().is_empty() {
|
||||||
self.platform_inner
|
self.platform_inner
|
||||||
.foreground_executor
|
.foreground_executor
|
||||||
.spawn(async {
|
.spawn(async {
|
||||||
@ -345,155 +325,210 @@ impl WindowsWindowInner {
|
|||||||
LRESULT(1)
|
LRESULT(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_key_msg_keystroke(&self, wparam: WPARAM) -> Option<Keystroke> {
|
fn parse_syskeydown_msg_keystroke(&self, wparam: WPARAM) -> Option<Keystroke> {
|
||||||
|
let modifiers = self.current_modifiers();
|
||||||
|
if !modifiers.alt {
|
||||||
|
// on Windows, F10 can trigger this event, not just the alt key
|
||||||
|
// and we just don't care about F10
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let vk_code = wparam.loword();
|
let vk_code = wparam.loword();
|
||||||
|
let basic_key = basic_vkcode_to_string(vk_code, modifiers);
|
||||||
// 0-9 https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
if basic_key.is_some() {
|
||||||
if vk_code >= 0x30 && vk_code <= 0x39 {
|
return basic_key;
|
||||||
let modifiers = self.current_modifiers();
|
|
||||||
|
|
||||||
if modifiers.shift {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let digit_char = (b'0' + ((vk_code - 0x30) as u8)) as char;
|
|
||||||
return Some(Keystroke {
|
|
||||||
modifiers,
|
|
||||||
key: digit_char.to_string(),
|
|
||||||
ime_key: Some(digit_char.to_string()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// A-Z https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
|
||||||
if vk_code >= 0x41 && vk_code <= 0x5A {
|
|
||||||
let offset = (vk_code - 0x41) as u8;
|
|
||||||
let alpha_char = (b'a' + offset) as char;
|
|
||||||
let alpha_char_upper = (b'A' + offset) as char;
|
|
||||||
let modifiers = self.current_modifiers();
|
|
||||||
return Some(Keystroke {
|
|
||||||
modifiers,
|
|
||||||
key: alpha_char.to_string(),
|
|
||||||
ime_key: Some(if modifiers.shift {
|
|
||||||
alpha_char_upper.to_string()
|
|
||||||
} else {
|
|
||||||
alpha_char.to_string()
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if vk_code >= VK_F1.0 && vk_code <= VK_F24.0 {
|
|
||||||
let offset = vk_code - VK_F1.0;
|
|
||||||
return Some(Keystroke {
|
|
||||||
modifiers: self.current_modifiers(),
|
|
||||||
key: format!("f{}", offset + 1),
|
|
||||||
ime_key: None,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = match VIRTUAL_KEY(vk_code) {
|
let key = match VIRTUAL_KEY(vk_code) {
|
||||||
VK_SPACE => Some(("space", Some(" "))),
|
VK_BACK => Some("backspace"),
|
||||||
VK_TAB => Some(("tab", Some("\t"))),
|
VK_RETURN => Some("enter"),
|
||||||
VK_BACK => Some(("backspace", None)),
|
VK_TAB => Some("tab"),
|
||||||
VK_RETURN => Some(("enter", None)),
|
VK_UP => Some("up"),
|
||||||
VK_UP => Some(("up", None)),
|
VK_DOWN => Some("down"),
|
||||||
VK_DOWN => Some(("down", None)),
|
VK_RIGHT => Some("right"),
|
||||||
VK_RIGHT => Some(("right", None)),
|
VK_LEFT => Some("left"),
|
||||||
VK_LEFT => Some(("left", None)),
|
VK_HOME => Some("home"),
|
||||||
VK_HOME => Some(("home", None)),
|
VK_END => Some("end"),
|
||||||
VK_END => Some(("end", None)),
|
VK_PRIOR => Some("pageup"),
|
||||||
VK_PRIOR => Some(("pageup", None)),
|
VK_NEXT => Some("pagedown"),
|
||||||
VK_NEXT => Some(("pagedown", None)),
|
VK_ESCAPE => Some("escape"),
|
||||||
VK_ESCAPE => Some(("escape", None)),
|
VK_INSERT => Some("insert"),
|
||||||
VK_INSERT => Some(("insert", None)),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some((key, ime_key)) = key {
|
if let Some(key) = key {
|
||||||
Some(Keystroke {
|
Some(Keystroke {
|
||||||
modifiers: self.current_modifiers(),
|
modifiers,
|
||||||
key: key.to_string(),
|
key: key.to_string(),
|
||||||
ime_key: ime_key.map(|k| k.to_string()),
|
ime_key: None,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_keydown_msg(&self, wparam: WPARAM) -> CallbackResult {
|
fn parse_keydown_msg_keystroke(&self, wparam: WPARAM) -> Option<Keystroke> {
|
||||||
let mut callbacks = self.callbacks.borrow_mut();
|
let vk_code = wparam.loword();
|
||||||
let keystroke = self.parse_key_msg_keystroke(wparam);
|
|
||||||
if let Some(keystroke) = keystroke {
|
|
||||||
if let Some(callback) = callbacks.input.as_mut() {
|
|
||||||
let ime_key = keystroke.ime_key.clone();
|
|
||||||
let event = KeyDownEvent {
|
|
||||||
keystroke,
|
|
||||||
is_held: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if callback(PlatformInput::KeyDown(event)) {
|
let modifiers = self.current_modifiers();
|
||||||
CallbackResult::Handled { by_callback: true }
|
if modifiers.control || modifiers.alt {
|
||||||
} else if let Some(mut input_handler) = self.input_handler.take() {
|
let basic_key = basic_vkcode_to_string(vk_code, modifiers);
|
||||||
if let Some(ime_key) = ime_key {
|
if basic_key.is_some() {
|
||||||
input_handler.replace_text_in_range(None, &ime_key);
|
return basic_key;
|
||||||
}
|
|
||||||
self.input_handler.set(Some(input_handler));
|
|
||||||
CallbackResult::Handled { by_callback: true }
|
|
||||||
} else {
|
|
||||||
CallbackResult::Handled { by_callback: false }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
CallbackResult::Handled { by_callback: false }
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
CallbackResult::Unhandled
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_keyup_msg(&self, wparam: WPARAM) -> CallbackResult {
|
if vk_code >= VK_F1.0 && vk_code <= VK_F24.0 {
|
||||||
let mut callbacks = self.callbacks.borrow_mut();
|
let offset = vk_code - VK_F1.0;
|
||||||
let keystroke = self.parse_key_msg_keystroke(wparam);
|
return Some(Keystroke {
|
||||||
if let Some(keystroke) = keystroke {
|
|
||||||
if let Some(callback) = callbacks.input.as_mut() {
|
|
||||||
let event = KeyUpEvent { keystroke };
|
|
||||||
let by_callback = callback(PlatformInput::KeyUp(event));
|
|
||||||
CallbackResult::Handled { by_callback }
|
|
||||||
} else {
|
|
||||||
CallbackResult::Handled { by_callback: false }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
CallbackResult::Unhandled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_char_msg(&self, wparam: WPARAM) -> LRESULT {
|
|
||||||
let mut callbacks = self.callbacks.borrow_mut();
|
|
||||||
if let Some(callback) = callbacks.input.as_mut() {
|
|
||||||
let modifiers = self.current_modifiers();
|
|
||||||
let msg_char = wparam.0 as u8 as char;
|
|
||||||
let keystroke = Keystroke {
|
|
||||||
modifiers,
|
modifiers,
|
||||||
key: msg_char.to_string(),
|
key: format!("f{}", offset + 1),
|
||||||
ime_key: Some(msg_char.to_string()),
|
ime_key: None,
|
||||||
};
|
});
|
||||||
let ime_key = keystroke.ime_key.clone();
|
|
||||||
let event = KeyDownEvent {
|
|
||||||
keystroke,
|
|
||||||
is_held: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if callback(PlatformInput::KeyDown(event)) {
|
|
||||||
return LRESULT(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(mut input_handler) = self.input_handler.take() {
|
|
||||||
if let Some(ime_key) = ime_key {
|
|
||||||
input_handler.replace_text_in_range(None, &ime_key);
|
|
||||||
}
|
|
||||||
self.input_handler.set(Some(input_handler));
|
|
||||||
return LRESULT(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return LRESULT(1);
|
|
||||||
|
let key = match VIRTUAL_KEY(vk_code) {
|
||||||
|
VK_BACK => Some("backspace"),
|
||||||
|
VK_RETURN => Some("enter"),
|
||||||
|
VK_TAB => Some("tab"),
|
||||||
|
VK_UP => Some("up"),
|
||||||
|
VK_DOWN => Some("down"),
|
||||||
|
VK_RIGHT => Some("right"),
|
||||||
|
VK_LEFT => Some("left"),
|
||||||
|
VK_HOME => Some("home"),
|
||||||
|
VK_END => Some("end"),
|
||||||
|
VK_PRIOR => Some("pageup"),
|
||||||
|
VK_NEXT => Some("pagedown"),
|
||||||
|
VK_ESCAPE => Some("escape"),
|
||||||
|
VK_INSERT => Some("insert"),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(key) = key {
|
||||||
|
Some(Keystroke {
|
||||||
|
modifiers,
|
||||||
|
key: key.to_string(),
|
||||||
|
ime_key: None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_char_msg_keystroke(&self, wparam: WPARAM) -> Option<Keystroke> {
|
||||||
|
let src = [wparam.0 as u16];
|
||||||
|
let Ok(first_char) = char::decode_utf16(src).collect::<Vec<_>>()[0] else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
if first_char.is_control() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Keystroke {
|
||||||
|
modifiers: self.current_modifiers(),
|
||||||
|
key: first_char.to_lowercase().to_string(),
|
||||||
|
ime_key: Some(first_char.to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_syskeydown_msg(&self, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||||
|
// we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
|
||||||
|
// shortcuts.
|
||||||
|
let Some(keystroke) = self.parse_syskeydown_msg_keystroke(wparam) else {
|
||||||
|
return unsafe { DefWindowProcW(self.hwnd, message, wparam, lparam) };
|
||||||
|
};
|
||||||
|
let Some(ref mut func) = self.callbacks.borrow_mut().input else {
|
||||||
|
return unsafe { DefWindowProcW(self.hwnd, message, wparam, lparam) };
|
||||||
|
};
|
||||||
|
let event = KeyDownEvent {
|
||||||
|
keystroke,
|
||||||
|
is_held: lparam.0 & (0x1 << 30) > 0,
|
||||||
|
};
|
||||||
|
if func(PlatformInput::KeyDown(event)) {
|
||||||
|
self.invalidate_client_area();
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
unsafe { DefWindowProcW(self.hwnd, message, wparam, lparam) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_syskeyup_msg(&self, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||||
|
// we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
|
||||||
|
// shortcuts.
|
||||||
|
let Some(keystroke) = self.parse_syskeydown_msg_keystroke(wparam) else {
|
||||||
|
return unsafe { DefWindowProcW(self.hwnd, message, wparam, lparam) };
|
||||||
|
};
|
||||||
|
let Some(ref mut func) = self.callbacks.borrow_mut().input else {
|
||||||
|
return unsafe { DefWindowProcW(self.hwnd, message, wparam, lparam) };
|
||||||
|
};
|
||||||
|
let event = KeyUpEvent { keystroke };
|
||||||
|
if func(PlatformInput::KeyUp(event)) {
|
||||||
|
self.invalidate_client_area();
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
unsafe { DefWindowProcW(self.hwnd, message, wparam, lparam) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_keydown_msg(&self, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||||
|
let Some(keystroke) = self.parse_keydown_msg_keystroke(wparam) else {
|
||||||
|
return LRESULT(1);
|
||||||
|
};
|
||||||
|
let Some(ref mut func) = self.callbacks.borrow_mut().input else {
|
||||||
|
return LRESULT(1);
|
||||||
|
};
|
||||||
|
let event = KeyDownEvent {
|
||||||
|
keystroke,
|
||||||
|
is_held: lparam.0 & (0x1 << 30) > 0,
|
||||||
|
};
|
||||||
|
if func(PlatformInput::KeyDown(event)) {
|
||||||
|
self.invalidate_client_area();
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
LRESULT(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_keyup_msg(&self, message: u32, wparam: WPARAM) -> LRESULT {
|
||||||
|
let Some(keystroke) = self.parse_keydown_msg_keystroke(wparam) else {
|
||||||
|
return LRESULT(1);
|
||||||
|
};
|
||||||
|
let Some(ref mut func) = self.callbacks.borrow_mut().input else {
|
||||||
|
return LRESULT(1);
|
||||||
|
};
|
||||||
|
let event = KeyUpEvent { keystroke };
|
||||||
|
if func(PlatformInput::KeyUp(event)) {
|
||||||
|
self.invalidate_client_area();
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
LRESULT(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_char_msg(&self, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||||
|
let Some(keystroke) = self.parse_char_msg_keystroke(wparam) else {
|
||||||
|
return LRESULT(1);
|
||||||
|
};
|
||||||
|
let mut callbacks = self.callbacks.borrow_mut();
|
||||||
|
let Some(ref mut func) = callbacks.input else {
|
||||||
|
return LRESULT(1);
|
||||||
|
};
|
||||||
|
let ime_key = keystroke.ime_key.clone();
|
||||||
|
let event = KeyDownEvent {
|
||||||
|
keystroke,
|
||||||
|
is_held: lparam.0 & (0x1 << 30) > 0,
|
||||||
|
};
|
||||||
|
if func(PlatformInput::KeyDown(event)) {
|
||||||
|
self.invalidate_client_area();
|
||||||
|
return LRESULT(0);
|
||||||
|
}
|
||||||
|
drop(callbacks);
|
||||||
|
let Some(ime_char) = ime_key else {
|
||||||
|
return LRESULT(1);
|
||||||
|
};
|
||||||
|
let Some(mut input_handler) = self.input_handler.take() else {
|
||||||
|
return LRESULT(1);
|
||||||
|
};
|
||||||
|
input_handler.replace_text_in_range(None, &ime_char);
|
||||||
|
self.input_handler.set(Some(input_handler));
|
||||||
|
self.invalidate_client_area();
|
||||||
|
LRESULT(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_mouse_down_msg(&self, button: MouseButton, lparam: LPARAM) -> LRESULT {
|
fn handle_mouse_down_msg(&self, button: MouseButton, lparam: LPARAM) -> LRESULT {
|
||||||
@ -671,9 +706,9 @@ impl WindowsWindow {
|
|||||||
drag_drop_handler,
|
drag_drop_handler,
|
||||||
};
|
};
|
||||||
platform_inner
|
platform_inner
|
||||||
.window_handle_values
|
.raw_window_handles
|
||||||
.borrow_mut()
|
.write()
|
||||||
.insert(wnd.inner.hwnd.0);
|
.push(wnd.inner.hwnd);
|
||||||
|
|
||||||
unsafe { ShowWindow(wnd.inner.hwnd, SW_SHOW) };
|
unsafe { ShowWindow(wnd.inner.hwnd, SW_SHOW) };
|
||||||
wnd
|
wnd
|
||||||
@ -1134,5 +1169,58 @@ unsafe fn set_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX, dwnewlong:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn basic_vkcode_to_string(code: u16, modifiers: Modifiers) -> Option<Keystroke> {
|
||||||
|
match code {
|
||||||
|
// VK_0 - VK_9
|
||||||
|
48..=57 => Some(Keystroke {
|
||||||
|
modifiers,
|
||||||
|
key: format!("{}", code - VK_0.0),
|
||||||
|
ime_key: None,
|
||||||
|
}),
|
||||||
|
// VK_A - VK_Z
|
||||||
|
65..=90 => Some(Keystroke {
|
||||||
|
modifiers,
|
||||||
|
key: format!("{}", (b'a' + code as u8 - VK_A.0 as u8) as char),
|
||||||
|
ime_key: None,
|
||||||
|
}),
|
||||||
|
// VK_F1 - VK_F24
|
||||||
|
112..=135 => Some(Keystroke {
|
||||||
|
modifiers,
|
||||||
|
key: format!("f{}", code - VK_F1.0 + 1),
|
||||||
|
ime_key: None,
|
||||||
|
}),
|
||||||
|
// OEM3: `/~, OEM_MINUS: -/_, OEM_PLUS: =/+, ...
|
||||||
|
_ => {
|
||||||
|
if let Some(key) = oemkey_vkcode_to_string(code) {
|
||||||
|
Some(Keystroke {
|
||||||
|
modifiers,
|
||||||
|
key,
|
||||||
|
ime_key: None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oemkey_vkcode_to_string(code: u16) -> Option<String> {
|
||||||
|
match code {
|
||||||
|
186 => Some(";".to_string()), // VK_OEM_1
|
||||||
|
187 => Some("=".to_string()), // VK_OEM_PLUS
|
||||||
|
188 => Some(",".to_string()), // VK_OEM_COMMA
|
||||||
|
189 => Some("-".to_string()), // VK_OEM_MINUS
|
||||||
|
190 => Some(".".to_string()), // VK_OEM_PERIOD
|
||||||
|
// https://kbdlayout.info/features/virtualkeys/VK_ABNT_C1
|
||||||
|
191 | 193 => Some("/".to_string()), // VK_OEM_2 VK_ABNT_C1
|
||||||
|
192 => Some("`".to_string()), // VK_OEM_3
|
||||||
|
219 => Some("[".to_string()), // VK_OEM_4
|
||||||
|
220 => Some("\\".to_string()), // VK_OEM_5
|
||||||
|
221 => Some("]".to_string()), // VK_OEM_6
|
||||||
|
222 => Some("'".to_string()), // VK_OEM_7
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
|
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
|
||||||
const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
|
const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
|
||||||
|
Loading…
Reference in New Issue
Block a user