471 lines
12 KiB
Go
471 lines
12 KiB
Go
//go:build windows
|
|
|
|
package input
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"runtime"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
var (
|
|
user32 = windows.NewLazySystemDLL("user32.dll")
|
|
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
|
|
|
procSetWindowsHookExW = user32.NewProc("SetWindowsHookExW")
|
|
procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx")
|
|
procCallNextHookEx = user32.NewProc("CallNextHookEx")
|
|
procGetMessageW = user32.NewProc("GetMessageW")
|
|
procSetCursorPos = user32.NewProc("SetCursorPos")
|
|
procShowCursor = user32.NewProc("ShowCursor")
|
|
procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
|
|
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
|
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
|
|
procPostThreadMessageW = user32.NewProc("PostThreadMessageW")
|
|
procGetActiveWindow = user32.NewProc("GetActiveWindow")
|
|
procSetForegroundWindow = user32.NewProc("SetForegroundWindow")
|
|
procGetWindowRect = user32.NewProc("GetWindowRect")
|
|
procClipCursor = user32.NewProc("ClipCursor")
|
|
procSetWindowPos = user32.NewProc("SetWindowPos")
|
|
procMonitorFromWindow = user32.NewProc("MonitorFromWindow")
|
|
procGetMonitorInfoW = user32.NewProc("GetMonitorInfoW")
|
|
procCreateWindowExW = user32.NewProc("CreateWindowExW")
|
|
procRegisterRawInputDevices = user32.NewProc("RegisterRawInputDevices")
|
|
procGetRawInputData = user32.NewProc("GetRawInputData")
|
|
procGetDpiForWindow = user32.NewProc("GetDpiForWindow")
|
|
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
|
|
procIsIconic = user32.NewProc("IsIconic")
|
|
procShowWindowAsync = user32.NewProc("ShowWindowAsync")
|
|
procGetWindowPlacement = user32.NewProc("GetWindowPlacement")
|
|
procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
|
|
procAttachThreadInput = user32.NewProc("AttachThreadInput")
|
|
procBringWindowToTop = user32.NewProc("BringWindowToTop")
|
|
)
|
|
|
|
type RECT struct {
|
|
Left, Top, Right, Bottom int32
|
|
}
|
|
|
|
type WINDOWPLACEMENT struct {
|
|
Length uint32
|
|
Flags uint32
|
|
ShowCmd uint32
|
|
PtMinPosition Point
|
|
PtMaxPosition Point
|
|
RcNormalPosition RECT
|
|
RcDevice RECT
|
|
}
|
|
|
|
type MONITORINFO struct {
|
|
CbSize uint32
|
|
RcMonitor RECT
|
|
RcWork RECT
|
|
DwFlags uint32
|
|
}
|
|
|
|
// Raw Input structs (layout deve bater exatamente com winuser.h)
|
|
type RAWINPUTHEADER struct {
|
|
DwType uint32
|
|
DwSize uint32
|
|
HDevice uintptr
|
|
WParam uintptr
|
|
}
|
|
|
|
type RAWMOUSE struct {
|
|
UsFlags uint16
|
|
_ [2]byte // padding para alinhar union de 4 bytes
|
|
UsButtonFlags uint16
|
|
UsButtonData uint16
|
|
UlRawButtons uint32
|
|
LLastX int32
|
|
LLastY int32
|
|
UlExtraInfo uint32
|
|
}
|
|
|
|
type RAWINPUT struct {
|
|
Header RAWINPUTHEADER
|
|
Mouse RAWMOUSE
|
|
}
|
|
|
|
type RAWINPUTDEVICE struct {
|
|
UsUsagePage uint16
|
|
UsUsage uint16
|
|
DwFlags uint32
|
|
HwndTarget uintptr
|
|
}
|
|
|
|
const (
|
|
WM_INPUT = 0x00FF
|
|
RID_INPUT = 0x10000003
|
|
RIM_TYPEMOUSE = 0
|
|
RI_MOUSE_WHEEL = 0x0400
|
|
RIDEV_INPUTSINK = 0x00000100
|
|
HWND_MESSAGE = ^uintptr(2) // -3
|
|
)
|
|
|
|
const (
|
|
WH_KEYBOARD_LL = 13
|
|
WH_MOUSE_LL = 14
|
|
SM_CXSCREEN = 0
|
|
SM_CYSCREEN = 1
|
|
WM_QUIT = 0x0012
|
|
)
|
|
|
|
func init() {
|
|
procSetProcessDPIAware.Call()
|
|
}
|
|
|
|
type MSLLHOOKSTRUCT struct {
|
|
Pt Point
|
|
MouseData uint32
|
|
Flags uint32
|
|
Time uint32
|
|
DwExtraInfo uintptr
|
|
}
|
|
|
|
type KBDLLHOOKSTRUCT struct {
|
|
VkCode uint32
|
|
ScanCode uint32
|
|
Flags uint32
|
|
Time uint32
|
|
DwExtraInfo uintptr
|
|
}
|
|
|
|
type windowsInputHandler struct {
|
|
mouseHook uintptr
|
|
keyHook uintptr
|
|
tid uint32
|
|
hwnd uintptr
|
|
}
|
|
|
|
func NewInputHandler() InputHandler {
|
|
return &windowsInputHandler{}
|
|
}
|
|
|
|
func (h *windowsInputHandler) Install(ctx context.Context, onMouse func(MouseEvent) bool, onKey func(KeyboardEvent) bool) error {
|
|
// GetForegroundWindow é system-wide (GetActiveWindow é thread-local e pode retornar 0)
|
|
hwnd, _, _ := procGetForegroundWindow.Call()
|
|
h.hwnd = hwnd
|
|
|
|
ready := make(chan error, 1)
|
|
|
|
go func() {
|
|
runtime.LockOSThread()
|
|
h.tid = windows.GetCurrentThreadId()
|
|
hMod, _, _ := procGetModuleHandleW.Call(0)
|
|
|
|
mouseCallback := windows.NewCallback(func(nCode int, wParam uintptr, lParam uintptr) uintptr {
|
|
if nCode >= 0 {
|
|
info := (*MSLLHOOKSTRUCT)(unsafe.Pointer(lParam))
|
|
ev := MouseEvent{
|
|
Message: uint32(wParam),
|
|
Point: info.Pt,
|
|
Data: info.MouseData,
|
|
}
|
|
|
|
// Se a engine tratar o evento (retornar true), bloqueamos o Windows
|
|
if onMouse(ev) {
|
|
return 1
|
|
}
|
|
}
|
|
ret, _, _ := procCallNextHookEx.Call(h.mouseHook, uintptr(nCode), wParam, lParam)
|
|
return ret
|
|
})
|
|
|
|
keyCallback := windows.NewCallback(func(nCode int, wParam uintptr, lParam uintptr) uintptr {
|
|
if nCode >= 0 {
|
|
info := (*KBDLLHOOKSTRUCT)(unsafe.Pointer(lParam))
|
|
ev := KeyboardEvent{
|
|
Message: uint32(wParam),
|
|
VKCode: info.VkCode,
|
|
ScanCode: info.ScanCode,
|
|
Flags: info.Flags,
|
|
}
|
|
if onKey(ev) {
|
|
return 1
|
|
}
|
|
}
|
|
ret, _, _ := procCallNextHookEx.Call(h.keyHook, uintptr(nCode), wParam, lParam)
|
|
return ret
|
|
})
|
|
|
|
mh, _, _ := procSetWindowsHookExW.Call(WH_MOUSE_LL, mouseCallback, hMod, 0)
|
|
if mh == 0 {
|
|
ready <- fmt.Errorf("failed mouse hook")
|
|
return
|
|
}
|
|
h.mouseHook = mh
|
|
|
|
kh, _, _ := procSetWindowsHookExW.Call(WH_KEYBOARD_LL, keyCallback, hMod, 0)
|
|
if kh == 0 {
|
|
ready <- fmt.Errorf("failed key hook")
|
|
return
|
|
}
|
|
h.keyHook = kh
|
|
|
|
ready <- nil
|
|
|
|
var msg struct {
|
|
Hwnd windows.Handle
|
|
Message uint32
|
|
WParam uintptr
|
|
LParam uintptr
|
|
Time uint32
|
|
Pt Point
|
|
}
|
|
for {
|
|
ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
|
|
if ret == 0 || msg.Message == WM_QUIT {
|
|
break
|
|
}
|
|
}
|
|
procUnhookWindowsHookEx.Call(h.mouseHook)
|
|
procUnhookWindowsHookEx.Call(h.keyHook)
|
|
}()
|
|
|
|
return <-ready
|
|
}
|
|
|
|
func (h *windowsInputHandler) Uninstall() {
|
|
if h.tid != 0 {
|
|
procPostThreadMessageW.Call(uintptr(h.tid), WM_QUIT, 0, 0)
|
|
}
|
|
}
|
|
|
|
func (h *windowsInputHandler) SetCursorPos(x, y int32) bool {
|
|
ret, _, _ := procSetCursorPos.Call(uintptr(x), uintptr(y))
|
|
return ret != 0
|
|
}
|
|
|
|
func (h *windowsInputHandler) ShowCursor(show bool) {
|
|
s := -1 // No Windows, ShowCursor(FALSE) decrementa um contador
|
|
if show {
|
|
s = 1
|
|
}
|
|
procShowCursor.Call(uintptr(s))
|
|
}
|
|
|
|
func (h *windowsInputHandler) GetScreenResolution() (int32, int32) {
|
|
w, _, _ := procGetSystemMetrics.Call(SM_CXSCREEN)
|
|
h_res, _, _ := procGetSystemMetrics.Call(SM_CYSCREEN)
|
|
return int32(w), int32(h_res)
|
|
}
|
|
|
|
func (h *windowsInputHandler) RequestFocus() {
|
|
if h.hwnd == 0 {
|
|
return
|
|
}
|
|
// AttachThreadInput trick: único jeito confiável de roubar foco de outra janela no Windows.
|
|
// SetForegroundWindow sozinho falha se o processo chamante não é o foreground.
|
|
fgHwnd, _, _ := procGetForegroundWindow.Call()
|
|
fgTid, _, _ := procGetWindowThreadProcessId.Call(fgHwnd, 0)
|
|
myTid := uintptr(windows.GetCurrentThreadId())
|
|
if fgTid != 0 && fgTid != myTid {
|
|
procAttachThreadInput.Call(myTid, fgTid, 1) // attach
|
|
}
|
|
procBringWindowToTop.Call(h.hwnd)
|
|
procSetForegroundWindow.Call(h.hwnd)
|
|
if fgTid != 0 && fgTid != myTid {
|
|
procAttachThreadInput.Call(myTid, fgTid, 0) // detach
|
|
}
|
|
}
|
|
|
|
func (h *windowsInputHandler) SetCursorClip(clip bool) {
|
|
// Removido temporariamente para testes
|
|
if !clip {
|
|
procClipCursor.Call(0)
|
|
}
|
|
}
|
|
|
|
func (h *windowsInputHandler) GetWindowPos() (int32, int32) {
|
|
if h.hwnd != 0 {
|
|
var rect RECT
|
|
procGetWindowRect.Call(h.hwnd, uintptr(unsafe.Pointer(&rect)))
|
|
return rect.Left, rect.Top
|
|
}
|
|
return 0, 0
|
|
}
|
|
|
|
func (h *windowsInputHandler) GetWindowSize() (int32, int32) {
|
|
if h.hwnd != 0 {
|
|
var rect RECT
|
|
procGetWindowRect.Call(h.hwnd, uintptr(unsafe.Pointer(&rect)))
|
|
return rect.Right - rect.Left, rect.Bottom - rect.Top
|
|
}
|
|
return 0, 0
|
|
}
|
|
|
|
func (h *windowsInputHandler) GetMonitorWorkArea() (x, y, w, ht int32) {
|
|
if h.hwnd == 0 {
|
|
return 0, 0, 1920, 1080
|
|
}
|
|
monitor, _, _ := procMonitorFromWindow.Call(h.hwnd, 2) // MONITOR_DEFAULTTONEAREST
|
|
if monitor == 0 {
|
|
return 0, 0, 1920, 1080
|
|
}
|
|
var mi MONITORINFO
|
|
mi.CbSize = uint32(unsafe.Sizeof(mi))
|
|
procGetMonitorInfoW.Call(monitor, uintptr(unsafe.Pointer(&mi)))
|
|
return mi.RcWork.Left, mi.RcWork.Top,
|
|
mi.RcWork.Right - mi.RcWork.Left,
|
|
mi.RcWork.Bottom - mi.RcWork.Top
|
|
}
|
|
|
|
func (h *windowsInputHandler) MoveWindow(x, y, w, ht int32, topmost bool) {
|
|
if h.hwnd == 0 {
|
|
return
|
|
}
|
|
// HWND_TOPMOST = -1, HWND_NOTOPMOST = -2
|
|
zorder := ^uintptr(1) // HWND_NOTOPMOST
|
|
if topmost {
|
|
zorder = ^uintptr(0) // HWND_TOPMOST
|
|
}
|
|
const SWP_NOACTIVATE = 0x0010
|
|
procSetWindowPos.Call(
|
|
h.hwnd, zorder,
|
|
uintptr(x), uintptr(y), uintptr(w), uintptr(ht),
|
|
SWP_NOACTIVATE,
|
|
)
|
|
}
|
|
|
|
func (h *windowsInputHandler) GetDpiScale() float64 {
|
|
if h.hwnd == 0 {
|
|
return 1.0
|
|
}
|
|
dpi, _, _ := procGetDpiForWindow.Call(h.hwnd)
|
|
if dpi == 0 {
|
|
return 1.0
|
|
}
|
|
return float64(dpi) / 96.0
|
|
}
|
|
|
|
func (h *windowsInputHandler) GetForegroundWindow() uintptr {
|
|
hwnd, _, _ := procGetForegroundWindow.Call()
|
|
return hwnd
|
|
}
|
|
|
|
func (h *windowsInputHandler) SetForegroundTo(hwnd uintptr) {
|
|
if hwnd == 0 {
|
|
return
|
|
}
|
|
fgHwnd, _, _ := procGetForegroundWindow.Call()
|
|
fgTid, _, _ := procGetWindowThreadProcessId.Call(fgHwnd, 0)
|
|
myTid := uintptr(windows.GetCurrentThreadId())
|
|
if fgTid != 0 && fgTid != myTid {
|
|
procAttachThreadInput.Call(myTid, fgTid, 1)
|
|
}
|
|
procBringWindowToTop.Call(hwnd)
|
|
procSetForegroundWindow.Call(hwnd)
|
|
if fgTid != 0 && fgTid != myTid {
|
|
procAttachThreadInput.Call(myTid, fgTid, 0)
|
|
}
|
|
}
|
|
|
|
func (h *windowsInputHandler) GetAppHwnd() uintptr {
|
|
return h.hwnd
|
|
}
|
|
|
|
// GetRestoredRect retorna posição e tamanho do KVMote no estado restaurado,
|
|
// mesmo que a janela esteja minimizada (GetWindowRect falha nesse caso).
|
|
func (h *windowsInputHandler) GetRestoredRect() (x, y, w, ht int32) {
|
|
if h.hwnd == 0 {
|
|
return 0, 0, 800, 600
|
|
}
|
|
var wp WINDOWPLACEMENT
|
|
wp.Length = uint32(unsafe.Sizeof(wp))
|
|
procGetWindowPlacement.Call(h.hwnd, uintptr(unsafe.Pointer(&wp)))
|
|
r := wp.RcNormalPosition
|
|
return r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top
|
|
}
|
|
|
|
func (h *windowsInputHandler) IsMinimized(hwnd uintptr) bool {
|
|
ret, _, _ := procIsIconic.Call(hwnd)
|
|
return ret != 0
|
|
}
|
|
|
|
func (h *windowsInputHandler) RestoreWindow(hwnd uintptr) {
|
|
procShowWindowAsync.Call(hwnd, 9) // SW_RESTORE
|
|
}
|
|
|
|
func (h *windowsInputHandler) MinimizeWindow(hwnd uintptr) {
|
|
procShowWindowAsync.Call(hwnd, 6) // SW_MINIMIZE
|
|
}
|
|
|
|
func (h *windowsInputHandler) RegisterRawScrollSink(callback func(delta int16)) error {
|
|
ready := make(chan error, 1)
|
|
|
|
go func() {
|
|
runtime.LockOSThread()
|
|
|
|
// Janela message-only como alvo do Raw Input (sem UI, sem z-order)
|
|
className, _ := windows.UTF16PtrFromString("STATIC")
|
|
hwndMsg, _, _ := procCreateWindowExW.Call(
|
|
0,
|
|
uintptr(unsafe.Pointer(className)),
|
|
0, 0,
|
|
0, 0, 0, 0,
|
|
HWND_MESSAGE, // janela oculta sem pai na tela
|
|
0, 0, 0,
|
|
)
|
|
if hwndMsg == 0 {
|
|
ready <- fmt.Errorf("CreateWindowExW failed")
|
|
return
|
|
}
|
|
|
|
// Registra mouse raw com RIDEV_INPUTSINK: recebe eventos mesmo sem foco/cursor
|
|
rid := RAWINPUTDEVICE{
|
|
UsUsagePage: 0x01, // HID_USAGE_PAGE_GENERIC
|
|
UsUsage: 0x02, // HID_USAGE_GENERIC_MOUSE
|
|
DwFlags: RIDEV_INPUTSINK,
|
|
HwndTarget: hwndMsg,
|
|
}
|
|
ret, _, _ := procRegisterRawInputDevices.Call(
|
|
uintptr(unsafe.Pointer(&rid)),
|
|
1,
|
|
uintptr(unsafe.Sizeof(rid)),
|
|
)
|
|
if ret == 0 {
|
|
ready <- fmt.Errorf("RegisterRawInputDevices failed")
|
|
return
|
|
}
|
|
|
|
ready <- nil
|
|
|
|
// Loop de mensagens dedicado para WM_INPUT
|
|
var msg struct {
|
|
Hwnd uintptr
|
|
Message uint32
|
|
WParam uintptr
|
|
LParam uintptr
|
|
Time uint32
|
|
Pt Point
|
|
}
|
|
for {
|
|
r, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&msg)), hwndMsg, 0, 0)
|
|
if r == 0 || msg.Message == WM_QUIT {
|
|
break
|
|
}
|
|
if msg.Message == WM_INPUT {
|
|
var raw RAWINPUT
|
|
size := uint32(unsafe.Sizeof(raw))
|
|
procGetRawInputData.Call(
|
|
msg.LParam, // HRAWINPUT
|
|
RID_INPUT,
|
|
uintptr(unsafe.Pointer(&raw)),
|
|
uintptr(unsafe.Pointer(&size)),
|
|
uintptr(unsafe.Sizeof(RAWINPUTHEADER{})),
|
|
)
|
|
if raw.Header.DwType == RIM_TYPEMOUSE &&
|
|
raw.Mouse.UsButtonFlags&RI_MOUSE_WHEEL != 0 {
|
|
callback(int16(raw.Mouse.UsButtonData))
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
return <-ready
|
|
}
|