From 7287c1eb5cdccdc148a0bf9ccfc9f1cf9c287724 Mon Sep 17 00:00:00 2001 From: popaprozac Date: Thu, 24 Apr 2025 20:23:28 -0700 Subject: [PATCH] render label on windows and sensible defaults --- docs/src/content/docs/learn/badges.mdx | 18 ++-- v3/go.mod | 1 + v3/go.sum | 2 + v3/pkg/services/badge/badge_darwin.go | 2 + v3/pkg/services/badge/badge_windows.go | 144 +++++++++++++++++++------ 5 files changed, 122 insertions(+), 45 deletions(-) diff --git a/docs/src/content/docs/learn/badges.mdx b/docs/src/content/docs/learn/badges.mdx index 9c73ccedb..d68bc3366 100644 --- a/docs/src/content/docs/learn/badges.mdx +++ b/docs/src/content/docs/learn/badges.mdx @@ -36,14 +36,14 @@ app := application.New(application.Options{ Set a badge on the application tile/dock icon: ```go +// Set a default badge +badgeService.SetBadge("") + // Set a numeric badge badgeService.SetBadge("3") // Set a text badge badgeService.SetBadge("New") - -// Set a symbol badge -badgeService.SetBadge("●") ``` ### Removing a Badge @@ -52,11 +52,6 @@ Remove the badge from the application icon: ```go badgeService.RemoveBadge() - -// or - -// Set an empty string -badgeService.SetBadge("") ``` ## Platform Considerations @@ -67,9 +62,10 @@ badgeService.SetBadge("") On macOS, badges: - Are displayed directly on the dock icon - - Support both text and numeric values + - Support text values - Automatically handle dark/light mode appearance - Use the standard macOS dock badge styling + - Automatically handle label overflow @@ -78,10 +74,10 @@ badgeService.SetBadge("") On Windows, badges: - Are displayed as an overlay icon in the taskbar - - Currently implemented as a red circle with a white center - - Do not currently support displaying text or numbers + - Support text values - Adapt to Windows theme settings - Require the application to have a window + - Does not handle label overflow diff --git a/v3/go.mod b/v3/go.mod index 947f0495f..2bcf98a79 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -49,6 +49,7 @@ require ( github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/flopp/go-findfont v0.1.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect ) diff --git a/v3/go.sum b/v3/go.sum index 2c07fe6c8..c778b0d96 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -117,6 +117,8 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU= +github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= diff --git a/v3/pkg/services/badge/badge_darwin.go b/v3/pkg/services/badge/badge_darwin.go index 22969fc16..d61b9ca70 100644 --- a/v3/pkg/services/badge/badge_darwin.go +++ b/v3/pkg/services/badge/badge_darwin.go @@ -45,6 +45,8 @@ func (d *darwinBadge) SetBadge(label string) error { if label != "" { cLabel = C.CString(label) defer C.free(unsafe.Pointer(cLabel)) + } else { + cLabel = C.CString("●") // Default badge character } C.setBadge(cLabel) return nil diff --git a/v3/pkg/services/badge/badge_windows.go b/v3/pkg/services/badge/badge_windows.go index c16375f00..e6b8e4061 100644 --- a/v3/pkg/services/badge/badge_windows.go +++ b/v3/pkg/services/badge/badge_windows.go @@ -8,11 +8,17 @@ import ( "image" "image/color" "image/png" + "os" + "strings" "syscall" "unsafe" + "github.com/flopp/go-findfont" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/w32" + "golang.org/x/image/font" + "golang.org/x/image/font/opentype" + "golang.org/x/image/math/fixed" ) var ( @@ -102,7 +108,9 @@ func (t *ITaskbarList3) SetOverlayIcon(hwnd syscall.Handle, hIcon syscall.Handle } type windowsBadge struct { - taskbar *ITaskbarList3 + taskbar *ITaskbarList3 + badgeImg *image.RGBA + badgeSize int } func New() *Service { @@ -150,13 +158,19 @@ func (w *windowsBadge) SetBadge(label string) error { return err } - if label == "" { - return w.taskbar.SetOverlayIcon(syscall.Handle(hwnd), 0, nil) - } + w.createBadge() - hicon, err := createBadgeIcon() - if err != nil { - return err + var hicon w32.HICON + if label == "" { + hicon, err = w.createBadgeIcon() + if err != nil { + return err + } + } else { + hicon, err = w.createBadgeIconWithText(label) + if err != nil { + return err + } } defer w32.DestroyIcon(hicon) @@ -186,17 +200,99 @@ func (w *windowsBadge) RemoveBadge() error { return w.taskbar.SetOverlayIcon(syscall.Handle(hwnd), 0, nil) } -func createBadgeIcon() (w32.HICON, error) { - const size = 32 +func (w *windowsBadge) createBadgeIcon() (w32.HICON, error) { + radius := w.badgeSize / 2 + centerX, centerY := radius, radius + white := color.RGBA{255, 255, 255, 255} + innerRadius := w.badgeSize / 5 - img := image.NewRGBA(image.Rect(0, 0, size, size)) + for y := 0; y < w.badgeSize; y++ { + for x := 0; x < w.badgeSize; x++ { + dx := float64(x - centerX) + dy := float64(y - centerY) + + if dx*dx+dy*dy < float64(innerRadius*innerRadius) { + w.badgeImg.Set(x, y, white) + } + } + } + + var buf bytes.Buffer + if err := png.Encode(&buf, w.badgeImg); err != nil { + return 0, err + } + + hicon, err := w32.CreateSmallHIconFromImage(buf.Bytes()) + return hicon, err +} + +func (w *windowsBadge) createBadgeIconWithText(label string) (w32.HICON, error) { + fontPath := "" + for _, path := range findfont.List() { + if strings.Contains(strings.ToLower(path), "segoeuib.ttf") || // Segoe UI Bold + strings.Contains(strings.ToLower(path), "arialbd.ttf") { + fontPath = path + break + } + } + if fontPath == "" { + return w.createBadgeIcon() + } + + fontBytes, err := os.ReadFile(fontPath) + if err != nil { + return 0, err + } + + ttf, err := opentype.Parse(fontBytes) + if err != nil { + return 0, err + } + + fontSize := 18.0 + if len(label) > 1 { + fontSize = 14.0 + } + + face, err := opentype.NewFace(ttf, &opentype.FaceOptions{ + Size: fontSize, + DPI: 96, + Hinting: font.HintingFull, + }) + if err != nil { + return 0, err + } + defer face.Close() + + d := &font.Drawer{ + Dst: w.badgeImg, + Src: image.NewUniform(color.White), + Face: face, + } + + textWidth := d.MeasureString(label).Ceil() + d.Dot = fixed.P((w.badgeSize-textWidth)/2, int(float64(w.badgeSize)/2+fontSize/2)) + d.DrawString(label) + + var buf bytes.Buffer + if err := png.Encode(&buf, w.badgeImg); err != nil { + return 0, err + } + + return w32.CreateSmallHIconFromImage(buf.Bytes()) +} + +func (w *windowsBadge) createBadge() { + w.badgeSize = 32 + + img := image.NewRGBA(image.Rect(0, 0, w.badgeSize, w.badgeSize)) red := color.RGBA{255, 0, 0, 255} - radius := size / 2 + radius := w.badgeSize / 2 centerX, centerY := radius, radius - for y := 0; y < size; y++ { - for x := 0; x < size; x++ { + for y := 0; y < w.badgeSize; y++ { + for x := 0; x < w.badgeSize; x++ { dx := float64(x - centerX) dy := float64(y - centerY) @@ -206,25 +302,5 @@ func createBadgeIcon() (w32.HICON, error) { } } - 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 hicon, err + w.badgeImg = img }