KVMote.go/internal/input/input_windows.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
}