Trying to clena the mess opened by plugins
Some checks failed
Deploy Website / deploy (push) Failing after 4m53s
Some checks failed
Deploy Website / deploy (push) Failing after 4m53s
This commit is contained in:
54
plugins/baseview/src/win/cursor.rs
Normal file
54
plugins/baseview/src/win/cursor.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::MouseCursor;
|
||||
use winapi::{
|
||||
shared::ntdef::LPCWSTR,
|
||||
um::winuser::{
|
||||
IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP, IDC_IBEAM, IDC_NO, IDC_SIZEALL,
|
||||
IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE, IDC_WAIT,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn cursor_to_lpcwstr(cursor: MouseCursor) -> LPCWSTR {
|
||||
match cursor {
|
||||
MouseCursor::Default => IDC_ARROW,
|
||||
MouseCursor::Hand => IDC_HAND,
|
||||
MouseCursor::HandGrabbing => IDC_SIZEALL,
|
||||
MouseCursor::Help => IDC_HELP,
|
||||
// an empty LPCWSTR results in the cursor being hidden
|
||||
MouseCursor::Hidden => std::ptr::null(),
|
||||
|
||||
MouseCursor::Text => IDC_IBEAM,
|
||||
MouseCursor::VerticalText => IDC_IBEAM,
|
||||
|
||||
MouseCursor::Working => IDC_WAIT,
|
||||
MouseCursor::PtrWorking => IDC_APPSTARTING,
|
||||
|
||||
MouseCursor::NotAllowed => IDC_NO,
|
||||
MouseCursor::PtrNotAllowed => IDC_NO,
|
||||
|
||||
MouseCursor::ZoomIn => IDC_ARROW,
|
||||
MouseCursor::ZoomOut => IDC_ARROW,
|
||||
|
||||
MouseCursor::Alias => IDC_ARROW,
|
||||
MouseCursor::Copy => IDC_ARROW,
|
||||
MouseCursor::Move => IDC_SIZEALL,
|
||||
MouseCursor::AllScroll => IDC_SIZEALL,
|
||||
MouseCursor::Cell => IDC_CROSS,
|
||||
MouseCursor::Crosshair => IDC_CROSS,
|
||||
|
||||
MouseCursor::EResize => IDC_SIZEWE,
|
||||
MouseCursor::NResize => IDC_SIZENS,
|
||||
MouseCursor::NeResize => IDC_SIZENESW,
|
||||
MouseCursor::NwResize => IDC_SIZENWSE,
|
||||
MouseCursor::SResize => IDC_SIZENS,
|
||||
MouseCursor::SeResize => IDC_SIZENWSE,
|
||||
MouseCursor::SwResize => IDC_SIZENESW,
|
||||
MouseCursor::WResize => IDC_SIZEWE,
|
||||
MouseCursor::EwResize => IDC_SIZEWE,
|
||||
MouseCursor::NsResize => IDC_SIZENS,
|
||||
MouseCursor::NwseResize => IDC_SIZENWSE,
|
||||
MouseCursor::NeswResize => IDC_SIZENESW,
|
||||
|
||||
MouseCursor::ColResize => IDC_SIZEWE,
|
||||
MouseCursor::RowResize => IDC_SIZENS,
|
||||
}
|
||||
}
|
||||
288
plugins/baseview/src/win/drop_target.rs
Normal file
288
plugins/baseview/src/win/drop_target.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
use std::ffi::OsString;
|
||||
use std::mem::transmute;
|
||||
use std::os::windows::prelude::OsStringExt;
|
||||
use std::ptr::null_mut;
|
||||
use std::rc::{Rc, Weak};
|
||||
|
||||
use winapi::shared::guiddef::{IsEqualIID, REFIID};
|
||||
use winapi::shared::minwindef::{DWORD, WPARAM};
|
||||
use winapi::shared::ntdef::{HRESULT, ULONG};
|
||||
use winapi::shared::windef::{POINT, POINTL};
|
||||
use winapi::shared::winerror::{E_NOINTERFACE, E_UNEXPECTED, S_OK};
|
||||
use winapi::shared::wtypes::DVASPECT_CONTENT;
|
||||
use winapi::um::objidl::{IDataObject, FORMATETC, STGMEDIUM, TYMED_HGLOBAL};
|
||||
use winapi::um::oleidl::{
|
||||
IDropTarget, IDropTargetVtbl, DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE,
|
||||
DROPEFFECT_NONE, DROPEFFECT_SCROLL,
|
||||
};
|
||||
use winapi::um::shellapi::{DragQueryFileW, HDROP};
|
||||
use winapi::um::unknwnbase::{IUnknown, IUnknownVtbl};
|
||||
use winapi::um::winuser::{ScreenToClient, CF_HDROP};
|
||||
use winapi::Interface;
|
||||
|
||||
use crate::{DropData, DropEffect, Event, EventStatus, MouseEvent, PhyPoint, Point};
|
||||
|
||||
use super::WindowState;
|
||||
|
||||
// These function pointers have to be stored in a (const) variable before they can be transmuted
|
||||
// Transmuting is needed because winapi has a bug where the pt parameter has an incorrect
|
||||
// type `*const POINTL`
|
||||
#[allow(non_snake_case)]
|
||||
const DRAG_ENTER_PTR: unsafe extern "system" fn(
|
||||
this: *mut IDropTarget,
|
||||
pDataObj: *const IDataObject,
|
||||
grfKeyState: DWORD,
|
||||
pt: POINTL,
|
||||
pdwEffect: *mut DWORD,
|
||||
) -> HRESULT = DropTarget::drag_enter;
|
||||
#[allow(non_snake_case)]
|
||||
const DRAG_OVER_PTR: unsafe extern "system" fn(
|
||||
this: *mut IDropTarget,
|
||||
grfKeyState: DWORD,
|
||||
pt: POINTL,
|
||||
pdwEffect: *mut DWORD,
|
||||
) -> HRESULT = DropTarget::drag_over;
|
||||
#[allow(non_snake_case)]
|
||||
const DROP_PTR: unsafe extern "system" fn(
|
||||
this: *mut IDropTarget,
|
||||
pDataObj: *const IDataObject,
|
||||
grfKeyState: DWORD,
|
||||
pt: POINTL,
|
||||
pdwEffect: *mut DWORD,
|
||||
) -> HRESULT = DropTarget::drop;
|
||||
|
||||
#[allow(clippy::missing_transmute_annotations)]
|
||||
const DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl {
|
||||
parent: IUnknownVtbl {
|
||||
QueryInterface: DropTarget::query_interface,
|
||||
AddRef: DropTarget::add_ref,
|
||||
Release: DropTarget::release,
|
||||
},
|
||||
DragEnter: unsafe { transmute(DRAG_ENTER_PTR) },
|
||||
DragOver: unsafe { transmute(DRAG_OVER_PTR) },
|
||||
DragLeave: DropTarget::drag_leave,
|
||||
Drop: unsafe { transmute(DROP_PTR) },
|
||||
};
|
||||
|
||||
#[repr(C)]
|
||||
pub(super) struct DropTarget {
|
||||
base: IDropTarget,
|
||||
|
||||
window_state: Weak<WindowState>,
|
||||
|
||||
// These are cached since DragOver and DragLeave callbacks don't provide them,
|
||||
// and handling drag move events gets awkward on the client end otherwise
|
||||
drag_position: Point,
|
||||
drag_screen_position: Point,
|
||||
drop_data: DropData,
|
||||
}
|
||||
|
||||
impl DropTarget {
|
||||
pub(super) fn new(window_state: Weak<WindowState>) -> Self {
|
||||
Self {
|
||||
base: IDropTarget { lpVtbl: &DROP_TARGET_VTBL },
|
||||
|
||||
window_state,
|
||||
|
||||
drag_position: Point::new(0.0, 0.0),
|
||||
drag_screen_position: Point::new(0.0, 0.0),
|
||||
drop_data: DropData::None,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn on_event(&self, pdwEffect: Option<*mut DWORD>, event: MouseEvent) {
|
||||
let Some(window_state) = self.window_state.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
let event = Event::Mouse(event);
|
||||
let event_status =
|
||||
window_state.handler_mut().as_mut().unwrap().on_event(&mut window, event);
|
||||
|
||||
if let Some(pdwEffect) = pdwEffect {
|
||||
match event_status {
|
||||
EventStatus::AcceptDrop(DropEffect::Copy) => *pdwEffect = DROPEFFECT_COPY,
|
||||
EventStatus::AcceptDrop(DropEffect::Move) => *pdwEffect = DROPEFFECT_MOVE,
|
||||
EventStatus::AcceptDrop(DropEffect::Link) => *pdwEffect = DROPEFFECT_LINK,
|
||||
EventStatus::AcceptDrop(DropEffect::Scroll) => *pdwEffect = DROPEFFECT_SCROLL,
|
||||
_ => *pdwEffect = DROPEFFECT_NONE,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_coordinates(&mut self, pt: POINTL) {
|
||||
let Some(window_state) = self.window_state.upgrade() else {
|
||||
return;
|
||||
};
|
||||
self.drag_screen_position = Point::new(pt.x as f64, pt.y as f64);
|
||||
let mut pt = POINT { x: pt.x, y: pt.y };
|
||||
unsafe { ScreenToClient(window_state.hwnd, &mut pt as *mut POINT) };
|
||||
let phy_point = PhyPoint::new(pt.x, pt.y);
|
||||
self.drag_position = phy_point.to_logical(&window_state.window_info());
|
||||
}
|
||||
|
||||
fn parse_drop_data(&mut self, data_object: &IDataObject) {
|
||||
let format = FORMATETC {
|
||||
cfFormat: CF_HDROP as u16,
|
||||
ptd: null_mut(),
|
||||
dwAspect: DVASPECT_CONTENT,
|
||||
lindex: -1,
|
||||
tymed: TYMED_HGLOBAL,
|
||||
};
|
||||
|
||||
let mut medium = STGMEDIUM { tymed: 0, u: null_mut(), pUnkForRelease: null_mut() };
|
||||
|
||||
unsafe {
|
||||
let hresult = data_object.GetData(&format, &mut medium);
|
||||
if hresult != S_OK {
|
||||
self.drop_data = DropData::None;
|
||||
return;
|
||||
}
|
||||
|
||||
let hdrop = *(*medium.u).hGlobal() as HDROP;
|
||||
|
||||
let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, null_mut(), 0);
|
||||
if item_count == 0 {
|
||||
self.drop_data = DropData::None;
|
||||
return;
|
||||
}
|
||||
|
||||
let mut paths = Vec::with_capacity(item_count as usize);
|
||||
|
||||
for i in 0..item_count {
|
||||
let characters = DragQueryFileW(hdrop, i, null_mut(), 0);
|
||||
let buffer_size = characters as usize + 1;
|
||||
let mut buffer = vec![0u16; buffer_size];
|
||||
|
||||
DragQueryFileW(hdrop, i, buffer.as_mut_ptr().cast(), buffer_size as u32);
|
||||
|
||||
paths.push(OsString::from_wide(&buffer[..characters as usize]).into())
|
||||
}
|
||||
|
||||
self.drop_data = DropData::Files(paths);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe extern "system" fn query_interface(
|
||||
this: *mut IUnknown, riid: REFIID, ppvObject: *mut *mut winapi::ctypes::c_void,
|
||||
) -> HRESULT {
|
||||
if IsEqualIID(&*riid, &IUnknown::uuidof()) || IsEqualIID(&*riid, &IDropTarget::uuidof()) {
|
||||
Self::add_ref(this);
|
||||
*ppvObject = this as *mut winapi::ctypes::c_void;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
E_NOINTERFACE
|
||||
}
|
||||
|
||||
unsafe extern "system" fn add_ref(this: *mut IUnknown) -> ULONG {
|
||||
let arc = Rc::from_raw(this);
|
||||
let result = Rc::strong_count(&arc) + 1;
|
||||
let _ = Rc::into_raw(arc);
|
||||
|
||||
Rc::increment_strong_count(this);
|
||||
|
||||
result as ULONG
|
||||
}
|
||||
|
||||
unsafe extern "system" fn release(this: *mut IUnknown) -> ULONG {
|
||||
let arc = Rc::from_raw(this);
|
||||
let result = Rc::strong_count(&arc) - 1;
|
||||
let _ = Rc::into_raw(arc);
|
||||
|
||||
Rc::decrement_strong_count(this);
|
||||
|
||||
result as ULONG
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe extern "system" fn drag_enter(
|
||||
this: *mut IDropTarget, pDataObj: *const IDataObject, grfKeyState: DWORD, pt: POINTL,
|
||||
pdwEffect: *mut DWORD,
|
||||
) -> HRESULT {
|
||||
let drop_target = &mut *(this as *mut DropTarget);
|
||||
let Some(window_state) = drop_target.window_state.upgrade() else {
|
||||
return E_UNEXPECTED;
|
||||
};
|
||||
|
||||
let modifiers =
|
||||
window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM);
|
||||
|
||||
drop_target.parse_coordinates(pt);
|
||||
drop_target.parse_drop_data(&*pDataObj);
|
||||
|
||||
let event = MouseEvent::DragEntered {
|
||||
position: drop_target.drag_position,
|
||||
screen_position: drop_target.drag_screen_position,
|
||||
modifiers,
|
||||
data: drop_target.drop_data.clone(),
|
||||
};
|
||||
|
||||
drop_target.on_event(Some(pdwEffect), event);
|
||||
S_OK
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe extern "system" fn drag_over(
|
||||
this: *mut IDropTarget, grfKeyState: DWORD, pt: POINTL, pdwEffect: *mut DWORD,
|
||||
) -> HRESULT {
|
||||
let drop_target = &mut *(this as *mut DropTarget);
|
||||
let Some(window_state) = drop_target.window_state.upgrade() else {
|
||||
return E_UNEXPECTED;
|
||||
};
|
||||
|
||||
let modifiers =
|
||||
window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM);
|
||||
|
||||
drop_target.parse_coordinates(pt);
|
||||
|
||||
let event = MouseEvent::DragMoved {
|
||||
position: drop_target.drag_position,
|
||||
screen_position: drop_target.drag_screen_position,
|
||||
modifiers,
|
||||
data: drop_target.drop_data.clone(),
|
||||
};
|
||||
|
||||
drop_target.on_event(Some(pdwEffect), event);
|
||||
S_OK
|
||||
}
|
||||
|
||||
unsafe extern "system" fn drag_leave(this: *mut IDropTarget) -> HRESULT {
|
||||
let drop_target = &mut *(this as *mut DropTarget);
|
||||
drop_target.on_event(None, MouseEvent::DragLeft);
|
||||
S_OK
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe extern "system" fn drop(
|
||||
this: *mut IDropTarget, pDataObj: *const IDataObject, grfKeyState: DWORD, pt: POINTL,
|
||||
pdwEffect: *mut DWORD,
|
||||
) -> HRESULT {
|
||||
let drop_target = &mut *(this as *mut DropTarget);
|
||||
let Some(window_state) = drop_target.window_state.upgrade() else {
|
||||
return E_UNEXPECTED;
|
||||
};
|
||||
|
||||
let modifiers =
|
||||
window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM);
|
||||
|
||||
drop_target.parse_coordinates(pt);
|
||||
drop_target.parse_drop_data(&*pDataObj);
|
||||
|
||||
let event = MouseEvent::DragDropped {
|
||||
position: drop_target.drag_position,
|
||||
screen_position: drop_target.drag_screen_position,
|
||||
modifiers,
|
||||
data: drop_target.drop_data.clone(),
|
||||
};
|
||||
|
||||
drop_target.on_event(Some(pdwEffect), event);
|
||||
S_OK
|
||||
}
|
||||
}
|
||||
142
plugins/baseview/src/win/hook.rs
Normal file
142
plugins/baseview/src/win/hook.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
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
|
||||
}
|
||||
704
plugins/baseview/src/win/keyboard.rs
Normal file
704
plugins/baseview/src/win/keyboard.rs
Normal file
@@ -0,0 +1,704 @@
|
||||
// Copyright 2020 The Druid Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Baseview modifications to druid code:
|
||||
// - update imports, paths etc
|
||||
|
||||
//! Key event handling.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::mem;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Location, Modifiers};
|
||||
|
||||
use winapi::shared::minwindef::{HKL, INT, LPARAM, UINT, WPARAM};
|
||||
use winapi::shared::ntdef::SHORT;
|
||||
use winapi::shared::windef::HWND;
|
||||
use winapi::um::winuser::{
|
||||
GetKeyState, GetKeyboardLayout, MapVirtualKeyExW, PeekMessageW, ToUnicodeEx, MAPVK_VK_TO_CHAR,
|
||||
MAPVK_VSC_TO_VK_EX, MK_CONTROL, MK_SHIFT, PM_NOREMOVE, VK_ACCEPT, VK_ADD, VK_APPS, VK_ATTN,
|
||||
VK_BACK, VK_BROWSER_BACK, VK_BROWSER_FAVORITES, VK_BROWSER_FORWARD, VK_BROWSER_HOME,
|
||||
VK_BROWSER_REFRESH, VK_BROWSER_SEARCH, VK_BROWSER_STOP, VK_CANCEL, VK_CAPITAL, VK_CLEAR,
|
||||
VK_CONTROL, VK_CONVERT, VK_CRSEL, VK_DECIMAL, VK_DELETE, VK_DIVIDE, VK_DOWN, VK_END, VK_EREOF,
|
||||
VK_ESCAPE, VK_EXECUTE, VK_EXSEL, VK_F1, VK_F10, VK_F11, VK_F12, VK_F2, VK_F3, VK_F4, VK_F5,
|
||||
VK_F6, VK_F7, VK_F8, VK_F9, VK_FINAL, VK_HELP, VK_HOME, VK_INSERT, VK_JUNJA, VK_KANA, VK_KANJI,
|
||||
VK_LAUNCH_APP1, VK_LAUNCH_APP2, VK_LAUNCH_MAIL, VK_LAUNCH_MEDIA_SELECT, VK_LCONTROL, VK_LEFT,
|
||||
VK_LMENU, VK_LSHIFT, VK_LWIN, VK_MEDIA_NEXT_TRACK, VK_MEDIA_PLAY_PAUSE, VK_MEDIA_PREV_TRACK,
|
||||
VK_MEDIA_STOP, VK_MENU, VK_MODECHANGE, VK_MULTIPLY, VK_NEXT, VK_NONCONVERT, VK_NUMLOCK,
|
||||
VK_NUMPAD0, VK_NUMPAD1, VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, VK_NUMPAD7,
|
||||
VK_NUMPAD8, VK_NUMPAD9, VK_OEM_ATTN, VK_OEM_CLEAR, VK_PAUSE, VK_PLAY, VK_PRINT, VK_PRIOR,
|
||||
VK_PROCESSKEY, VK_RCONTROL, VK_RETURN, VK_RIGHT, VK_RMENU, VK_RSHIFT, VK_RWIN, VK_SCROLL,
|
||||
VK_SELECT, VK_SHIFT, VK_SLEEP, VK_SNAPSHOT, VK_SUBTRACT, VK_TAB, VK_UP, VK_VOLUME_DOWN,
|
||||
VK_VOLUME_MUTE, VK_VOLUME_UP, VK_ZOOM, WM_CHAR, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP,
|
||||
WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP,
|
||||
};
|
||||
|
||||
const VK_ABNT_C2: INT = 0xc2;
|
||||
|
||||
/// A (non-extended) virtual key code.
|
||||
type VkCode = u8;
|
||||
|
||||
// This is really bitfields.
|
||||
type ShiftState = u8;
|
||||
const SHIFT_STATE_SHIFT: ShiftState = 1;
|
||||
const SHIFT_STATE_ALTGR: ShiftState = 2;
|
||||
const N_SHIFT_STATE: ShiftState = 4;
|
||||
|
||||
/// Per-window keyboard state.
|
||||
pub(super) struct KeyboardState {
|
||||
hkl: HKL,
|
||||
// A map from (vk, is_shifted) to string val
|
||||
key_vals: HashMap<(VkCode, ShiftState), String>,
|
||||
dead_keys: HashSet<(VkCode, ShiftState)>,
|
||||
has_altgr: bool,
|
||||
stash_vk: Option<VkCode>,
|
||||
stash_utf16: Vec<u16>,
|
||||
}
|
||||
|
||||
/// Virtual key codes that are considered printable.
|
||||
///
|
||||
/// This logic is borrowed from KeyboardLayout::GetKeyIndex
|
||||
/// in Mozilla.
|
||||
const PRINTABLE_VKS: &[RangeInclusive<VkCode>] = &[
|
||||
0x20..=0x20,
|
||||
0x30..=0x39,
|
||||
0x41..=0x5A,
|
||||
0x60..=0x6B,
|
||||
0x6D..=0x6F,
|
||||
0xBA..=0xC2,
|
||||
0xDB..=0xDF,
|
||||
0xE1..=0xE4,
|
||||
];
|
||||
|
||||
/// Bits of lparam indicating scan code, including extended bit.
|
||||
const SCAN_MASK: LPARAM = 0x1ff_0000;
|
||||
|
||||
/// Determine whether there are more messages in the queue for this key event.
|
||||
///
|
||||
/// When this function returns `false`, there is another message in the queue
|
||||
/// with a matching scan code, therefore it is reasonable to stash the data
|
||||
/// from this message and defer til later to actually produce the event.
|
||||
unsafe fn is_last_message(hwnd: HWND, msg: UINT, lparam: LPARAM) -> bool {
|
||||
let expected_msg = match msg {
|
||||
WM_KEYDOWN | WM_CHAR => WM_CHAR,
|
||||
WM_SYSKEYDOWN | WM_SYSCHAR => WM_SYSCHAR,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut msg = mem::zeroed();
|
||||
let avail = PeekMessageW(&mut msg, hwnd, expected_msg, expected_msg, PM_NOREMOVE);
|
||||
avail == 0 || msg.lParam & SCAN_MASK != lparam & SCAN_MASK
|
||||
}
|
||||
|
||||
const MODIFIER_MAP: &[(INT, Modifiers, SHORT)] = &[
|
||||
(VK_MENU, Modifiers::ALT, 0x80),
|
||||
(VK_CAPITAL, Modifiers::CAPS_LOCK, 0x1),
|
||||
(VK_CONTROL, Modifiers::CONTROL, 0x80),
|
||||
(VK_NUMLOCK, Modifiers::NUM_LOCK, 0x1),
|
||||
(VK_SCROLL, Modifiers::SCROLL_LOCK, 0x1),
|
||||
(VK_SHIFT, Modifiers::SHIFT, 0x80),
|
||||
];
|
||||
|
||||
/// Convert scan code to W3C standard code.
|
||||
///
|
||||
/// It's hard to get an authoritative source for this; it's mostly based
|
||||
/// on NativeKeyToDOMCodeName.h in Mozilla.
|
||||
fn scan_to_code(scan_code: u32) -> Code {
|
||||
use Code::*;
|
||||
match scan_code {
|
||||
0x1 => Escape,
|
||||
0x2 => Digit1,
|
||||
0x3 => Digit2,
|
||||
0x4 => Digit3,
|
||||
0x5 => Digit4,
|
||||
0x6 => Digit5,
|
||||
0x7 => Digit6,
|
||||
0x8 => Digit7,
|
||||
0x9 => Digit8,
|
||||
0xA => Digit9,
|
||||
0xB => Digit0,
|
||||
0xC => Minus,
|
||||
0xD => Equal,
|
||||
0xE => Backspace,
|
||||
0xF => Tab,
|
||||
0x10 => KeyQ,
|
||||
0x11 => KeyW,
|
||||
0x12 => KeyE,
|
||||
0x13 => KeyR,
|
||||
0x14 => KeyT,
|
||||
0x15 => KeyY,
|
||||
0x16 => KeyU,
|
||||
0x17 => KeyI,
|
||||
0x18 => KeyO,
|
||||
0x19 => KeyP,
|
||||
0x1A => BracketLeft,
|
||||
0x1B => BracketRight,
|
||||
0x1C => Enter,
|
||||
0x1D => ControlLeft,
|
||||
0x1E => KeyA,
|
||||
0x1F => KeyS,
|
||||
0x20 => KeyD,
|
||||
0x21 => KeyF,
|
||||
0x22 => KeyG,
|
||||
0x23 => KeyH,
|
||||
0x24 => KeyJ,
|
||||
0x25 => KeyK,
|
||||
0x26 => KeyL,
|
||||
0x27 => Semicolon,
|
||||
0x28 => Quote,
|
||||
0x29 => Backquote,
|
||||
0x2A => ShiftLeft,
|
||||
0x2B => Backslash,
|
||||
0x2C => KeyZ,
|
||||
0x2D => KeyX,
|
||||
0x2E => KeyC,
|
||||
0x2F => KeyV,
|
||||
0x30 => KeyB,
|
||||
0x31 => KeyN,
|
||||
0x32 => KeyM,
|
||||
0x33 => Comma,
|
||||
0x34 => Period,
|
||||
0x35 => Slash,
|
||||
0x36 => ShiftRight,
|
||||
0x37 => NumpadMultiply,
|
||||
0x38 => AltLeft,
|
||||
0x39 => Space,
|
||||
0x3A => CapsLock,
|
||||
0x3B => F1,
|
||||
0x3C => F2,
|
||||
0x3D => F3,
|
||||
0x3E => F4,
|
||||
0x3F => F5,
|
||||
0x40 => F6,
|
||||
0x41 => F7,
|
||||
0x42 => F8,
|
||||
0x43 => F9,
|
||||
0x44 => F10,
|
||||
0x45 => Pause,
|
||||
0x46 => ScrollLock,
|
||||
0x47 => Numpad7,
|
||||
0x48 => Numpad8,
|
||||
0x49 => Numpad9,
|
||||
0x4A => NumpadSubtract,
|
||||
0x4B => Numpad4,
|
||||
0x4C => Numpad5,
|
||||
0x4D => Numpad6,
|
||||
0x4E => NumpadAdd,
|
||||
0x4F => Numpad1,
|
||||
0x50 => Numpad2,
|
||||
0x51 => Numpad3,
|
||||
0x52 => Numpad0,
|
||||
0x53 => NumpadDecimal,
|
||||
0x54 => PrintScreen,
|
||||
0x56 => IntlBackslash,
|
||||
0x57 => F11,
|
||||
0x58 => F12,
|
||||
0x59 => NumpadEqual,
|
||||
0x70 => KanaMode,
|
||||
0x71 => Lang2,
|
||||
0x72 => Lang1,
|
||||
0x73 => IntlRo,
|
||||
0x79 => Convert,
|
||||
0x7B => NonConvert,
|
||||
0x7D => IntlYen,
|
||||
0x7E => NumpadComma,
|
||||
0x110 => MediaTrackPrevious,
|
||||
0x119 => MediaTrackNext,
|
||||
0x11C => NumpadEnter,
|
||||
0x11D => ControlRight,
|
||||
0x120 => AudioVolumeMute,
|
||||
0x121 => LaunchApp2,
|
||||
0x122 => MediaPlayPause,
|
||||
0x124 => MediaStop,
|
||||
0x12E => AudioVolumeDown,
|
||||
0x130 => AudioVolumeUp,
|
||||
0x132 => BrowserHome,
|
||||
0x135 => NumpadDivide,
|
||||
0x137 => PrintScreen,
|
||||
0x138 => AltRight,
|
||||
0x145 => NumLock,
|
||||
0x147 => Home,
|
||||
0x148 => ArrowUp,
|
||||
0x149 => PageUp,
|
||||
0x14B => ArrowLeft,
|
||||
0x14D => ArrowRight,
|
||||
0x14F => End,
|
||||
0x150 => ArrowDown,
|
||||
0x151 => PageDown,
|
||||
0x152 => Insert,
|
||||
0x153 => Delete,
|
||||
0x15B => MetaLeft,
|
||||
0x15C => MetaRight,
|
||||
0x15D => ContextMenu,
|
||||
0x15E => Power,
|
||||
0x165 => BrowserSearch,
|
||||
0x166 => BrowserFavorites,
|
||||
0x167 => BrowserRefresh,
|
||||
0x168 => BrowserStop,
|
||||
0x169 => BrowserForward,
|
||||
0x16A => BrowserBack,
|
||||
0x16B => LaunchApp1,
|
||||
0x16C => LaunchMail,
|
||||
0x16D => MediaSelect,
|
||||
0x1F1 => Lang2,
|
||||
0x1F2 => Lang1,
|
||||
_ => Unidentified,
|
||||
}
|
||||
}
|
||||
|
||||
fn vk_to_key(vk: VkCode) -> Option<Key> {
|
||||
Some(match vk as INT {
|
||||
VK_CANCEL => Key::Cancel,
|
||||
VK_BACK => Key::Backspace,
|
||||
VK_TAB => Key::Tab,
|
||||
VK_CLEAR => Key::Clear,
|
||||
VK_RETURN => Key::Enter,
|
||||
VK_SHIFT | VK_LSHIFT | VK_RSHIFT => Key::Shift,
|
||||
VK_CONTROL | VK_LCONTROL | VK_RCONTROL => Key::Control,
|
||||
VK_MENU | VK_LMENU | VK_RMENU => Key::Alt,
|
||||
VK_PAUSE => Key::Pause,
|
||||
VK_CAPITAL => Key::CapsLock,
|
||||
// TODO: disambiguate kana and hangul? same vk
|
||||
VK_KANA => Key::KanaMode,
|
||||
VK_JUNJA => Key::JunjaMode,
|
||||
VK_FINAL => Key::FinalMode,
|
||||
VK_KANJI => Key::KanjiMode,
|
||||
VK_ESCAPE => Key::Escape,
|
||||
VK_NONCONVERT => Key::NonConvert,
|
||||
VK_ACCEPT => Key::Accept,
|
||||
VK_PRIOR => Key::PageUp,
|
||||
VK_NEXT => Key::PageDown,
|
||||
VK_END => Key::End,
|
||||
VK_HOME => Key::Home,
|
||||
VK_LEFT => Key::ArrowLeft,
|
||||
VK_UP => Key::ArrowUp,
|
||||
VK_RIGHT => Key::ArrowRight,
|
||||
VK_DOWN => Key::ArrowDown,
|
||||
VK_SELECT => Key::Select,
|
||||
VK_PRINT => Key::Print,
|
||||
VK_EXECUTE => Key::Execute,
|
||||
VK_SNAPSHOT => Key::PrintScreen,
|
||||
VK_INSERT => Key::Insert,
|
||||
VK_DELETE => Key::Delete,
|
||||
VK_HELP => Key::Help,
|
||||
VK_LWIN | VK_RWIN => Key::Meta,
|
||||
VK_APPS => Key::ContextMenu,
|
||||
VK_SLEEP => Key::Standby,
|
||||
VK_F1 => Key::F1,
|
||||
VK_F2 => Key::F2,
|
||||
VK_F3 => Key::F3,
|
||||
VK_F4 => Key::F4,
|
||||
VK_F5 => Key::F5,
|
||||
VK_F6 => Key::F6,
|
||||
VK_F7 => Key::F7,
|
||||
VK_F8 => Key::F8,
|
||||
VK_F9 => Key::F9,
|
||||
VK_F10 => Key::F10,
|
||||
VK_F11 => Key::F11,
|
||||
VK_F12 => Key::F12,
|
||||
VK_NUMLOCK => Key::NumLock,
|
||||
VK_SCROLL => Key::ScrollLock,
|
||||
VK_BROWSER_BACK => Key::BrowserBack,
|
||||
VK_BROWSER_FORWARD => Key::BrowserForward,
|
||||
VK_BROWSER_REFRESH => Key::BrowserRefresh,
|
||||
VK_BROWSER_STOP => Key::BrowserStop,
|
||||
VK_BROWSER_SEARCH => Key::BrowserSearch,
|
||||
VK_BROWSER_FAVORITES => Key::BrowserFavorites,
|
||||
VK_BROWSER_HOME => Key::BrowserHome,
|
||||
VK_VOLUME_MUTE => Key::AudioVolumeMute,
|
||||
VK_VOLUME_DOWN => Key::AudioVolumeDown,
|
||||
VK_VOLUME_UP => Key::AudioVolumeUp,
|
||||
VK_MEDIA_NEXT_TRACK => Key::MediaTrackNext,
|
||||
VK_MEDIA_PREV_TRACK => Key::MediaTrackPrevious,
|
||||
VK_MEDIA_STOP => Key::MediaStop,
|
||||
VK_MEDIA_PLAY_PAUSE => Key::MediaPlayPause,
|
||||
VK_LAUNCH_MAIL => Key::LaunchMail,
|
||||
VK_LAUNCH_MEDIA_SELECT => Key::LaunchMediaPlayer,
|
||||
VK_LAUNCH_APP1 => Key::LaunchApplication1,
|
||||
VK_LAUNCH_APP2 => Key::LaunchApplication2,
|
||||
VK_OEM_ATTN => Key::Alphanumeric,
|
||||
VK_CONVERT => Key::Convert,
|
||||
VK_MODECHANGE => Key::ModeChange,
|
||||
VK_PROCESSKEY => Key::Process,
|
||||
VK_ATTN => Key::Attn,
|
||||
VK_CRSEL => Key::CrSel,
|
||||
VK_EXSEL => Key::ExSel,
|
||||
VK_EREOF => Key::EraseEof,
|
||||
VK_PLAY => Key::Play,
|
||||
VK_ZOOM => Key::ZoomToggle,
|
||||
VK_OEM_CLEAR => Key::Clear,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn code_unit_to_key(code_unit: u32) -> Key {
|
||||
match code_unit {
|
||||
0x8 | 0x7F => Key::Backspace,
|
||||
0x9 => Key::Tab,
|
||||
0xA | 0xD => Key::Enter,
|
||||
0x1B => Key::Escape,
|
||||
_ if code_unit >= 0x20 => {
|
||||
if let Some(c) = std::char::from_u32(code_unit) {
|
||||
Key::Character(c.to_string())
|
||||
} else {
|
||||
// UTF-16 error, very unlikely
|
||||
Key::Unidentified
|
||||
}
|
||||
}
|
||||
_ => Key::Unidentified,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get location from virtual key code.
|
||||
///
|
||||
/// This logic is based on NativeKey::GetKeyLocation from Mozilla.
|
||||
fn vk_to_location(vk: VkCode, is_extended: bool) -> Location {
|
||||
match vk as INT {
|
||||
VK_LSHIFT | VK_LCONTROL | VK_LMENU | VK_LWIN => Location::Left,
|
||||
VK_RSHIFT | VK_RCONTROL | VK_RMENU | VK_RWIN => Location::Right,
|
||||
VK_RETURN if is_extended => Location::Numpad,
|
||||
VK_INSERT | VK_DELETE | VK_END | VK_DOWN | VK_NEXT | VK_LEFT | VK_CLEAR | VK_RIGHT
|
||||
| VK_HOME | VK_UP | VK_PRIOR => {
|
||||
if is_extended {
|
||||
Location::Standard
|
||||
} else {
|
||||
Location::Numpad
|
||||
}
|
||||
}
|
||||
VK_NUMPAD0 | VK_NUMPAD1 | VK_NUMPAD2 | VK_NUMPAD3 | VK_NUMPAD4 | VK_NUMPAD5
|
||||
| VK_NUMPAD6 | VK_NUMPAD7 | VK_NUMPAD8 | VK_NUMPAD9 | VK_DECIMAL | VK_DIVIDE
|
||||
| VK_MULTIPLY | VK_SUBTRACT | VK_ADD | VK_ABNT_C2 => Location::Numpad,
|
||||
_ => Location::Standard,
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyboardState {
|
||||
/// Create a new keyboard state.
|
||||
///
|
||||
/// There should be one of these per window. It loads the current keyboard
|
||||
/// layout and retains some mapping information from it.
|
||||
pub(crate) fn new() -> KeyboardState {
|
||||
unsafe {
|
||||
let hkl = GetKeyboardLayout(0);
|
||||
let key_vals = HashMap::new();
|
||||
let dead_keys = HashSet::new();
|
||||
let stash_vk = None;
|
||||
let stash_utf16 = Vec::new();
|
||||
let has_altgr = false;
|
||||
let mut result =
|
||||
KeyboardState { hkl, key_vals, dead_keys, has_altgr, stash_vk, stash_utf16 };
|
||||
result.load_keyboard_layout();
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Process one message from the platform.
|
||||
///
|
||||
/// This is the main interface point for generating cooked keyboard events
|
||||
/// from raw platform messages. It should be called for each relevant message,
|
||||
/// which comprises: `WM_KEYDOWN`, `WM_KEYUP`, `WM_CHAR`, `WM_SYSKEYDOWN`,
|
||||
/// `WM_SYSKEYUP`, `WM_SYSCHAR`, and `WM_INPUTLANGCHANGE`.
|
||||
///
|
||||
/// As a general theory, many keyboard events generate a sequence of platform
|
||||
/// messages. In these cases, we stash information from all messages but the
|
||||
/// last, and generate the event from the last (using `PeekMessage` to detect
|
||||
/// that case). Mozilla handling is slightly different; it actually tries to
|
||||
/// do the processing on the first message, fetching the subsequent messages
|
||||
/// from the queue. We believe our handling is simpler and more robust.
|
||||
///
|
||||
/// A simple example of a multi-message sequence is the key "=". In a US layout,
|
||||
/// we'd expect `WM_KEYDOWN` with `wparam = VK_OEM_PLUS` and lparam encoding the
|
||||
/// keycode that translates into `Code::Equal`, followed by a `WM_CHAR` with
|
||||
/// `wparam = b"="` and the same scancode.
|
||||
///
|
||||
/// A more complex example of a multi-message sequence is the second press of
|
||||
/// that key in a German layout, where it's mapped to the dead key for accent
|
||||
/// acute. Then we expect `WM_KEYDOWN` with `wparam = VK_OEM_6` followed by
|
||||
/// two `WM_CHAR` with `wparam = 0xB4` (corresponding to U+00B4 = acute accent).
|
||||
/// In this case, the result (produced on the final message in the sequence) is
|
||||
/// a key event with `key = Key::Character("´´")`, which also matches browser
|
||||
/// behavior.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The `hwnd` argument must be a valid `HWND`. Similarly, the `lparam` must be
|
||||
/// a valid `HKL` reference in the `WM_INPUTLANGCHANGE` message. Actual danger
|
||||
/// is likely low, though.
|
||||
pub(crate) unsafe fn process_message(
|
||||
&mut self, hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM,
|
||||
) -> Option<KeyboardEvent> {
|
||||
match msg {
|
||||
WM_KEYDOWN | WM_SYSKEYDOWN => {
|
||||
//println!("keydown wparam {:x} lparam {:x}", wparam, lparam);
|
||||
let scan_code = ((lparam & SCAN_MASK) >> 16) as u32;
|
||||
let vk = self.refine_vk(wparam as u8, scan_code);
|
||||
if is_last_message(hwnd, msg, lparam) {
|
||||
let modifiers = self.get_modifiers();
|
||||
let code = scan_to_code(scan_code);
|
||||
let key = vk_to_key(vk).unwrap_or_else(|| self.get_base_key(vk, modifiers));
|
||||
let repeat = (lparam & 0x4000_0000) != 0;
|
||||
let is_extended = (lparam & 0x100_0000) != 0;
|
||||
let location = vk_to_location(vk, is_extended);
|
||||
let state = KeyState::Down;
|
||||
let event = KeyboardEvent {
|
||||
state,
|
||||
modifiers,
|
||||
code,
|
||||
key,
|
||||
is_composing: false,
|
||||
location,
|
||||
repeat,
|
||||
};
|
||||
Some(event)
|
||||
} else {
|
||||
self.stash_vk = Some(vk);
|
||||
None
|
||||
}
|
||||
}
|
||||
WM_KEYUP | WM_SYSKEYUP => {
|
||||
let scan_code = ((lparam & SCAN_MASK) >> 16) as u32;
|
||||
let vk = self.refine_vk(wparam as u8, scan_code);
|
||||
let modifiers = self.get_modifiers();
|
||||
let code = scan_to_code(scan_code);
|
||||
let key = vk_to_key(vk).unwrap_or_else(|| self.get_base_key(vk, modifiers));
|
||||
let repeat = false;
|
||||
let is_extended = (lparam & 0x100_0000) != 0;
|
||||
let location = vk_to_location(vk, is_extended);
|
||||
let state = KeyState::Up;
|
||||
let event = KeyboardEvent {
|
||||
state,
|
||||
modifiers,
|
||||
code,
|
||||
key,
|
||||
is_composing: false,
|
||||
location,
|
||||
repeat,
|
||||
};
|
||||
Some(event)
|
||||
}
|
||||
WM_CHAR | WM_SYSCHAR => {
|
||||
//println!("char wparam {:x} lparam {:x}", wparam, lparam);
|
||||
if is_last_message(hwnd, msg, lparam) {
|
||||
let stash_vk = self.stash_vk.take();
|
||||
let modifiers = self.get_modifiers();
|
||||
let scan_code = ((lparam & SCAN_MASK) >> 16) as u32;
|
||||
let vk = self.refine_vk(stash_vk.unwrap_or(0), scan_code);
|
||||
let code = scan_to_code(scan_code);
|
||||
let key = if self.stash_utf16.is_empty() && wparam < 0x20 {
|
||||
vk_to_key(vk).unwrap_or_else(|| self.get_base_key(vk, modifiers))
|
||||
} else {
|
||||
self.stash_utf16.push(wparam as u16);
|
||||
if let Ok(s) = String::from_utf16(&self.stash_utf16) {
|
||||
Key::Character(s)
|
||||
} else {
|
||||
Key::Unidentified
|
||||
}
|
||||
};
|
||||
self.stash_utf16.clear();
|
||||
let repeat = (lparam & 0x4000_0000) != 0;
|
||||
let is_extended = (lparam & 0x100_0000) != 0;
|
||||
let location = vk_to_location(vk, is_extended);
|
||||
let state = KeyState::Down;
|
||||
let event = KeyboardEvent {
|
||||
state,
|
||||
modifiers,
|
||||
code,
|
||||
key,
|
||||
is_composing: false,
|
||||
location,
|
||||
repeat,
|
||||
};
|
||||
Some(event)
|
||||
} else {
|
||||
self.stash_utf16.push(wparam as u16);
|
||||
None
|
||||
}
|
||||
}
|
||||
WM_INPUTLANGCHANGE => {
|
||||
self.hkl = lparam as HKL;
|
||||
self.load_keyboard_layout();
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the modifier state.
|
||||
///
|
||||
/// This function is designed to be called from a message handler, and
|
||||
/// gives the modifier state at the time of the message (ie is the
|
||||
/// synchronous variant). See [`GetKeyState`] for more context.
|
||||
///
|
||||
/// The interpretation of modifiers depends on the keyboard layout, as
|
||||
/// some layouts have [AltGr] and others do not.
|
||||
///
|
||||
/// [`GetKeyState`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getkeystate
|
||||
/// [AltGr]: https://en.wikipedia.org/wiki/AltGr_key
|
||||
pub(crate) fn get_modifiers(&self) -> Modifiers {
|
||||
unsafe {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
for &(vk, modifier, mask) in MODIFIER_MAP {
|
||||
if GetKeyState(vk) & mask != 0 {
|
||||
modifiers |= modifier;
|
||||
}
|
||||
}
|
||||
if self.has_altgr && GetKeyState(VK_RMENU) & 0x80 != 0 {
|
||||
modifiers |= Modifiers::ALT_GRAPH;
|
||||
modifiers &= !(Modifiers::CONTROL | Modifiers::ALT);
|
||||
}
|
||||
modifiers
|
||||
}
|
||||
}
|
||||
|
||||
/// The same as [Self::get_modifiers()], but it reads the Ctrl and Shift state from a mouse
|
||||
/// event's wParam parameter. Saves two calls to [GetKeyState()].
|
||||
pub(crate) fn get_modifiers_from_mouse_wparam(&self, wparam: WPARAM) -> Modifiers {
|
||||
unsafe {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
for &(vk, modifier, mask) in MODIFIER_MAP {
|
||||
let modifier_active = match modifier {
|
||||
Modifiers::CONTROL => wparam & MK_CONTROL != 0,
|
||||
Modifiers::SHIFT => wparam & MK_SHIFT != 0,
|
||||
_ => GetKeyState(vk) & mask != 0,
|
||||
};
|
||||
|
||||
if modifier_active {
|
||||
modifiers |= modifier;
|
||||
}
|
||||
}
|
||||
if self.has_altgr && GetKeyState(VK_RMENU) & 0x80 != 0 {
|
||||
modifiers |= Modifiers::ALT_GRAPH;
|
||||
modifiers &= !(Modifiers::CONTROL | Modifiers::ALT);
|
||||
}
|
||||
modifiers
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a keyboard layout.
|
||||
///
|
||||
/// We need to retain a map of virtual key codes in various modifier
|
||||
/// states, because it's not practical to query that at keyboard event
|
||||
/// time (the main culprit is that `ToUnicodeEx` is stateful).
|
||||
///
|
||||
/// The logic is based on Mozilla KeyboardLayout::LoadLayout but is
|
||||
/// considerably simplified.
|
||||
fn load_keyboard_layout(&mut self) {
|
||||
unsafe {
|
||||
self.key_vals.clear();
|
||||
self.dead_keys.clear();
|
||||
self.has_altgr = false;
|
||||
let mut key_state = [0u8; 256];
|
||||
let mut uni_chars = [0u16; 5];
|
||||
// Right now, we're only getting the values for base and shifted
|
||||
// variants. Mozilla goes through 16 mod states.
|
||||
for shift_state in 0..N_SHIFT_STATE {
|
||||
let has_shift = shift_state & SHIFT_STATE_SHIFT != 0;
|
||||
let has_altgr = shift_state & SHIFT_STATE_ALTGR != 0;
|
||||
key_state[VK_SHIFT as usize] = if has_shift { 0x80 } else { 0 };
|
||||
key_state[VK_CONTROL as usize] = if has_altgr { 0x80 } else { 0 };
|
||||
key_state[VK_LCONTROL as usize] = if has_altgr { 0x80 } else { 0 };
|
||||
key_state[VK_MENU as usize] = if has_altgr { 0x80 } else { 0 };
|
||||
key_state[VK_RMENU as usize] = if has_altgr { 0x80 } else { 0 };
|
||||
#[allow(clippy::iter_overeager_cloned)]
|
||||
for vk in PRINTABLE_VKS.iter().cloned().flatten() {
|
||||
let ret = ToUnicodeEx(
|
||||
vk as UINT,
|
||||
0,
|
||||
key_state.as_ptr(),
|
||||
uni_chars.as_mut_ptr(),
|
||||
uni_chars.len() as _,
|
||||
0,
|
||||
self.hkl,
|
||||
);
|
||||
match ret.cmp(&0) {
|
||||
Ordering::Greater => {
|
||||
let utf16_slice = &uni_chars[..ret as usize];
|
||||
if let Ok(strval) = String::from_utf16(utf16_slice) {
|
||||
self.key_vals.insert((vk, shift_state), strval);
|
||||
}
|
||||
// If the AltGr version of the key has a different string than
|
||||
// the base, then the layout has AltGr. Note that Mozilla also
|
||||
// checks dead keys for change.
|
||||
if has_altgr
|
||||
&& !self.has_altgr
|
||||
&& self.key_vals.get(&(vk, shift_state))
|
||||
!= self.key_vals.get(&(vk, shift_state & !SHIFT_STATE_ALTGR))
|
||||
{
|
||||
self.has_altgr = true;
|
||||
}
|
||||
}
|
||||
Ordering::Less => {
|
||||
// It's a dead key, press it again to reset the state.
|
||||
self.dead_keys.insert((vk, shift_state));
|
||||
let _ = ToUnicodeEx(
|
||||
vk as UINT,
|
||||
0,
|
||||
key_state.as_ptr(),
|
||||
uni_chars.as_mut_ptr(),
|
||||
uni_chars.len() as _,
|
||||
0,
|
||||
self.hkl,
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_base_key(&self, vk: VkCode, modifiers: Modifiers) -> Key {
|
||||
let mut shift_state = 0;
|
||||
if modifiers.contains(Modifiers::SHIFT) {
|
||||
shift_state |= SHIFT_STATE_SHIFT;
|
||||
}
|
||||
if modifiers.contains(Modifiers::ALT_GRAPH) {
|
||||
shift_state |= SHIFT_STATE_ALTGR;
|
||||
}
|
||||
if let Some(s) = self.key_vals.get(&(vk, shift_state)) {
|
||||
Key::Character(s.clone())
|
||||
} else {
|
||||
let mapped = self.map_vk(vk);
|
||||
if mapped >= (1 << 31) {
|
||||
Key::Dead
|
||||
} else {
|
||||
code_unit_to_key(mapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a virtual key code to a code unit, also indicate if dead key.
|
||||
///
|
||||
/// Bit 31 is set if the mapping is to a dead key. The bottom bits contain the code unit.
|
||||
fn map_vk(&self, vk: VkCode) -> u32 {
|
||||
unsafe { MapVirtualKeyExW(vk as _, MAPVK_VK_TO_CHAR, self.hkl) }
|
||||
}
|
||||
|
||||
/// Refine a virtual key code to distinguish left and right.
|
||||
///
|
||||
/// This only does the mapping if the original code is ambiguous, as otherwise the
|
||||
/// virtual key code reported in `wparam` is more reliable.
|
||||
fn refine_vk(&self, vk: VkCode, mut scan_code: u32) -> VkCode {
|
||||
match vk as INT {
|
||||
0 | VK_SHIFT | VK_CONTROL | VK_MENU => {
|
||||
if scan_code >= 0x100 {
|
||||
scan_code += 0xE000 - 0x100;
|
||||
}
|
||||
unsafe { MapVirtualKeyExW(scan_code, MAPVK_VSC_TO_VK_EX, self.hkl) as u8 }
|
||||
}
|
||||
_ => vk,
|
||||
}
|
||||
}
|
||||
}
|
||||
7
plugins/baseview/src/win/mod.rs
Normal file
7
plugins/baseview/src/win/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod cursor;
|
||||
mod drop_target;
|
||||
mod hook;
|
||||
mod keyboard;
|
||||
mod window;
|
||||
|
||||
pub use window::*;
|
||||
867
plugins/baseview/src/win/window.rs
Normal file
867
plugins/baseview/src/win/window.rs
Normal file
@@ -0,0 +1,867 @@
|
||||
use winapi::shared::guiddef::GUID;
|
||||
use winapi::shared::minwindef::{ATOM, FALSE, LOWORD, LPARAM, LRESULT, UINT, WPARAM};
|
||||
use winapi::shared::windef::{HWND, POINT, RECT};
|
||||
use winapi::um::combaseapi::CoCreateGuid;
|
||||
use winapi::um::ole2::{OleInitialize, RegisterDragDrop, RevokeDragDrop};
|
||||
use winapi::um::oleidl::LPDROPTARGET;
|
||||
use winapi::um::winuser::{
|
||||
AdjustWindowRectEx, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW,
|
||||
GetCursorPos, GetDpiForWindow, GetFocus, GetMessageW, GetWindowLongPtrW, LoadCursorW,
|
||||
PostMessageW, RegisterClassW, ReleaseCapture, SetCapture, SetCursor, SetFocus,
|
||||
SetProcessDpiAwarenessContext, SetTimer, SetWindowLongPtrW, SetWindowPos, TrackMouseEvent,
|
||||
TranslateMessage, UnregisterClassW,
|
||||
CS_OWNDC, GET_XBUTTON_WPARAM, GWLP_USERDATA, HTCLIENT, IDC_ARROW, MSG, SWP_NOMOVE,
|
||||
SWP_NOZORDER, TRACKMOUSEEVENT, WHEEL_DELTA, WM_CHAR, WM_CLOSE, WM_CREATE, WM_DPICHANGED,
|
||||
WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN,
|
||||
WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSELEAVE, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCDESTROY,
|
||||
WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SHOWWINDOW, WM_SIZE, WM_SYSCHAR, WM_SYSKEYDOWN,
|
||||
WM_SYSKEYUP, WM_TIMER, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW, WS_CAPTION, WS_CHILD,
|
||||
WS_CLIPSIBLINGS, WS_MAXIMIZEBOX, WS_MINIMIZEBOX, WS_POPUPWINDOW, WS_SIZEBOX, WS_VISIBLE,
|
||||
XBUTTON1, XBUTTON2,
|
||||
};
|
||||
|
||||
use std::cell::{Cell, Ref, RefCell, RefMut};
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::{c_void, OsStr};
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::ptr::null_mut;
|
||||
use std::rc::Rc;
|
||||
|
||||
use raw_window_handle::{
|
||||
HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, Win32WindowHandle,
|
||||
WindowsDisplayHandle,
|
||||
};
|
||||
|
||||
const BV_WINDOW_MUST_CLOSE: UINT = WM_USER + 1;
|
||||
|
||||
use crate::win::hook::{self, KeyboardHookHandle};
|
||||
use crate::{
|
||||
Event, MouseButton, MouseCursor, MouseEvent, PhyPoint, PhySize, Point, ScrollDelta, Size,
|
||||
WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy,
|
||||
};
|
||||
|
||||
use super::cursor::cursor_to_lpcwstr;
|
||||
use super::drop_target::DropTarget;
|
||||
use super::keyboard::KeyboardState;
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
use crate::gl::GlContext;
|
||||
|
||||
unsafe fn generate_guid() -> String {
|
||||
let mut guid: GUID = std::mem::zeroed();
|
||||
CoCreateGuid(&mut guid);
|
||||
format!(
|
||||
"{:0X}-{:0X}-{:0X}-{:0X}{:0X}-{:0X}{:0X}{:0X}{:0X}{:0X}{:0X}\0",
|
||||
guid.Data1,
|
||||
guid.Data2,
|
||||
guid.Data3,
|
||||
guid.Data4[0],
|
||||
guid.Data4[1],
|
||||
guid.Data4[2],
|
||||
guid.Data4[3],
|
||||
guid.Data4[4],
|
||||
guid.Data4[5],
|
||||
guid.Data4[6],
|
||||
guid.Data4[7]
|
||||
)
|
||||
}
|
||||
|
||||
const WIN_FRAME_TIMER: usize = 4242;
|
||||
|
||||
pub struct WindowHandle {
|
||||
hwnd: Option<HWND>,
|
||||
is_open: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl WindowHandle {
|
||||
pub fn close(&mut self) {
|
||||
if let Some(hwnd) = self.hwnd.take() {
|
||||
unsafe {
|
||||
PostMessageW(hwnd, BV_WINDOW_MUST_CLOSE, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.is_open.get()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl HasRawWindowHandle for WindowHandle {
|
||||
fn raw_window_handle(&self) -> RawWindowHandle {
|
||||
if let Some(hwnd) = self.hwnd {
|
||||
let mut handle = Win32WindowHandle::empty();
|
||||
handle.hwnd = hwnd as *mut c_void;
|
||||
|
||||
RawWindowHandle::Win32(handle)
|
||||
} else {
|
||||
RawWindowHandle::Win32(Win32WindowHandle::empty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ParentHandle {
|
||||
is_open: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl ParentHandle {
|
||||
pub fn new(hwnd: HWND) -> (Self, WindowHandle) {
|
||||
let is_open = Rc::new(Cell::new(true));
|
||||
|
||||
let handle = WindowHandle { hwnd: Some(hwnd), is_open: Rc::clone(&is_open) };
|
||||
|
||||
(Self { is_open }, handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ParentHandle {
|
||||
fn drop(&mut self) {
|
||||
self.is_open.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe extern "system" fn wnd_proc(
|
||||
hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
if msg == WM_CREATE {
|
||||
PostMessageW(hwnd, WM_SHOWWINDOW, 0, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
let window_state_ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut WindowState;
|
||||
if !window_state_ptr.is_null() {
|
||||
let result = wnd_proc_inner(hwnd, msg, wparam, lparam, &*window_state_ptr);
|
||||
|
||||
// If any of the above event handlers caused tasks to be pushed to the deferred tasks list,
|
||||
// then we'll try to handle them now
|
||||
loop {
|
||||
// NOTE: This is written like this instead of using a `while let` loop to avoid exending
|
||||
// the borrow of `window_state.deferred_tasks` into the call of
|
||||
// `window_state.handle_deferred_task()` since that may also generate additional
|
||||
// messages.
|
||||
let task = match (*window_state_ptr).deferred_tasks.borrow_mut().pop_front() {
|
||||
Some(task) => task,
|
||||
None => break,
|
||||
};
|
||||
|
||||
(*window_state_ptr).handle_deferred_task(task);
|
||||
}
|
||||
|
||||
// NOTE: This is not handled in `wnd_proc_inner` because of the deferred task loop above
|
||||
if msg == WM_NCDESTROY {
|
||||
RevokeDragDrop(hwnd);
|
||||
unregister_wnd_class((*window_state_ptr).window_class);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
|
||||
drop(Rc::from_raw(window_state_ptr));
|
||||
}
|
||||
|
||||
// The actual custom window proc has been moved to another function so we can always handle
|
||||
// the deferred tasks regardless of whether the custom window proc returns early or not
|
||||
if let Some(result) = result {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
DefWindowProcW(hwnd, msg, wparam, lparam)
|
||||
}
|
||||
|
||||
/// Our custom `wnd_proc` handler. If the result contains a value, then this is returned after
|
||||
/// handling any deferred tasks. otherwise the default window procedure is invoked.
|
||||
unsafe fn wnd_proc_inner(
|
||||
hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM, window_state: &WindowState,
|
||||
) -> Option<LRESULT> {
|
||||
match msg {
|
||||
WM_MOUSEMOVE => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
let mut mouse_was_outside_window = window_state.mouse_was_outside_window.borrow_mut();
|
||||
if *mouse_was_outside_window {
|
||||
// this makes Windows track whether the mouse leaves the window.
|
||||
// When the mouse leaves it results in a `WM_MOUSELEAVE` event.
|
||||
let mut track_mouse = TRACKMOUSEEVENT {
|
||||
cbSize: std::mem::size_of::<TRACKMOUSEEVENT>() as u32,
|
||||
dwFlags: winapi::um::winuser::TME_LEAVE,
|
||||
hwndTrack: hwnd,
|
||||
dwHoverTime: winapi::um::winuser::HOVER_DEFAULT,
|
||||
};
|
||||
// Couldn't find a good way to track whether the mouse enters,
|
||||
// but if `WM_MOUSEMOVE` happens, the mouse must have entered.
|
||||
TrackMouseEvent(&mut track_mouse);
|
||||
*mouse_was_outside_window = false;
|
||||
|
||||
let enter_event = Event::Mouse(MouseEvent::CursorEntered);
|
||||
window_state
|
||||
.handler
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.on_event(&mut window, enter_event);
|
||||
}
|
||||
|
||||
let x = (lparam & 0xFFFF) as i16 as i32;
|
||||
let y = ((lparam >> 16) & 0xFFFF) as i16 as i32;
|
||||
|
||||
let physical_pos = PhyPoint { x, y };
|
||||
let logical_pos = physical_pos.to_logical(&window_state.window_info.borrow());
|
||||
|
||||
let mut screen_pt = POINT { x: 0, y: 0 };
|
||||
GetCursorPos(&mut screen_pt);
|
||||
let screen_position = Point::new(screen_pt.x as f64, screen_pt.y as f64);
|
||||
|
||||
let move_event = Event::Mouse(MouseEvent::CursorMoved {
|
||||
position: logical_pos,
|
||||
screen_position,
|
||||
modifiers: window_state
|
||||
.keyboard_state
|
||||
.borrow()
|
||||
.get_modifiers_from_mouse_wparam(wparam),
|
||||
});
|
||||
window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, move_event);
|
||||
Some(0)
|
||||
}
|
||||
|
||||
WM_MOUSELEAVE => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
let event = Event::Mouse(MouseEvent::CursorLeft);
|
||||
window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, event);
|
||||
|
||||
*window_state.mouse_was_outside_window.borrow_mut() = true;
|
||||
Some(0)
|
||||
}
|
||||
WM_MOUSEWHEEL | WM_MOUSEHWHEEL => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
let value = (wparam >> 16) as i16;
|
||||
let value = value as i32;
|
||||
let value = value as f32 / WHEEL_DELTA as f32;
|
||||
|
||||
let event = Event::Mouse(MouseEvent::WheelScrolled {
|
||||
delta: if msg == WM_MOUSEWHEEL {
|
||||
ScrollDelta::Lines { x: 0.0, y: value }
|
||||
} else {
|
||||
ScrollDelta::Lines { x: value, y: 0.0 }
|
||||
},
|
||||
modifiers: window_state
|
||||
.keyboard_state
|
||||
.borrow()
|
||||
.get_modifiers_from_mouse_wparam(wparam),
|
||||
});
|
||||
|
||||
window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, event);
|
||||
|
||||
Some(0)
|
||||
}
|
||||
WM_LBUTTONDOWN | WM_LBUTTONUP | WM_MBUTTONDOWN | WM_MBUTTONUP | WM_RBUTTONDOWN
|
||||
| WM_RBUTTONUP | WM_XBUTTONDOWN | WM_XBUTTONUP => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
let mut mouse_button_counter = window_state.mouse_button_counter.get();
|
||||
|
||||
let button = match msg {
|
||||
WM_LBUTTONDOWN | WM_LBUTTONUP => Some(MouseButton::Left),
|
||||
WM_MBUTTONDOWN | WM_MBUTTONUP => Some(MouseButton::Middle),
|
||||
WM_RBUTTONDOWN | WM_RBUTTONUP => Some(MouseButton::Right),
|
||||
WM_XBUTTONDOWN | WM_XBUTTONUP => match GET_XBUTTON_WPARAM(wparam) {
|
||||
XBUTTON1 => Some(MouseButton::Back),
|
||||
XBUTTON2 => Some(MouseButton::Forward),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(button) = button {
|
||||
let event = match msg {
|
||||
WM_LBUTTONDOWN | WM_MBUTTONDOWN | WM_RBUTTONDOWN | WM_XBUTTONDOWN => {
|
||||
// Capture the mouse cursor on button down
|
||||
mouse_button_counter = mouse_button_counter.saturating_add(1);
|
||||
SetCapture(hwnd);
|
||||
MouseEvent::ButtonPressed {
|
||||
button,
|
||||
modifiers: window_state
|
||||
.keyboard_state
|
||||
.borrow()
|
||||
.get_modifiers_from_mouse_wparam(wparam),
|
||||
}
|
||||
}
|
||||
WM_LBUTTONUP | WM_MBUTTONUP | WM_RBUTTONUP | WM_XBUTTONUP => {
|
||||
// Release the mouse cursor capture when all buttons are released
|
||||
mouse_button_counter = mouse_button_counter.saturating_sub(1);
|
||||
if mouse_button_counter == 0 {
|
||||
ReleaseCapture();
|
||||
}
|
||||
|
||||
MouseEvent::ButtonReleased {
|
||||
button,
|
||||
modifiers: window_state
|
||||
.keyboard_state
|
||||
.borrow()
|
||||
.get_modifiers_from_mouse_wparam(wparam),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
unreachable!()
|
||||
}
|
||||
};
|
||||
|
||||
window_state.mouse_button_counter.set(mouse_button_counter);
|
||||
|
||||
window_state
|
||||
.handler
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.on_event(&mut window, Event::Mouse(event));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
WM_TIMER => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
if wparam == WIN_FRAME_TIMER {
|
||||
window_state.handler.borrow_mut().as_mut().unwrap().on_frame(&mut window);
|
||||
}
|
||||
|
||||
Some(0)
|
||||
}
|
||||
WM_CLOSE => {
|
||||
// Make sure to release the borrow before the DefWindowProc call
|
||||
{
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
window_state
|
||||
.handler
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.on_event(&mut window, Event::Window(WindowEvent::WillClose));
|
||||
}
|
||||
|
||||
// DestroyWindow(hwnd);
|
||||
// Some(0)
|
||||
Some(DefWindowProcW(hwnd, msg, wparam, lparam))
|
||||
}
|
||||
WM_CHAR | WM_SYSCHAR | WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP
|
||||
| WM_INPUTLANGCHANGE => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
let opt_event =
|
||||
window_state.keyboard_state.borrow_mut().process_message(hwnd, msg, wparam, lparam);
|
||||
|
||||
if let Some(event) = opt_event {
|
||||
window_state
|
||||
.handler
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.on_event(&mut window, Event::Keyboard(event));
|
||||
}
|
||||
|
||||
if msg != WM_SYSKEYDOWN {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
WM_SIZE => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
let width = (lparam & 0xFFFF) as u16 as u32;
|
||||
let height = ((lparam >> 16) & 0xFFFF) as u16 as u32;
|
||||
|
||||
let new_window_info = {
|
||||
let mut window_info = window_state.window_info.borrow_mut();
|
||||
let new_window_info =
|
||||
WindowInfo::from_physical_size(PhySize { width, height }, window_info.scale());
|
||||
|
||||
// Only send the event if anything changed
|
||||
if window_info.physical_size() == new_window_info.physical_size() {
|
||||
return None;
|
||||
}
|
||||
|
||||
*window_info = new_window_info;
|
||||
|
||||
new_window_info
|
||||
};
|
||||
|
||||
window_state
|
||||
.handler
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.on_event(&mut window, Event::Window(WindowEvent::Resized(new_window_info)));
|
||||
|
||||
None
|
||||
}
|
||||
WM_DPICHANGED => {
|
||||
// To avoid weirdness with the realtime borrow checker.
|
||||
let new_rect = {
|
||||
if let WindowScalePolicy::SystemScaleFactor = window_state.scale_policy {
|
||||
let dpi = (wparam & 0xFFFF) as u16 as u32;
|
||||
let scale_factor = dpi as f64 / 96.0;
|
||||
|
||||
let mut window_info = window_state.window_info.borrow_mut();
|
||||
*window_info =
|
||||
WindowInfo::from_logical_size(window_info.logical_size(), scale_factor);
|
||||
|
||||
Some((
|
||||
RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
// todo: check if usize fits into i32
|
||||
right: window_info.physical_size().width as i32,
|
||||
bottom: window_info.physical_size().height as i32,
|
||||
},
|
||||
window_state.dw_style,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some((mut new_rect, dw_style)) = new_rect {
|
||||
// Convert this desired "client rectangle" size to the actual "window rectangle"
|
||||
// size (Because of course you have to do that).
|
||||
AdjustWindowRectEx(&mut new_rect, dw_style, 0, 0);
|
||||
|
||||
// Windows makes us resize the window manually. This will trigger another `WM_SIZE` event,
|
||||
// which we can then send the user the new scale factor.
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
hwnd,
|
||||
new_rect.left,
|
||||
new_rect.top,
|
||||
new_rect.right - new_rect.left,
|
||||
new_rect.bottom - new_rect.top,
|
||||
SWP_NOZORDER | SWP_NOMOVE,
|
||||
);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
// If WM_SETCURSOR returns `None`, WM_SETCURSOR continues to get handled by the outer window(s),
|
||||
// If it returns `Some(1)`, the current window decides what the cursor is
|
||||
WM_SETCURSOR => {
|
||||
let low_word = LOWORD(lparam as u32) as isize;
|
||||
let mouse_in_window = low_word == HTCLIENT;
|
||||
if mouse_in_window {
|
||||
// Here we need to set the cursor back to what the state says, since it can have changed when outside the window
|
||||
let cursor =
|
||||
LoadCursorW(null_mut(), cursor_to_lpcwstr(window_state.cursor_icon.get()));
|
||||
unsafe {
|
||||
SetCursor(cursor);
|
||||
}
|
||||
Some(1)
|
||||
} else {
|
||||
// Cursor is being changed by some other window, e.g. when having mouse on the borders to resize it
|
||||
None
|
||||
}
|
||||
}
|
||||
// NOTE: `WM_NCDESTROY` is handled in the outer function because this deallocates the window
|
||||
// state
|
||||
BV_WINDOW_MUST_CLOSE => {
|
||||
DestroyWindow(hwnd);
|
||||
Some(0)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn register_wnd_class() -> ATOM {
|
||||
// We generate a unique name for the new window class to prevent name collisions
|
||||
let class_name_str = format!("Baseview-{}", generate_guid());
|
||||
let mut class_name: Vec<u16> = OsStr::new(&class_name_str).encode_wide().collect();
|
||||
class_name.push(0);
|
||||
|
||||
let wnd_class = WNDCLASSW {
|
||||
style: CS_OWNDC,
|
||||
lpfnWndProc: Some(wnd_proc),
|
||||
hInstance: null_mut(),
|
||||
lpszClassName: class_name.as_ptr(),
|
||||
cbClsExtra: 0,
|
||||
cbWndExtra: 0,
|
||||
hIcon: null_mut(),
|
||||
hCursor: LoadCursorW(null_mut(), IDC_ARROW),
|
||||
hbrBackground: null_mut(),
|
||||
lpszMenuName: null_mut(),
|
||||
};
|
||||
|
||||
RegisterClassW(&wnd_class)
|
||||
}
|
||||
|
||||
unsafe fn unregister_wnd_class(wnd_class: ATOM) {
|
||||
UnregisterClassW(wnd_class as _, null_mut());
|
||||
}
|
||||
|
||||
/// All data associated with the window. This uses internal mutability so the outer struct doesn't
|
||||
/// need to be mutably borrowed. Mutably borrowing the entire `WindowState` can be problematic
|
||||
/// because of the Windows message loops' reentrant nature. Care still needs to be taken to prevent
|
||||
/// `handler` from indirectly triggering other events that would also need to be handled using
|
||||
/// `handler`.
|
||||
pub(super) struct WindowState {
|
||||
/// The HWND belonging to this window. The window's actual state is stored in the `WindowState`
|
||||
/// struct associated with this HWND through `unsafe { GetWindowLongPtrW(self.hwnd,
|
||||
/// GWLP_USERDATA) } as *const WindowState`.
|
||||
pub hwnd: HWND,
|
||||
window_class: ATOM,
|
||||
window_info: RefCell<WindowInfo>,
|
||||
_parent_handle: Option<ParentHandle>,
|
||||
keyboard_state: RefCell<KeyboardState>,
|
||||
mouse_button_counter: Cell<usize>,
|
||||
mouse_was_outside_window: RefCell<bool>,
|
||||
cursor_icon: Cell<MouseCursor>,
|
||||
// Initialized late so the `Window` can hold a reference to this `WindowState`
|
||||
handler: RefCell<Option<Box<dyn WindowHandler>>>,
|
||||
_drop_target: RefCell<Option<Rc<DropTarget>>>,
|
||||
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
|
||||
/// borrowed in `wnd_proc`. So the `resize()` function below cannot also mutably borrow that
|
||||
/// window state at the same time.
|
||||
pub deferred_tasks: RefCell<VecDeque<WindowTask>>,
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
pub gl_context: Option<GlContext>,
|
||||
}
|
||||
|
||||
impl WindowState {
|
||||
pub(super) fn create_window(&self) -> Window<'_> {
|
||||
Window { state: self }
|
||||
}
|
||||
|
||||
pub(super) fn window_info(&self) -> Ref<'_, WindowInfo> {
|
||||
self.window_info.borrow()
|
||||
}
|
||||
|
||||
pub(super) fn keyboard_state(&self) -> Ref<'_, KeyboardState> {
|
||||
self.keyboard_state.borrow()
|
||||
}
|
||||
|
||||
pub(super) fn handler_mut(&self) -> RefMut<'_, Option<Box<dyn WindowHandler>>> {
|
||||
self.handler.borrow_mut()
|
||||
}
|
||||
|
||||
/// Handle a deferred task as described in [`Self::deferred_tasks`].
|
||||
pub(self) fn handle_deferred_task(&self, task: WindowTask) {
|
||||
match task {
|
||||
WindowTask::Resize(size) => {
|
||||
// `self.window_info` will be modified in response to the `WM_SIZE` event that
|
||||
// follows the `SetWindowPos()` call
|
||||
let scaling = self.window_info.borrow().scale();
|
||||
let window_info = WindowInfo::from_logical_size(size, scaling);
|
||||
|
||||
// If the window is a standalone window then the size needs to include the window
|
||||
// decorations
|
||||
let mut rect = RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: window_info.physical_size().width as i32,
|
||||
bottom: window_info.physical_size().height as i32,
|
||||
};
|
||||
unsafe {
|
||||
AdjustWindowRectEx(&mut rect, self.dw_style, 0, 0);
|
||||
SetWindowPos(
|
||||
self.hwnd,
|
||||
self.hwnd,
|
||||
0,
|
||||
0,
|
||||
rect.right - rect.left,
|
||||
rect.bottom - rect.top,
|
||||
SWP_NOZORDER | SWP_NOMOVE,
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tasks that must be deferred until the end of [`wnd_proc()`] to avoid reentrant `WindowState`
|
||||
/// borrows. See the docstring on [`WindowState::deferred_tasks`] for more information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) enum WindowTask {
|
||||
/// Resize the window to the given size. The size is in logical pixels. DPI scaling is applied
|
||||
/// automatically.
|
||||
Resize(Size),
|
||||
}
|
||||
|
||||
pub struct Window<'a> {
|
||||
state: &'a WindowState,
|
||||
}
|
||||
|
||||
impl Window<'_> {
|
||||
pub fn open_parented<P, H, B>(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle
|
||||
where
|
||||
P: HasRawWindowHandle,
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut crate::Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
let parent = match parent.raw_window_handle() {
|
||||
RawWindowHandle::Win32(h) => h.hwnd as HWND,
|
||||
h => panic!("unsupported parent handle {:?}", h),
|
||||
};
|
||||
|
||||
let (window_handle, _) = Self::open(true, parent, options, build);
|
||||
|
||||
window_handle
|
||||
}
|
||||
|
||||
pub fn open_blocking<H, B>(options: WindowOpenOptions, build: B)
|
||||
where
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut crate::Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
let (_, hwnd) = Self::open(false, null_mut(), options, build);
|
||||
|
||||
unsafe {
|
||||
let mut msg: MSG = std::mem::zeroed();
|
||||
|
||||
loop {
|
||||
let status = GetMessageW(&mut msg, hwnd, 0, 0);
|
||||
|
||||
if status == -1 {
|
||||
break;
|
||||
}
|
||||
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open<H, B>(
|
||||
parented: bool, parent: HWND, options: WindowOpenOptions, build: B,
|
||||
) -> (WindowHandle, HWND)
|
||||
where
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut crate::Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
unsafe {
|
||||
let mut title: Vec<u16> = OsStr::new(&options.title[..]).encode_wide().collect();
|
||||
title.push(0);
|
||||
|
||||
let window_class = register_wnd_class();
|
||||
// todo: manage error ^
|
||||
|
||||
let scaling = match options.scale {
|
||||
WindowScalePolicy::SystemScaleFactor => 1.0,
|
||||
WindowScalePolicy::ScaleFactor(scale) => scale,
|
||||
};
|
||||
|
||||
let window_info = WindowInfo::from_logical_size(options.size, scaling);
|
||||
|
||||
let mut rect = RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
// todo: check if usize fits into i32
|
||||
right: window_info.physical_size().width as i32,
|
||||
bottom: window_info.physical_size().height as i32,
|
||||
};
|
||||
|
||||
let flags = if parented {
|
||||
WS_CHILD | WS_VISIBLE
|
||||
} else {
|
||||
WS_POPUPWINDOW
|
||||
| WS_CAPTION
|
||||
| WS_VISIBLE
|
||||
| WS_SIZEBOX
|
||||
| WS_MINIMIZEBOX
|
||||
| WS_MAXIMIZEBOX
|
||||
| WS_CLIPSIBLINGS
|
||||
};
|
||||
|
||||
if !parented {
|
||||
AdjustWindowRectEx(&mut rect, flags, FALSE, 0);
|
||||
}
|
||||
|
||||
let hwnd = CreateWindowExW(
|
||||
0,
|
||||
window_class as _,
|
||||
title.as_ptr(),
|
||||
flags,
|
||||
0,
|
||||
0,
|
||||
rect.right - rect.left,
|
||||
rect.bottom - rect.top,
|
||||
parent as *mut _,
|
||||
null_mut(),
|
||||
null_mut(),
|
||||
null_mut(),
|
||||
);
|
||||
// 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();
|
||||
handle.hwnd = hwnd as *mut c_void;
|
||||
let handle = RawWindowHandle::Win32(handle);
|
||||
|
||||
GlContext::create(&handle, gl_config).expect("Could not create OpenGL context")
|
||||
});
|
||||
|
||||
let (parent_handle, window_handle) = ParentHandle::new(hwnd);
|
||||
let parent_handle = if parented { Some(parent_handle) } else { None };
|
||||
|
||||
let window_state = Rc::new(WindowState {
|
||||
hwnd,
|
||||
window_class,
|
||||
window_info: RefCell::new(window_info),
|
||||
_parent_handle: parent_handle,
|
||||
keyboard_state: RefCell::new(KeyboardState::new()),
|
||||
mouse_button_counter: Cell::new(0),
|
||||
mouse_was_outside_window: RefCell::new(true),
|
||||
cursor_icon: Cell::new(MouseCursor::Default),
|
||||
// The Window refers to this `WindowState`, so this `handler` needs to be
|
||||
// initialized later
|
||||
handler: RefCell::new(None),
|
||||
_drop_target: RefCell::new(None),
|
||||
scale_policy: options.scale,
|
||||
dw_style: flags,
|
||||
|
||||
deferred_tasks: RefCell::new(VecDeque::with_capacity(4)),
|
||||
|
||||
kb_hook,
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
gl_context,
|
||||
});
|
||||
|
||||
let handler = {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
build(&mut window)
|
||||
};
|
||||
*window_state.handler.borrow_mut() = Some(Box::new(handler));
|
||||
|
||||
// Only works on Windows 10 unfortunately.
|
||||
SetProcessDpiAwarenessContext(
|
||||
winapi::shared::windef::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE,
|
||||
);
|
||||
|
||||
// Now we can get the actual dpi of the window.
|
||||
let new_rect = if let WindowScalePolicy::SystemScaleFactor = options.scale {
|
||||
// Only works on Windows 10 unfortunately.
|
||||
let dpi = GetDpiForWindow(hwnd);
|
||||
let scale_factor = dpi as f64 / 96.0;
|
||||
|
||||
let mut window_info = window_state.window_info.borrow_mut();
|
||||
if window_info.scale() != scale_factor {
|
||||
*window_info =
|
||||
WindowInfo::from_logical_size(window_info.logical_size(), scale_factor);
|
||||
|
||||
Some(RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
// todo: check if usize fits into i32
|
||||
right: window_info.physical_size().width as i32,
|
||||
bottom: window_info.physical_size().height as i32,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let drop_target = Rc::new(DropTarget::new(Rc::downgrade(&window_state)));
|
||||
*window_state._drop_target.borrow_mut() = Some(drop_target.clone());
|
||||
|
||||
OleInitialize(null_mut());
|
||||
RegisterDragDrop(hwnd, Rc::as_ptr(&drop_target) as LPDROPTARGET);
|
||||
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, Rc::into_raw(window_state) as *const _ as _);
|
||||
SetTimer(hwnd, WIN_FRAME_TIMER, 15, None);
|
||||
|
||||
if let Some(mut new_rect) = new_rect {
|
||||
// Convert this desired"client rectangle" size to the actual "window rectangle"
|
||||
// size (Because of course you have to do that).
|
||||
AdjustWindowRectEx(&mut new_rect, flags, 0, 0);
|
||||
|
||||
// Windows makes us resize the window manually. This will trigger another `WM_SIZE` event,
|
||||
// which we can then send the user the new scale factor.
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
hwnd,
|
||||
new_rect.left,
|
||||
new_rect.top,
|
||||
new_rect.right - new_rect.left,
|
||||
new_rect.bottom - new_rect.top,
|
||||
SWP_NOZORDER | SWP_NOMOVE,
|
||||
);
|
||||
}
|
||||
|
||||
(window_handle, hwnd)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(&mut self) {
|
||||
unsafe {
|
||||
PostMessageW(self.state.hwnd, BV_WINDOW_MUST_CLOSE, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_focus(&mut self) -> bool {
|
||||
let focused_window = unsafe { GetFocus() };
|
||||
focused_window == self.state.hwnd
|
||||
}
|
||||
|
||||
pub fn focus(&mut self) {
|
||||
unsafe {
|
||||
SetFocus(self.state.hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: Size) {
|
||||
// To avoid reentrant event handler calls we'll defer the actual resizing until after the
|
||||
// event has been handled
|
||||
let task = WindowTask::Resize(size);
|
||||
self.state.deferred_tasks.borrow_mut().push_back(task);
|
||||
}
|
||||
|
||||
pub fn physical_size(&self) -> PhySize {
|
||||
self.state.window_info().physical_size()
|
||||
}
|
||||
|
||||
pub fn set_mouse_cursor(&mut self, mouse_cursor: MouseCursor) {
|
||||
self.state.cursor_icon.set(mouse_cursor);
|
||||
unsafe {
|
||||
let cursor = LoadCursorW(null_mut(), cursor_to_lpcwstr(mouse_cursor));
|
||||
SetCursor(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
pub fn gl_context(&self) -> Option<&GlContext> {
|
||||
self.state.gl_context.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl HasRawWindowHandle for Window<'_> {
|
||||
fn raw_window_handle(&self) -> RawWindowHandle {
|
||||
let mut handle = Win32WindowHandle::empty();
|
||||
handle.hwnd = self.state.hwnd as *mut c_void;
|
||||
|
||||
RawWindowHandle::Win32(handle)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl HasRawDisplayHandle for Window<'_> {
|
||||
fn raw_display_handle(&self) -> RawDisplayHandle {
|
||||
RawDisplayHandle::Windows(WindowsDisplayHandle::empty())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_to_clipboard(_data: &str) {
|
||||
todo!()
|
||||
}
|
||||
Reference in New Issue
Block a user