mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
windows badge
This commit is contained in:
parent
94d03d5f13
commit
be28da26b8
3 changed files with 139 additions and 273 deletions
|
|
@ -12,6 +12,7 @@ type platformBadge interface {
|
|||
Shutdown() error
|
||||
|
||||
SetBadge(label string) error
|
||||
RemoveBadge() error
|
||||
}
|
||||
|
||||
// Service represents the notifications service
|
||||
|
|
@ -37,3 +38,7 @@ func (b *Service) ServiceShutdown() error {
|
|||
func (b *Service) SetBadge(label string) error {
|
||||
return b.impl.SetBadge(label)
|
||||
}
|
||||
|
||||
func (b *Service) RemoveBadge() error {
|
||||
return b.impl.RemoveBadge()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,3 +49,8 @@ func (d *darwinBadge) SetBadge(label string) error {
|
|||
C.setBadge(cLabel)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *darwinBadge) RemoveBadge() error {
|
||||
C.setBadge(nil)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,44 +3,47 @@
|
|||
package badge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
)
|
||||
|
||||
// COM GUIDs for taskbar interfaces
|
||||
var (
|
||||
CLSID_TaskbarList = syscall.GUID{
|
||||
Data1: 0x56FDF344,
|
||||
Data2: 0xFD6D,
|
||||
Data3: 0x11D0,
|
||||
Data4: [8]byte{0x95, 0x8A, 0x00, 0x60, 0x97, 0xC9, 0xA0, 0x90},
|
||||
}
|
||||
IID_ITaskbarList3 = syscall.GUID{
|
||||
Data1: 0xEA1AFB91,
|
||||
Data2: 0x9E28,
|
||||
Data3: 0x4B86,
|
||||
Data4: [8]byte{0x90, 0xE9, 0x9E, 0x9F, 0x8A, 0x5E, 0xEF, 0xAF},
|
||||
}
|
||||
ole32 = syscall.NewLazyDLL("ole32.dll")
|
||||
shobjidl = syscall.NewLazyDLL("shell32.dll")
|
||||
coCreateInstance = ole32.NewProc("CoCreateInstance")
|
||||
)
|
||||
|
||||
// ITaskbarList3 COM interface
|
||||
type ITaskbarList3Vtbl struct {
|
||||
QueryInterface uintptr
|
||||
AddRef uintptr
|
||||
Release uintptr
|
||||
// ITaskbarList methods
|
||||
HrInit uintptr
|
||||
AddTab uintptr
|
||||
DeleteTab uintptr
|
||||
ActivateTab uintptr
|
||||
SetActiveAlt uintptr
|
||||
// ITaskbarList2 methods
|
||||
MarkFullscreenWindow uintptr
|
||||
// ITaskbarList3 methods
|
||||
const (
|
||||
CLSCTX_INPROC_SERVER = 0x1
|
||||
)
|
||||
|
||||
var (
|
||||
CLSID_TaskbarList = syscall.GUID{0x56FDF344, 0xFD6D, 0x11D0, [8]byte{0x95, 0x8A, 0x00, 0x60, 0x97, 0xC9, 0xA0, 0x90}}
|
||||
IID_ITaskbarList3 = syscall.GUID{0xEA1AFB91, 0x9E28, 0x4B86, [8]byte{0x90, 0xE9, 0x9E, 0x9F, 0x8A, 0x5E, 0xEF, 0xAF}}
|
||||
)
|
||||
|
||||
type ITaskbarList3 struct {
|
||||
lpVtbl *taskbarList3Vtbl
|
||||
}
|
||||
|
||||
type taskbarList3Vtbl struct {
|
||||
QueryInterface uintptr
|
||||
AddRef uintptr
|
||||
Release uintptr
|
||||
HrInit uintptr
|
||||
AddTab uintptr
|
||||
DeleteTab uintptr
|
||||
ActivateTab uintptr
|
||||
SetActiveAlt uintptr
|
||||
MarkFullscreenWindow uintptr
|
||||
SetProgressValue uintptr
|
||||
SetProgressState uintptr
|
||||
RegisterTab uintptr
|
||||
|
|
@ -53,308 +56,161 @@ type ITaskbarList3Vtbl struct {
|
|||
SetOverlayIcon uintptr
|
||||
SetThumbnailTooltip uintptr
|
||||
SetThumbnailClip uintptr
|
||||
SetTabProperties uintptr
|
||||
}
|
||||
|
||||
type ITaskbarList3 struct {
|
||||
Vtbl *ITaskbarList3Vtbl
|
||||
func newTaskbarList3() (*ITaskbarList3, error) {
|
||||
var taskbar *ITaskbarList3
|
||||
hr, _, _ := coCreateInstance.Call(
|
||||
uintptr(unsafe.Pointer(&CLSID_TaskbarList)),
|
||||
0,
|
||||
uintptr(CLSCTX_INPROC_SERVER),
|
||||
uintptr(unsafe.Pointer(&IID_ITaskbarList3)),
|
||||
uintptr(unsafe.Pointer(&taskbar)),
|
||||
)
|
||||
|
||||
if hr != 0 {
|
||||
return nil, syscall.Errno(hr)
|
||||
}
|
||||
|
||||
return taskbar, nil
|
||||
}
|
||||
|
||||
func (t *ITaskbarList3) SetOverlayIcon(hwnd syscall.Handle, hIcon syscall.Handle, description *uint16) error {
|
||||
ret, _, _ := syscall.SyscallN(
|
||||
t.lpVtbl.SetOverlayIcon,
|
||||
uintptr(unsafe.Pointer(t)),
|
||||
uintptr(hwnd),
|
||||
uintptr(hIcon),
|
||||
uintptr(unsafe.Pointer(description)),
|
||||
)
|
||||
if ret != 0 {
|
||||
return syscall.Errno(ret)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type windowsBadge struct {
|
||||
taskbarList *ITaskbarList3
|
||||
mainWindow *application.WebviewWindow
|
||||
initialized bool
|
||||
redBadgeIcon syscall.Handle
|
||||
taskbar *ITaskbarList3
|
||||
}
|
||||
|
||||
func New() *Service {
|
||||
return &Service{
|
||||
impl: &windowsBadge{
|
||||
mainWindow: application.Get().CurrentWindow(),
|
||||
redBadgeIcon: 0,
|
||||
},
|
||||
impl: &windowsBadge{},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *windowsBadge) Startup(ctx context.Context, options application.ServiceOptions) error {
|
||||
if d.initialized {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize COM
|
||||
err := coInitialize()
|
||||
taskbar, err := newTaskbarList3()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize COM: %w", err)
|
||||
return err
|
||||
}
|
||||
d.taskbar = taskbar
|
||||
|
||||
// Create an instance of the TaskbarList COM object
|
||||
var taskbarList *ITaskbarList3
|
||||
hr, _, _ := procCoCreateInstance.Call(
|
||||
uintptr(unsafe.Pointer(&CLSID_TaskbarList)),
|
||||
0,
|
||||
21, // CLSCTX_INPROC_SERVER
|
||||
uintptr(unsafe.Pointer(&IID_ITaskbarList3)),
|
||||
uintptr(unsafe.Pointer(&taskbarList)))
|
||||
if hr != 0 {
|
||||
return fmt.Errorf("failed to create TaskbarList instance: %d", hr)
|
||||
}
|
||||
|
||||
// Initialize the taskbar list
|
||||
hr, _, _ = syscall.Syscall(
|
||||
taskbarList.Vtbl.HrInit,
|
||||
1,
|
||||
uintptr(unsafe.Pointer(taskbarList)),
|
||||
0,
|
||||
0)
|
||||
if hr != 0 {
|
||||
return fmt.Errorf("failed to initialize TaskbarList: %d", hr)
|
||||
}
|
||||
|
||||
// Create the red badge icon
|
||||
redBadge, err := d.createRedBadgeIcon()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create red badge icon: %w", err)
|
||||
}
|
||||
|
||||
d.taskbarList = taskbarList
|
||||
d.redBadgeIcon = redBadge
|
||||
d.initialized = true
|
||||
// Don't try to get the window handle here - wait until SetBadge is called
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *windowsBadge) Shutdown() error {
|
||||
if !d.initialized {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy the red badge icon
|
||||
if d.redBadgeIcon != 0 {
|
||||
destroyIcon(d.redBadgeIcon)
|
||||
d.redBadgeIcon = 0
|
||||
}
|
||||
|
||||
// Release the taskbar list interface
|
||||
if d.taskbarList != nil {
|
||||
syscall.Syscall(
|
||||
d.taskbarList.Vtbl.Release,
|
||||
1,
|
||||
uintptr(unsafe.Pointer(d.taskbarList)),
|
||||
0,
|
||||
0)
|
||||
d.taskbarList = nil
|
||||
}
|
||||
|
||||
// Uninitialize COM
|
||||
coUninitialize()
|
||||
d.initialized = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *windowsBadge) SetBadge(label string) error {
|
||||
// If not initialized, initialize
|
||||
if !d.initialized {
|
||||
err := d.Startup(context.Background(), application.ServiceOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we have a window to work with
|
||||
if d.mainWindow == nil {
|
||||
d.mainWindow = application.Get().CurrentWindow()
|
||||
if d.mainWindow == nil {
|
||||
return fmt.Errorf("no window available for setting badge")
|
||||
}
|
||||
}
|
||||
|
||||
// Get the window handle using NativeWindowHandle() method
|
||||
handle, err := d.mainWindow.NativeWindowHandle()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get window handle: %w", err)
|
||||
}
|
||||
hwnd := handle
|
||||
|
||||
// If empty value, remove the overlay
|
||||
if label == "" {
|
||||
hr, _, _ := syscall.SyscallN(
|
||||
d.taskbarList.Vtbl.SetOverlayIcon,
|
||||
4,
|
||||
uintptr(unsafe.Pointer(d.taskbarList)),
|
||||
hwnd,
|
||||
0, // NULL icon handle
|
||||
0, // NULL description
|
||||
0, 0)
|
||||
if hr != 0 {
|
||||
return fmt.Errorf("failed to remove overlay icon: %d", hr)
|
||||
}
|
||||
if d.taskbar == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set the overlay icon with a description
|
||||
description, err := syscall.UTF16PtrFromString("New notification")
|
||||
// Get the window handle when SetBadge is called, not during startup
|
||||
app := application.Get()
|
||||
if app == nil {
|
||||
return nil // App not initialized yet
|
||||
}
|
||||
|
||||
window := app.CurrentWindow()
|
||||
if window == nil {
|
||||
return nil // No window available yet
|
||||
}
|
||||
|
||||
hwnd, err := window.NativeWindowHandle()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert description: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
hr, _, _ := syscall.SyscallN(
|
||||
d.taskbarList.Vtbl.SetOverlayIcon,
|
||||
4,
|
||||
uintptr(unsafe.Pointer(d.taskbarList)),
|
||||
hwnd,
|
||||
uintptr(d.redBadgeIcon),
|
||||
uintptr(unsafe.Pointer(description)),
|
||||
0, 0)
|
||||
if hr != 0 {
|
||||
return fmt.Errorf("failed to set overlay icon: %d", hr)
|
||||
if label == "" {
|
||||
return d.taskbar.SetOverlayIcon(syscall.Handle(hwnd), 0, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
hicon, err := createBadgeIcon()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w32.DestroyIcon(hicon)
|
||||
|
||||
return d.taskbar.SetOverlayIcon(syscall.Handle(hwnd), syscall.Handle(hicon), nil)
|
||||
}
|
||||
|
||||
// ICONINFO structure for creating icons
|
||||
type ICONINFO struct {
|
||||
FIcon uint32
|
||||
XHotspot uint32
|
||||
YHotspot uint32
|
||||
HbmMask syscall.Handle
|
||||
HbmColor syscall.Handle
|
||||
}
|
||||
|
||||
// createRedBadgeIcon creates a simple red circle icon for the badge
|
||||
func (d *windowsBadge) createRedBadgeIcon() (syscall.Handle, error) {
|
||||
// Create a 16x16 pixel bitmap
|
||||
hdc := getDC(0)
|
||||
if hdc == 0 {
|
||||
return 0, fmt.Errorf("failed to get DC")
|
||||
}
|
||||
defer releaseDC(0, hdc)
|
||||
|
||||
memDC := createCompatibleDC(hdc)
|
||||
if memDC == 0 {
|
||||
return 0, fmt.Errorf("failed to create compatible DC")
|
||||
}
|
||||
defer deleteObject(memDC)
|
||||
|
||||
// Create a bitmap
|
||||
bmp := createCompatibleBitmap(hdc, 16, 16)
|
||||
if bmp == 0 {
|
||||
return 0, fmt.Errorf("failed to create bitmap")
|
||||
func (d *windowsBadge) RemoveBadge() error {
|
||||
if d.taskbar == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
oldBmp := selectObject(memDC, bmp)
|
||||
|
||||
// Create a solid red brush
|
||||
redBrush := createSolidBrush(0x0000FF) // BGR format (blue=0, green=0, red=255)
|
||||
defer deleteObject(redBrush)
|
||||
|
||||
// Fill the circle with red
|
||||
selectObject(memDC, redBrush)
|
||||
ellipse(memDC, 0, 0, 16, 16)
|
||||
|
||||
// Restore original bitmap
|
||||
selectObject(memDC, oldBmp)
|
||||
|
||||
// Convert bitmap to icon
|
||||
iconInfo := ICONINFO{
|
||||
FIcon: 1, // TRUE for icon (vs. cursor)
|
||||
XHotspot: 0,
|
||||
YHotspot: 0,
|
||||
HbmMask: bmp, // Use same bitmap for mask
|
||||
HbmColor: bmp,
|
||||
// Get the window handle when SetBadge is called, not during startup
|
||||
app := application.Get()
|
||||
if app == nil {
|
||||
return nil // App not initialized yet
|
||||
}
|
||||
|
||||
icon := createIconIndirect(&iconInfo)
|
||||
if icon == 0 {
|
||||
deleteObject(bmp)
|
||||
return 0, fmt.Errorf("failed to create icon")
|
||||
window := app.CurrentWindow()
|
||||
if window == nil {
|
||||
return nil // No window available yet
|
||||
}
|
||||
|
||||
// Don't delete the bitmap here, as it's now owned by the icon
|
||||
return icon, nil
|
||||
}
|
||||
|
||||
// Define Windows API functions
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
gdi32 = syscall.NewLazyDLL("gdi32.dll")
|
||||
ole32 = syscall.NewLazyDLL("ole32.dll")
|
||||
|
||||
procCoCreateInstance = ole32.NewProc("CoCreateInstance")
|
||||
procCoInitialize = ole32.NewProc("CoInitialize")
|
||||
procCoUninitialize = ole32.NewProc("CoUninitialize")
|
||||
|
||||
procGetDC = user32.NewProc("GetDC")
|
||||
procReleaseDC = user32.NewProc("ReleaseDC")
|
||||
procDestroyIcon = user32.NewProc("DestroyIcon")
|
||||
procCreateIconIndirect = user32.NewProc("CreateIconIndirect")
|
||||
|
||||
procCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC")
|
||||
procCreateCompatibleBitmap = gdi32.NewProc("CreateCompatibleBitmap")
|
||||
procSelectObject = gdi32.NewProc("SelectObject")
|
||||
procDeleteObject = gdi32.NewProc("DeleteObject")
|
||||
procCreateSolidBrush = gdi32.NewProc("CreateSolidBrush")
|
||||
procEllipse = gdi32.NewProc("Ellipse")
|
||||
)
|
||||
|
||||
// GDI function wrappers
|
||||
func getDC(hwnd syscall.Handle) syscall.Handle {
|
||||
ret, _, _ := procGetDC.Call(uintptr(hwnd))
|
||||
return syscall.Handle(ret)
|
||||
}
|
||||
|
||||
func releaseDC(hwnd, hdc syscall.Handle) bool {
|
||||
ret, _, _ := procReleaseDC.Call(uintptr(hwnd), uintptr(hdc))
|
||||
return ret != 0
|
||||
}
|
||||
|
||||
func createCompatibleDC(hdc syscall.Handle) syscall.Handle {
|
||||
ret, _, _ := procCreateCompatibleDC.Call(uintptr(hdc))
|
||||
return syscall.Handle(ret)
|
||||
}
|
||||
|
||||
func createCompatibleBitmap(hdc syscall.Handle, width, height int32) syscall.Handle {
|
||||
ret, _, _ := procCreateCompatibleBitmap.Call(uintptr(hdc), uintptr(width), uintptr(height))
|
||||
return syscall.Handle(ret)
|
||||
}
|
||||
|
||||
func selectObject(hdc, hgdiobj syscall.Handle) syscall.Handle {
|
||||
ret, _, _ := procSelectObject.Call(uintptr(hdc), uintptr(hgdiobj))
|
||||
return syscall.Handle(ret)
|
||||
}
|
||||
|
||||
func deleteObject(hObject syscall.Handle) bool {
|
||||
ret, _, _ := procDeleteObject.Call(uintptr(hObject))
|
||||
return ret != 0
|
||||
}
|
||||
|
||||
func createSolidBrush(color int32) syscall.Handle {
|
||||
ret, _, _ := procCreateSolidBrush.Call(uintptr(color))
|
||||
return syscall.Handle(ret)
|
||||
}
|
||||
|
||||
func ellipse(hdc syscall.Handle, left, top, right, bottom int32) bool {
|
||||
ret, _, _ := procEllipse.Call(uintptr(hdc), uintptr(left), uintptr(top), uintptr(right), uintptr(bottom))
|
||||
return ret != 0
|
||||
}
|
||||
|
||||
func createIconIndirect(iconInfo *ICONINFO) syscall.Handle {
|
||||
ret, _, _ := procCreateIconIndirect.Call(uintptr(unsafe.Pointer(iconInfo)))
|
||||
return syscall.Handle(ret)
|
||||
}
|
||||
|
||||
func destroyIcon(hIcon syscall.Handle) bool {
|
||||
ret, _, _ := procDestroyIcon.Call(uintptr(hIcon))
|
||||
return ret != 0
|
||||
}
|
||||
|
||||
func coInitialize() error {
|
||||
hr, _, _ := procCoInitialize.Call(0)
|
||||
if hr != 0 {
|
||||
return fmt.Errorf("CoInitialize failed with code: %d", hr)
|
||||
hwnd, err := window.NativeWindowHandle()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
return d.taskbar.SetOverlayIcon(syscall.Handle(hwnd), 0, nil)
|
||||
}
|
||||
|
||||
func coUninitialize() {
|
||||
procCoUninitialize.Call()
|
||||
func createBadgeIcon() (uintptr, error) {
|
||||
const size = 32
|
||||
|
||||
img := image.NewRGBA(image.Rect(0, 0, size, size))
|
||||
|
||||
red := color.RGBA{255, 0, 0, 255}
|
||||
radius := size / 2
|
||||
centerX, centerY := radius, radius
|
||||
|
||||
for y := 0; y < size; y++ {
|
||||
for x := 0; x < size; x++ {
|
||||
dx := float64(x - centerX)
|
||||
dy := float64(y - centerY)
|
||||
|
||||
if dx*dx+dy*dy < float64(radius*radius) {
|
||||
img.Set(x, y, red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
white := color.RGBA{255, 255, 255, 255}
|
||||
innerRadius := size / 5
|
||||
|
||||
for y := 0; y < size; y++ {
|
||||
for x := 0; x < size; x++ {
|
||||
dx := float64(x - centerX)
|
||||
dy := float64(y - centerY)
|
||||
|
||||
if dx*dx+dy*dy < float64(innerRadius*innerRadius) {
|
||||
img.Set(x, y, white)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
hicon, err := w32.CreateSmallHIconFromImage(buf.Bytes())
|
||||
return uintptr(hicon), err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue