KVMote.go/internal/input/overlay_windows.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
neonGreen = 0x0014FF39 // #39FF14 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, neonGreen)
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
}