feat: add portable Windows zone manager
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
//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()
|
||||
}
|
||||
Reference in New Issue
Block a user