Skip to content

[V3] Badging #4234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Apr 28, 2025
Merged
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
94d03d5
badge service
popaprozac Apr 24, 2025
be28da2
windows badge
popaprozac Apr 24, 2025
b55f4ea
add example, update docs, update changelog
popaprozac Apr 24, 2025
f311a33
update changelog
popaprozac Apr 24, 2025
d1d57c1
rabbit suggestions
popaprozac Apr 24, 2025
7287c1e
render label on windows and sensible defaults
popaprozac Apr 25, 2025
c0a83ef
Update docs/src/content/docs/learn/badges.mdx
popaprozac Apr 25, 2025
59cc71b
fix code block
popaprozac Apr 25, 2025
9fa5967
Include assets in example
leaanthony Apr 25, 2025
5154f0b
Add FontManager to better handle fonts. Remove the go-findfont depend…
leaanthony Apr 25, 2025
f7aaf84
Update v3/pkg/services/badge/badge_windows.go
leaanthony Apr 25, 2025
52df483
Tidy up
leaanthony Apr 26, 2025
e991615
Add badge options. Add new example.
leaanthony Apr 26, 2025
8d9dc19
Update badge-custom example
leaanthony Apr 26, 2025
73705dc
add build tag
popaprozac Apr 26, 2025
dabc18f
center circle color
popaprozac Apr 26, 2025
800810f
extract taskbar
popaprozac Apr 27, 2025
9e3786c
add comments and macOS stub
popaprozac Apr 27, 2025
ce8c102
fix double uninit
popaprozac Apr 27, 2025
40117e6
fallback to default badge if font cannot be used
popaprozac Apr 27, 2025
af3c6af
update docs and readmes
popaprozac Apr 27, 2025
51c0d1d
update headers
popaprozac Apr 27, 2025
900da01
add set custom badge and update docs/examples/readmes
popaprozac Apr 27, 2025
b942293
Update docs/src/content/docs/learn/badges.mdx
popaprozac Apr 27, 2025
934d8c8
update docs
popaprozac Apr 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
render label on windows and sensible defaults
  • Loading branch information
popaprozac committed Apr 25, 2025
commit 7287c1eb5cdccdc148a0bf9ccfc9f1cf9c287724
18 changes: 7 additions & 11 deletions docs/src/content/docs/learn/badges.mdx
Original file line number Diff line number Diff line change
@@ -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

</TabItem>

@@ -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

</TabItem>

1 change: 1 addition & 0 deletions v3/go.mod
Original file line number Diff line number Diff line change
@@ -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
)

2 changes: 2 additions & 0 deletions v3/go.sum
Original file line number Diff line number Diff line change
@@ -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=
2 changes: 2 additions & 0 deletions v3/pkg/services/badge/badge_darwin.go
Original file line number Diff line number Diff line change
@@ -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
136 changes: 106 additions & 30 deletions v3/pkg/services/badge/badge_windows.go
Original file line number Diff line number Diff line change
@@ -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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Potential memory leak: badge image creation

The createBadge() method is called every time SetBadge() is called, resulting in a new image allocation. This could lead to memory pressure under frequent badge updates.

Consider caching the badge image and only recreating it when necessary:

-	w.createBadge()
+	// Only create the badge image if it doesn't exist
+	if w.badgeImg == 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,45 +200,107 @@ func (w *windowsBadge) RemoveBadge() error {
return w.taskbar.SetOverlayIcon(syscall.Handle(hwnd), 0, nil)
}

func createBadgeIcon() (w32.HICON, error) {
const size = 32

img := image.NewRGBA(image.Rect(0, 0, size, size))

red := color.RGBA{255, 0, 0, 255}
radius := size / 2
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

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)

if dx*dx+dy*dy < float64(radius*radius) {
img.Set(x, y, red)
if dx*dx+dy*dy < float64(innerRadius*innerRadius) {
w.badgeImg.Set(x, y, white)
}
}
}

white := color.RGBA{255, 255, 255, 255}
innerRadius := size / 5
var buf bytes.Buffer
if err := png.Encode(&buf, w.badgeImg); err != nil {
return 0, err
}

for y := 0; y < size; y++ {
for x := 0; x < size; x++ {
dx := float64(x - centerX)
dy := float64(y - centerY)
hicon, err := w32.CreateSmallHIconFromImage(buf.Bytes())
return hicon, err
}

if dx*dx+dy*dy < float64(innerRadius*innerRadius) {
img.Set(x, y, white)
}
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, img); err != nil {
if err := png.Encode(&buf, w.badgeImg); err != nil {
return 0, err
}

hicon, err := w32.CreateSmallHIconFromImage(buf.Bytes())
return hicon, 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 := w.badgeSize / 2
centerX, centerY := radius, radius

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(radius*radius) {
img.Set(x, y, red)
}
}
}

w.badgeImg = img
}