204 lines
5.3 KiB
Go
204 lines
5.3 KiB
Go
//go:build windows
|
|
|
|
package input
|
|
|
|
import (
|
|
"runtime"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
var (
|
|
gdi32 = windows.NewLazySystemDLL("gdi32.dll")
|
|
|
|
procCreateSolidBrush = gdi32.NewProc("CreateSolidBrush")
|
|
procGdiDeleteObject = gdi32.NewProc("DeleteObject")
|
|
procGetStockObject = gdi32.NewProc("GetStockObject")
|
|
procGdiSelectObject = gdi32.NewProc("SelectObject")
|
|
procSetBkMode = gdi32.NewProc("SetBkMode")
|
|
procSetTextColor = gdi32.NewProc("SetTextColor")
|
|
|
|
procBeginPaint = user32.NewProc("BeginPaint")
|
|
procEndPaint = user32.NewProc("EndPaint")
|
|
procGetClientRect = user32.NewProc("GetClientRect")
|
|
procFillRect = user32.NewProc("FillRect")
|
|
procDrawTextW = user32.NewProc("DrawTextW")
|
|
procRegisterClassExW = user32.NewProc("RegisterClassExW")
|
|
procDefWindowProcW = user32.NewProc("DefWindowProcW")
|
|
procUpdateWindow = user32.NewProc("UpdateWindow")
|
|
procShowWindowFn = user32.NewProc("ShowWindow")
|
|
procTranslateMessage = user32.NewProc("TranslateMessage")
|
|
procDispatchMessageW = user32.NewProc("DispatchMessageW")
|
|
)
|
|
|
|
// WNDCLASSEXW layout: 80 bytes on 64-bit
|
|
type WNDCLASSEXW struct {
|
|
CbSize uint32
|
|
Style uint32
|
|
LpfnWndProc uintptr
|
|
CbClsExtra int32
|
|
CbWndExtra int32
|
|
HInstance uintptr
|
|
HIcon uintptr
|
|
HCursor uintptr
|
|
HbrBackground uintptr
|
|
LpszMenuName *uint16
|
|
LpszClassName *uint16
|
|
HIconSm uintptr
|
|
}
|
|
|
|
// PAINTSTRUCT layout: 72 bytes on 64-bit (includes trailing padding)
|
|
type PAINTSTRUCT struct {
|
|
Hdc uintptr
|
|
FErase int32
|
|
RcPaint RECT
|
|
FRestore int32
|
|
FIncUpdate int32
|
|
RgbReserved [32]byte
|
|
}
|
|
|
|
var (
|
|
overlayOnce sync.Once
|
|
overlayCallback uintptr // prevent GC of callback
|
|
)
|
|
|
|
// OverlayWindow é uma janela Win32 nativa independente do Wails.
|
|
// Exibe indicador neon "KVMote" no canto da tela durante modo cliente.
|
|
// Não interfere com WebView2, Raw Input, nem coordenadas do Wails.
|
|
type OverlayWindow struct {
|
|
hwnd uintptr
|
|
ready chan struct{}
|
|
}
|
|
|
|
func NewOverlayWindow() *OverlayWindow {
|
|
ov := &OverlayWindow{ready: make(chan struct{})}
|
|
go ov.run()
|
|
<-ov.ready
|
|
return ov
|
|
}
|
|
|
|
func (ov *OverlayWindow) run() {
|
|
runtime.LockOSThread()
|
|
|
|
className, _ := windows.UTF16PtrFromString("KVMoteOverlay")
|
|
|
|
overlayOnce.Do(func() {
|
|
cb := windows.NewCallback(func(hwnd, msg, wParam, lParam uintptr) uintptr {
|
|
const (
|
|
wmPaint = 0x000F
|
|
wmEraseBkgnd = 0x0014
|
|
dtCenter = 0x00000001
|
|
dtVCenter = 0x00000004
|
|
dtSingleLine = 0x00000020
|
|
transparent = 1
|
|
neonMagenta = 0x00FF00FF // #FF00FF em COLORREF (0x00BBGGRR)
|
|
defaultFont = 17 // DEFAULT_GUI_FONT
|
|
)
|
|
switch msg {
|
|
case wmEraseBkgnd:
|
|
return 1
|
|
case wmPaint:
|
|
var ps PAINTSTRUCT
|
|
hdc, _, _ := procBeginPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
|
|
var rc RECT
|
|
procGetClientRect.Call(hwnd, uintptr(unsafe.Pointer(&rc)))
|
|
|
|
brush, _, _ := procCreateSolidBrush.Call(0) // preto
|
|
procFillRect.Call(hdc, uintptr(unsafe.Pointer(&rc)), brush)
|
|
procGdiDeleteObject.Call(brush)
|
|
|
|
font, _, _ := procGetStockObject.Call(defaultFont)
|
|
old, _, _ := procGdiSelectObject.Call(hdc, font)
|
|
procSetBkMode.Call(hdc, transparent)
|
|
procSetTextColor.Call(hdc, neonMagenta)
|
|
text, _ := windows.UTF16PtrFromString("KVMote")
|
|
procDrawTextW.Call(hdc, uintptr(unsafe.Pointer(text)), ^uintptr(0),
|
|
uintptr(unsafe.Pointer(&rc)), dtCenter|dtVCenter|dtSingleLine)
|
|
procGdiSelectObject.Call(hdc, old)
|
|
|
|
procEndPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
|
|
return 0
|
|
}
|
|
r, _, _ := procDefWindowProcW.Call(hwnd, msg, wParam, lParam)
|
|
return r
|
|
})
|
|
overlayCallback = cb
|
|
|
|
hMod, _, _ := procGetModuleHandleW.Call(0)
|
|
wc := WNDCLASSEXW{
|
|
LpfnWndProc: cb,
|
|
HInstance: hMod,
|
|
LpszClassName: className,
|
|
}
|
|
wc.CbSize = uint32(unsafe.Sizeof(wc))
|
|
procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc)))
|
|
})
|
|
|
|
const (
|
|
wsPopup = 0x80000000
|
|
wsExTopmost = 0x00000008
|
|
wsExNoActivate = 0x08000000
|
|
wsExToolWindow = 0x00000080
|
|
)
|
|
hMod, _, _ := procGetModuleHandleW.Call(0)
|
|
hwnd, _, _ := procCreateWindowExW.Call(
|
|
wsExTopmost|wsExNoActivate|wsExToolWindow,
|
|
uintptr(unsafe.Pointer(className)),
|
|
0, wsPopup,
|
|
0, 0, 200, 60,
|
|
0, 0, hMod, 0,
|
|
)
|
|
ov.hwnd = hwnd
|
|
close(ov.ready)
|
|
|
|
if hwnd == 0 {
|
|
return
|
|
}
|
|
|
|
var msg struct {
|
|
Hwnd uintptr
|
|
Message uint32
|
|
WParam uintptr
|
|
LParam uintptr
|
|
Time uint32
|
|
Pt Point
|
|
}
|
|
for {
|
|
ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&msg)), hwnd, 0, 0)
|
|
if ret == 0 {
|
|
break
|
|
}
|
|
procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
|
|
procDispatchMessageW.Call(uintptr(unsafe.Pointer(&msg)))
|
|
}
|
|
}
|
|
|
|
// Show exibe o overlay no canto superior esquerdo do monitor especificado.
|
|
// mx, my, mw, mh: work area do monitor em pixels físicos.
|
|
func (ov *OverlayWindow) Show(mx, my, mw, mh int32) {
|
|
if ov.hwnd == 0 {
|
|
return
|
|
}
|
|
_ = mw
|
|
_ = mh
|
|
const overlayW, overlayH = int32(150), int32(40)
|
|
const swpNoActivate = 0x0010
|
|
// Canto superior esquerdo do monitor — sem depender de taskbar
|
|
x := mx + 10
|
|
y := my + 10
|
|
procSetWindowPos.Call(ov.hwnd, ^uintptr(0), // HWND_TOPMOST
|
|
uintptr(x), uintptr(y), uintptr(overlayW), uintptr(overlayH), swpNoActivate)
|
|
procShowWindowFn.Call(ov.hwnd, 5) // SW_SHOW
|
|
procUpdateWindow.Call(ov.hwnd)
|
|
}
|
|
|
|
// Hide esconde o overlay.
|
|
func (ov *OverlayWindow) Hide() {
|
|
if ov.hwnd == 0 {
|
|
return
|
|
}
|
|
procShowWindowFn.Call(ov.hwnd, 0) // SW_HIDE
|
|
}
|