diff --git a/v3/pkg/services/badge/badge.go b/v3/pkg/services/badge/badge.go index 61bffcdc7..d43e4bcc5 100644 --- a/v3/pkg/services/badge/badge.go +++ b/v3/pkg/services/badge/badge.go @@ -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() +} diff --git a/v3/pkg/services/badge/badge_darwin.go b/v3/pkg/services/badge/badge_darwin.go index 160d651a8..22969fc16 100644 --- a/v3/pkg/services/badge/badge_darwin.go +++ b/v3/pkg/services/badge/badge_darwin.go @@ -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 +} diff --git a/v3/pkg/services/badge/badge_windows.go b/v3/pkg/services/badge/badge_windows.go index 035cf0af5..76d4069bc 100644 --- a/v3/pkg/services/badge/badge_windows.go +++ b/v3/pkg/services/badge/badge_windows.go @@ -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 }