301 lines
9.3 KiB
Go
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()
|
|
}
|