//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") procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId") procAttachThreadInput = user32.NewProc("AttachThreadInput") procBringWindowToTop = user32.NewProc("BringWindowToTop") ) type RECT struct { Left, Top, Right, Bottom int32 } 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 } 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 }