Files
2026-08-20 08:57:41 +01:00

301 lines
9.3 KiB
Go

//go:build windows
package platform
import (
"fmt"
"strconv"
"strings"
"sync"
"unsafe"
"github.com/stevec/fancywin/internal/config"
"github.com/stevec/fancywin/internal/layout"
"golang.org/x/sys/windows"
)
const (
wmPaint = 0x000F
wmEraseBackground = 0x0014
wmNcHitTest = 0x0084
wsPopup = 0x80000000
wsExTopmost = 0x00000008
wsExTransparent = 0x00000020
wsExToolWindow = 0x00000080
wsExLayered = 0x00080000
wsExNoActivate = 0x08000000
lwaColorKey = 0x00000001
lwaAlpha = 0x00000002
hwndTopmost = ^uintptr(0)
swpShowWindow = 0x0040
psSolid = 0
nullBrush = 5
bkModeTransparent = 1
dtCenter = 0x00000001
dtVCenter = 0x00000004
dtSingleLine = 0x00000020
dtEndEllipsis = 0x00008000
fontWeightBold = 700
nonAntialiased = 3
overlayColorKey = 0x00010001 // RGB(1,0,1), made fully transparent.
)
var (
gdi32 = windows.NewLazySystemDLL("gdi32.dll")
procRegisterClassEx = user32.NewProc("RegisterClassExW")
procCreateWindowEx = user32.NewProc("CreateWindowExW")
procDestroyWindow = user32.NewProc("DestroyWindow")
procDefWindowProc = user32.NewProc("DefWindowProcW")
procBeginPaint = user32.NewProc("BeginPaint")
procEndPaint = user32.NewProc("EndPaint")
procGetClientRect = user32.NewProc("GetClientRect")
procFillRect = user32.NewProc("FillRect")
procDrawText = user32.NewProc("DrawTextW")
procInvalidateRect = user32.NewProc("InvalidateRect")
procUpdateWindow = user32.NewProc("UpdateWindow")
procSetLayeredWindowAttributes = user32.NewProc("SetLayeredWindowAttributes")
procGetModuleHandle = kernel32.NewProc("GetModuleHandleW")
procCreateSolidBrush = gdi32.NewProc("CreateSolidBrush")
procCreatePen = gdi32.NewProc("CreatePen")
procSelectObject = gdi32.NewProc("SelectObject")
procDeleteObject = gdi32.NewProc("DeleteObject")
procRectangle = gdi32.NewProc("Rectangle")
procSetBkMode = gdi32.NewProc("SetBkMode")
procSetTextColor = gdi32.NewProc("SetTextColor")
procCreateFont = gdi32.NewProc("CreateFontW")
procGetStockObject = gdi32.NewProc("GetStockObject")
overlayClassName, _ = windows.UTF16PtrFromString("FancyWinZoneOverlay")
overlayWindowProc = windows.NewCallback(overlayWndProc)
overlayViews = struct {
sync.RWMutex
items map[windows.Handle]*overlayView
}{items: make(map[windows.Handle]*overlayView)}
)
type windowClassEx struct {
Size uint32
Style uint32
WndProc uintptr
ClsExtra int32
WndExtra int32
Instance windows.Handle
Icon windows.Handle
Cursor windows.Handle
Background windows.Handle
MenuName *uint16
ClassName *uint16
IconSmall windows.Handle
}
type paintStruct struct {
DC windows.Handle
Erase int32
Paint rect
Restore int32
IncUpdate int32
Reserved [32]byte
}
type overlayView struct {
hwnd windows.Handle
work layout.Rect
zones []config.Zone
gap int
style config.Overlay
}
type overlayManager struct {
classReady bool
hwnd windows.Handle
monitor windows.Handle
signature string
}
func (m *overlayManager) show(mon windows.Handle, work layout.Rect, zones []config.Zone, gap int, style config.Overlay) error {
signature := overlaySignature(mon, work, zones, gap, style)
if m.hwnd != 0 && m.signature == signature {
return nil
}
m.hide()
if err := m.ensureClass(); err != nil {
return err
}
instance, _, instanceErr := procGetModuleHandle.Call(0)
if instance == 0 {
return fmt.Errorf("GetModuleHandleW: %v", instanceErr)
}
w, h := work.Right-work.Left, work.Bottom-work.Top
raw, _, createErr := procCreateWindowEx.Call(
wsExTopmost|wsExTransparent|wsExToolWindow|wsExLayered|wsExNoActivate,
uintptr(unsafe.Pointer(overlayClassName)),
uintptr(unsafe.Pointer(overlayClassName)),
wsPopup,
uintptr(work.Left), uintptr(work.Top), uintptr(w), uintptr(h),
0, 0, instance, 0,
)
if raw == 0 {
return fmt.Errorf("CreateWindowExW: %v", createErr)
}
hwnd := windows.Handle(raw)
view := &overlayView{hwnd: hwnd, work: work, zones: append([]config.Zone(nil), zones...), gap: gap, style: style}
overlayViews.Lock()
overlayViews.items[hwnd] = view
overlayViews.Unlock()
alpha := byte((style.Opacity*255 + 50) / 100)
if ok, _, err := procSetLayeredWindowAttributes.Call(raw, overlayColorKey, uintptr(alpha), lwaColorKey|lwaAlpha); ok == 0 {
m.destroy(hwnd)
return fmt.Errorf("SetLayeredWindowAttributes: %v", err)
}
procInvalidateRect.Call(raw, 0, 1)
procUpdateWindow.Call(raw)
if ok, _, err := procSetWindowPos.Call(raw, hwndTopmost, uintptr(work.Left), uintptr(work.Top), uintptr(w), uintptr(h), swpNoActivate|swpNoOwnerZOrder|swpShowWindow); ok == 0 {
m.destroy(hwnd)
return fmt.Errorf("show overlay: %v", err)
}
m.hwnd = hwnd
m.monitor = mon
m.signature = signature
return nil
}
func (m *overlayManager) hide() {
if m.hwnd != 0 {
m.destroy(m.hwnd)
}
m.hwnd = 0
m.monitor = 0
m.signature = ""
}
func (m *overlayManager) destroy(hwnd windows.Handle) {
overlayViews.Lock()
delete(overlayViews.items, hwnd)
overlayViews.Unlock()
procDestroyWindow.Call(uintptr(hwnd))
}
func (m *overlayManager) ensureClass() error {
if m.classReady {
return nil
}
instance, _, instanceErr := procGetModuleHandle.Call(0)
if instance == 0 {
return fmt.Errorf("GetModuleHandleW: %v", instanceErr)
}
wc := windowClassEx{
Size: uint32(unsafe.Sizeof(windowClassEx{})),
WndProc: overlayWindowProc,
Instance: windows.Handle(instance),
ClassName: overlayClassName,
}
if atom, _, err := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc))); atom == 0 {
return fmt.Errorf("RegisterClassExW: %v", err)
}
m.classReady = true
return nil
}
func overlayWndProc(rawHWND, message, wParam, lParam uintptr) uintptr {
hwnd := windows.Handle(rawHWND)
switch message {
case wmNcHitTest:
return ^uintptr(0) // HTTRANSPARENT: always pass pointer input through.
case wmEraseBackground:
return 1
case wmPaint:
overlayViews.RLock()
view := overlayViews.items[hwnd]
overlayViews.RUnlock()
if view != nil {
view.paint()
return 0
}
}
r, _, _ := procDefWindowProc.Call(rawHWND, message, wParam, lParam)
return r
}
func (v *overlayView) paint() {
var ps paintStruct
dc, _, _ := procBeginPaint.Call(uintptr(v.hwnd), uintptr(unsafe.Pointer(&ps)))
if dc == 0 {
return
}
defer procEndPaint.Call(uintptr(v.hwnd), uintptr(unsafe.Pointer(&ps)))
var client rect
procGetClientRect.Call(uintptr(v.hwnd), uintptr(unsafe.Pointer(&client)))
background, _, _ := procCreateSolidBrush.Call(overlayColorKey)
if background != 0 {
procFillRect.Call(dc, uintptr(unsafe.Pointer(&client)), background)
procDeleteObject.Call(background)
}
color := colorRef(v.style.Color)
pen, _, _ := procCreatePen.Call(psSolid, uintptr(v.style.BorderWidth), uintptr(color))
nullFill, _, _ := procGetStockObject.Call(nullBrush)
oldPen, _, _ := procSelectObject.Call(dc, pen)
oldBrush, _, _ := procSelectObject.Call(dc, nullFill)
defer func() {
procSelectObject.Call(dc, oldPen)
procSelectObject.Call(dc, oldBrush)
if pen != 0 {
procDeleteObject.Call(pen)
}
}()
fontName, _ := windows.UTF16PtrFromString("Segoe UI")
font, _, _ := procCreateFont.Call(^uintptr(55), 0, 0, 0, fontWeightBold, 0, 0, 0, 1, 0, 0, nonAntialiased, 0, uintptr(unsafe.Pointer(fontName)))
oldFont := uintptr(0)
if font != 0 {
oldFont, _, _ = procSelectObject.Call(dc, font)
defer func() { procSelectObject.Call(dc, oldFont); procDeleteObject.Call(font) }()
}
procSetBkMode.Call(dc, bkModeTransparent)
for i, zone := range v.zones {
r := layout.Resolve(v.work, zone, v.gap)
r.Left -= v.work.Left
r.Right -= v.work.Left
r.Top -= v.work.Top
r.Bottom -= v.work.Top
procRectangle.Call(dc, uintptr(r.Left), uintptr(r.Top), uintptr(r.Right), uintptr(r.Bottom))
name := strings.TrimSpace(zone.Name)
if name == "" {
name = fmt.Sprintf("Zone %d", i+1)
}
text, err := windows.UTF16FromString(name)
if err != nil || len(text) == 0 {
continue
}
shadow := rect{Left: r.Left + 2, Top: r.Top + 2, Right: r.Right + 2, Bottom: r.Bottom + 2}
procSetTextColor.Call(dc, 0x00000000)
procDrawText.Call(dc, uintptr(unsafe.Pointer(&text[0])), ^uintptr(0), uintptr(unsafe.Pointer(&shadow)), dtCenter|dtVCenter|dtSingleLine|dtEndEllipsis)
label := rect{Left: r.Left, Top: r.Top, Right: r.Right, Bottom: r.Bottom}
procSetTextColor.Call(dc, uintptr(color))
procDrawText.Call(dc, uintptr(unsafe.Pointer(&text[0])), ^uintptr(0), uintptr(unsafe.Pointer(&label)), dtCenter|dtVCenter|dtSingleLine|dtEndEllipsis)
}
}
func colorRef(value string) uint32 {
rgb, err := strconv.ParseUint(strings.TrimPrefix(value, "#"), 16, 24)
if err != nil {
return 0x00EFAE00
}
r := uint32(rgb >> 16)
g := uint32((rgb >> 8) & 0xff)
b := uint32(rgb & 0xff)
return r | g<<8 | b<<16
}
func overlaySignature(mon windows.Handle, work layout.Rect, zones []config.Zone, gap int, style config.Overlay) string {
var b strings.Builder
fmt.Fprintf(&b, "%x|%v|%d|%v", uintptr(mon), work, gap, style)
for _, zone := range zones {
fmt.Fprintf(&b, "|%s:%g:%g:%g:%g", zone.Name, zone.X, zone.Y, zone.Width, zone.Height)
}
return b.String()
}