//go:build windows package platform import ( "bytes" "fmt" "image/png" "strings" "unsafe" "github.com/stevec/fancywin/internal/assets" "golang.org/x/sys/windows" ) const ( wmAppTray = 0x8001 wmRButtonUp = 0x0205 wmContextMenu = 0x007B wmNull = 0x0000 nimAdd = 0x00000000 nimModify = 0x00000001 nimDelete = 0x00000002 nifMessage = 0x00000001 nifIcon = 0x00000002 nifTip = 0x00000004 mfString = 0x00000000 mfGray = 0x00000001 mfChecked = 0x00000008 mfSeparator = 0x00000800 tpmRightButton = 0x00000002 tpmReturnCmd = 0x00000100 tpmNonotify = 0x00000080 trayCommandExit = 1001 trayLayoutBase = 2000 idiApplication = 32512 biRGB = 0 dibRGBColors = 0 ) var ( shell32 = windows.NewLazySystemDLL("shell32.dll") procShellNotifyIcon = shell32.NewProc("Shell_NotifyIconW") procLoadIcon = user32.NewProc("LoadIconW") procCreatePopupMenu = user32.NewProc("CreatePopupMenu") procAppendMenu = user32.NewProc("AppendMenuW") procTrackPopupMenu = user32.NewProc("TrackPopupMenu") procDestroyMenu = user32.NewProc("DestroyMenu") procSetForegroundWindow = user32.NewProc("SetForegroundWindow") procPostMessage = user32.NewProc("PostMessageW") procPostQuitMessage = user32.NewProc("PostQuitMessage") procRegisterWindowMessage = user32.NewProc("RegisterWindowMessageW") procCreateIconIndirect = user32.NewProc("CreateIconIndirect") procDestroyIcon = user32.NewProc("DestroyIcon") procCreateDIBSection = gdi32.NewProc("CreateDIBSection") procCreateBitmap = gdi32.NewProc("CreateBitmap") trayClassName, _ = windows.UTF16PtrFromString("FancyWinTrayWindow") trayWindowProc = windows.NewCallback(trayWndProc) activeTray *trayIcon ) type notifyIconData struct { Size uint32 Window windows.Handle ID uint32 Flags uint32 CallbackMessage uint32 Icon windows.Handle Tip [128]uint16 State uint32 StateMask uint32 Info [256]uint16 TimeoutOrVersion uint32 InfoTitle [64]uint16 InfoFlags uint32 GUID [16]byte BalloonIcon windows.Handle } type trayIcon struct { hwnd windows.Handle icon windows.Handle activeLayout string layouts []string taskbarMessage uint32 ownsIcon bool } type bitmapInfoHeader struct { Size uint32 Width int32 Height int32 Planes uint16 BitCount uint16 Compression uint32 SizeImage uint32 XPelsPerMeter int32 YPelsPerMeter int32 ClrUsed uint32 ClrImportant uint32 } type iconInfo struct { Icon int32 XHotspot uint32 YHotspot uint32 Mask windows.Handle Color windows.Handle } func newTrayIcon(activeLayout string, layouts []string) (*trayIcon, error) { instance, _, instanceErr := procGetModuleHandle.Call(0) if instance == 0 { return nil, fmt.Errorf("tray GetModuleHandleW: %v", instanceErr) } wc := windowClassEx{ Size: uint32(unsafe.Sizeof(windowClassEx{})), WndProc: trayWindowProc, Instance: windows.Handle(instance), ClassName: trayClassName, } if atom, _, err := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc))); atom == 0 { return nil, fmt.Errorf("register tray window class: %v", err) } raw, _, createErr := procCreateWindowEx.Call( 0, uintptr(unsafe.Pointer(trayClassName)), uintptr(unsafe.Pointer(trayClassName)), 0, 0, 0, 0, 0, 0, 0, instance, 0, ) if raw == 0 { return nil, fmt.Errorf("create tray message window: %v", createErr) } icon, iconErr := loadFancyIcon() ownsIcon := icon != 0 if icon == 0 { fallback, _, fallbackErr := procLoadIcon.Call(0, idiApplication) if fallback == 0 { procDestroyWindow.Call(raw) return nil, fmt.Errorf("load tray icon: generated icon: %v; fallback: %v", iconErr, fallbackErr) } icon = windows.Handle(fallback) } taskbarName, _ := windows.UTF16PtrFromString("TaskbarCreated") taskbarMessage, _, _ := procRegisterWindowMessage.Call(uintptr(unsafe.Pointer(taskbarName))) t := &trayIcon{ hwnd: windows.Handle(raw), icon: windows.Handle(icon), activeLayout: activeLayout, layouts: append([]string(nil), layouts...), taskbarMessage: uint32(taskbarMessage), ownsIcon: ownsIcon, } activeTray = t if err := t.notify(nimAdd); err != nil { activeTray = nil procDestroyWindow.Call(raw) return nil, err } return t, nil } func (t *trayIcon) close() { if t == nil || t.hwnd == 0 { return } _ = t.notify(nimDelete) procDestroyWindow.Call(uintptr(t.hwnd)) if t.ownsIcon && t.icon != 0 { procDestroyIcon.Call(uintptr(t.icon)) } if activeTray == t { activeTray = nil } t.hwnd = 0 } func (t *trayIcon) setLayouts(active string, layouts []string) { if t == nil { return } t.activeLayout = active t.layouts = append(t.layouts[:0], layouts...) _ = t.notify(nimModify) } func (t *trayIcon) notify(operation uintptr) error { nid := notifyIconData{ Size: uint32(unsafe.Sizeof(notifyIconData{})), Window: t.hwnd, ID: 1, Flags: nifMessage | nifIcon | nifTip, CallbackMessage: wmAppTray, Icon: t.icon, } tip, _ := windows.UTF16FromString("FancyWin — " + t.activeLayout) copy(nid.Tip[:len(nid.Tip)-1], tip) if ok, _, err := procShellNotifyIcon.Call(operation, uintptr(unsafe.Pointer(&nid))); ok == 0 { return fmt.Errorf("update notification-area icon: %v", err) } return nil } func trayWndProc(rawHWND, message, wParam, lParam uintptr) uintptr { t := activeTray if t != nil && t.taskbarMessage != 0 && uint32(message) == t.taskbarMessage { _ = t.notify(nimAdd) // Explorer was restarted; restore the icon. return 0 } if message == wmAppTray && (uint32(lParam) == wmRButtonUp || uint32(lParam) == wmContextMenu) { if t != nil { t.showMenu() } return 0 } r, _, _ := procDefWindowProc.Call(rawHWND, message, wParam, lParam) return r } func (t *trayIcon) showMenu() { menu, _, _ := procCreatePopupMenu.Call() if menu == 0 { return } defer procDestroyMenu.Call(menu) heading, _ := windows.UTF16PtrFromString("Layouts") exit, _ := windows.UTF16PtrFromString("Exit") procAppendMenu.Call(menu, mfString|mfGray, 0, uintptr(unsafe.Pointer(heading))) for i, name := range t.layouts { label, _ := windows.UTF16PtrFromString(name) flags := uintptr(mfString) if strings.EqualFold(name, t.activeLayout) { flags |= mfChecked } procAppendMenu.Call(menu, flags, uintptr(trayLayoutBase+i), uintptr(unsafe.Pointer(label))) } procAppendMenu.Call(menu, mfSeparator, 0, 0) procAppendMenu.Call(menu, mfString, trayCommandExit, uintptr(unsafe.Pointer(exit))) var p point if ok, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&p))); ok == 0 { return } procSetForegroundWindow.Call(uintptr(t.hwnd)) command, _, _ := procTrackPopupMenu.Call(menu, tpmRightButton|tpmReturnCmd|tpmNonotify, uintptr(p.X), uintptr(p.Y), 0, uintptr(t.hwnd), 0) procPostMessage.Call(uintptr(t.hwnd), wmNull, 0, 0) if command == trayCommandExit { procPostQuitMessage.Call(0) } else if command >= trayLayoutBase && command < uintptr(trayLayoutBase+len(t.layouts)) { if e := activeEngine; e != nil { e.selectLayout(t.layouts[int(command)-trayLayoutBase]) } } } func loadFancyIcon() (windows.Handle, error) { img, err := png.Decode(bytes.NewReader(assets.TrayIconPNG)) if err != nil { return 0, err } bounds := img.Bounds() w, h := bounds.Dx(), bounds.Dy() if w < 1 || h < 1 { return 0, fmt.Errorf("embedded icon is empty") } header := bitmapInfoHeader{ Size: uint32(unsafe.Sizeof(bitmapInfoHeader{})), Width: int32(w), Height: -int32(h), Planes: 1, BitCount: 32, Compression: biRGB, SizeImage: uint32(w * h * 4), } var bits unsafe.Pointer color, _, dibErr := procCreateDIBSection.Call(0, uintptr(unsafe.Pointer(&header)), dibRGBColors, uintptr(unsafe.Pointer(&bits)), 0, 0) if color == 0 || bits == nil { return 0, fmt.Errorf("CreateDIBSection: %v", dibErr) } defer procDeleteObject.Call(color) pixels := unsafe.Slice((*byte)(bits), w*h*4) for y := 0; y < h; y++ { for x := 0; x < w; x++ { r, g, b, a := img.At(bounds.Min.X+x, bounds.Min.Y+y).RGBA() i := (y*w + x) * 4 pixels[i+0] = byte(b >> 8) pixels[i+1] = byte(g >> 8) pixels[i+2] = byte(r >> 8) pixels[i+3] = byte(a >> 8) } } maskStride := ((w + 15) / 16) * 2 maskBits := make([]byte, maskStride*h) mask, _, maskErr := procCreateBitmap.Call(uintptr(w), uintptr(h), 1, 1, uintptr(unsafe.Pointer(&maskBits[0]))) if mask == 0 { return 0, fmt.Errorf("CreateBitmap icon mask: %v", maskErr) } defer procDeleteObject.Call(mask) info := iconInfo{Icon: 1, Mask: windows.Handle(mask), Color: windows.Handle(color)} icon, _, iconErr := procCreateIconIndirect.Call(uintptr(unsafe.Pointer(&info))) if icon == 0 { return 0, fmt.Errorf("CreateIconIndirect: %v", iconErr) } return windows.Handle(icon), nil }