//go:build windows package dockicon import ( "bytes" _ "embed" "image" "image/png" "sync" "unsafe" "golang.org/x/image/draw" "golang.org/x/sys/windows" ) //go:embed icon_idle.png var iconIdlePNG []byte //go:embed icon_connected.png var iconConnectedPNG []byte var ( user32 = windows.NewLazySystemDLL("user32.dll") gdi32 = windows.NewLazySystemDLL("gdi32.dll") procSendMessage = user32.NewProc("SendMessageW") procCreateIconFromResource = user32.NewProc("CreateIconFromResourceEx") procCreateIconIndirect = user32.NewProc("CreateIconIndirect") procDestroyIcon = user32.NewProc("DestroyIcon") procCreateDIBSection = gdi32.NewProc("CreateDIBSection") procCreateBitmap = gdi32.NewProc("CreateBitmap") procDeleteObject = gdi32.NewProc("DeleteObject") mu sync.Mutex hwnd uintptr last *bool idleSmall windows.Handle idleBig windows.Handle connSmall windows.Handle connBig windows.Handle iconsReady bool ) const ( wmSetIcon = 0x0080 iconSmall = 0 iconBig = 1 biRGB = 0 dibRGBColors = 0 ) type iconInfo struct { FIcon int32 XHotspot uint32 YHotspot uint32 HbmMask windows.Handle HbmColor windows.Handle } type bitmapInfoHeader struct { BiSize uint32 BiWidth int32 BiHeight int32 BiPlanes uint16 BiBitCount uint16 BiCompression uint32 BiSizeImage uint32 BiXPelsPerMeter int32 BiYPelsPerMeter int32 BiClrUsed uint32 BiClrImportant uint32 } type bitmapInfo struct { Header bitmapInfoHeader Colors [1]uint32 } // BindWindow attaches the main HWND so SetConnected can update the taskbar icon. func BindWindow(h unsafe.Pointer) { mu.Lock() defer mu.Unlock() hwnd = uintptr(h) if err := ensureIconsLocked(); err != nil { return } connected := last != nil && *last applyLocked(connected) } // SetConnected swaps the taskbar/window icon: sea-wave N when connected, original when idle. func SetConnected(connected bool) { mu.Lock() if last != nil && *last == connected { mu.Unlock() return } v := connected last = &v if hwnd == 0 { mu.Unlock() if OnIconChange != nil { OnIconChange(connected) } return } if err := ensureIconsLocked(); err != nil { mu.Unlock() return } applyLocked(connected) mu.Unlock() if OnIconChange != nil { OnIconChange(connected) } } // StatusIcon returns a shared HICON for the current connection state (do not DestroyIcon). func StatusIcon(connected bool) windows.Handle { mu.Lock() defer mu.Unlock() if err := ensureIconsLocked(); err != nil { return 0 } if connected { return connBig } return idleBig } func applyLocked(connected bool) { if hwnd == 0 { return } small, big := idleSmall, idleBig if connected { small, big = connSmall, connBig } if small != 0 { procSendMessage.Call(hwnd, wmSetIcon, iconSmall, uintptr(small)) } if big != 0 { procSendMessage.Call(hwnd, wmSetIcon, iconBig, uintptr(big)) } } func ensureIconsLocked() error { if iconsReady { return nil } var err error idleSmall, err = hiconFromPNG(iconIdlePNG, 16) if err != nil { return err } idleBig, err = hiconFromPNG(iconIdlePNG, 32) if err != nil { return err } connSmall, err = hiconFromPNG(iconConnectedPNG, 16) if err != nil { return err } connBig, err = hiconFromPNG(iconConnectedPNG, 32) if err != nil { return err } iconsReady = true return nil } func hiconFromPNG(src []byte, size int) (windows.Handle, error) { resized, err := resizePNG(src, size) if err != nil { return 0, err } if h := createIconFromPNGResource(resized, size); h != 0 { return h, nil } return createIconFromImage(resized, size) } func resizePNG(src []byte, size int) ([]byte, error) { img, err := png.Decode(bytes.NewReader(src)) if err != nil { return nil, err } dst := image.NewRGBA(image.Rect(0, 0, size, size)) draw.CatmullRom.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil) var buf bytes.Buffer if err := png.Encode(&buf, dst); err != nil { return nil, err } return buf.Bytes(), nil } func createIconFromPNGResource(pngBytes []byte, size int) windows.Handle { if len(pngBytes) == 0 { return 0 } h, _, _ := procCreateIconFromResource.Call( uintptr(unsafe.Pointer(&pngBytes[0])), uintptr(len(pngBytes)), 1, 0x00030000, uintptr(size), uintptr(size), 0, ) return windows.Handle(h) } func createIconFromImage(pngBytes []byte, size int) (windows.Handle, error) { img, err := png.Decode(bytes.NewReader(pngBytes)) if err != nil { return 0, err } rgba, ok := img.(*image.RGBA) if !ok { tmp := image.NewRGBA(img.Bounds()) draw.Draw(tmp, tmp.Bounds(), img, img.Bounds().Min, draw.Src) rgba = tmp } hbmColor, err := dibFromRGBA(rgba) if err != nil { return 0, err } // Monochrome mask: all zeros => fully opaque where color has alpha handled by 32-bit DIB. hbmMask, _, err2 := procCreateBitmap.Call(uintptr(size), uintptr(size), 1, 1, 0) if hbmMask == 0 { procDeleteObject.Call(uintptr(hbmColor)) if err2 != nil { return 0, err2 } return 0, windows.ERROR_INVALID_HANDLE } ii := iconInfo{ FIcon: 1, XHotspot: 0, YHotspot: 0, HbmMask: windows.Handle(hbmMask), HbmColor: hbmColor, } h, _, err3 := procCreateIconIndirect.Call(uintptr(unsafe.Pointer(&ii))) procDeleteObject.Call(uintptr(hbmMask)) procDeleteObject.Call(uintptr(hbmColor)) if h == 0 { if err3 != nil { return 0, err3 } return 0, windows.ERROR_INVALID_HANDLE } return windows.Handle(h), nil } func dibFromRGBA(src *image.RGBA) (windows.Handle, error) { w := src.Bounds().Dx() h := src.Bounds().Dy() bi := bitmapInfo{ Header: bitmapInfoHeader{ BiSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})), BiWidth: int32(w), BiHeight: -int32(h), // top-down BiPlanes: 1, BiBitCount: 32, BiCompression: biRGB, }, } var bits unsafe.Pointer hbm, _, err := procCreateDIBSection.Call(0, uintptr(unsafe.Pointer(&bi)), dibRGBColors, uintptr(unsafe.Pointer(&bits)), 0, 0) if hbm == 0 || bits == nil { if err != nil { return 0, err } return 0, windows.ERROR_INVALID_HANDLE } // Windows wants BGRA. dst := unsafe.Slice((*byte)(bits), w*h*4) for y := 0; y < h; y++ { for x := 0; x < w; x++ { i := src.PixOffset(x, y) o := (y*w + x) * 4 dst[o+0] = src.Pix[i+2] // B dst[o+1] = src.Pix[i+1] // G dst[o+2] = src.Pix[i+0] // R dst[o+3] = src.Pix[i+3] // A } } return windows.Handle(hbm), nil }