Add FontManager to better handle fonts. Remove the go-findfont dependency. Add Go examples.

This commit is contained in:
Lea Anthony 2025-04-25 15:17:39 +10:00
commit 5154f0b60f
No known key found for this signature in database
GPG key ID: 33DAF7BB90A58405
13 changed files with 335 additions and 29 deletions

View file

@ -127,10 +127,10 @@ function eventTarget(event) {
document.addEventListener("DOMContentLoaded", () => {
});
window.addEventListener("contextmenu", contextMenuHandler);
const call$1 = newRuntimeCaller(objectNames.ContextMenu);
const call$2 = newRuntimeCaller(objectNames.ContextMenu);
const ContextMenuOpen = 0;
function openContextMenu(id, x, y, data) {
void call$1(ContextMenuOpen, { id, x, y, data });
void call$2(ContextMenuOpen, { id, x, y, data });
}
function contextMenuHandler(event) {
const target = eventTarget(event);
@ -1138,7 +1138,7 @@ if (promiseWithResolvers && typeof promiseWithResolvers === "function") {
window._wails = window._wails || {};
window._wails.callResultHandler = resultHandler;
window._wails.callErrorHandler = errorHandler;
const call = newRuntimeCaller(objectNames.Call);
const call$1 = newRuntimeCaller(objectNames.Call);
const cancelCall = newRuntimeCaller(objectNames.CancelCall);
const callResponses = /* @__PURE__ */ new Map();
const CallBinding = 0;
@ -1224,7 +1224,7 @@ function Call(options) {
const id = generateID();
const result = CancellablePromise.withResolvers();
callResponses.set(id, { resolve: result.resolve, reject: result.reject });
const request = call(CallBinding, Object.assign({ "call-id": id }, options));
const request = call$1(CallBinding, Object.assign({ "call-id": id }, options));
let running = false;
request.then(() => {
running = true;
@ -1283,7 +1283,8 @@ function listenerOff(listener) {
}
window._wails = window._wails || {};
window._wails.dispatchWailsEvent = dispatchWailsEvent;
newRuntimeCaller(objectNames.Events);
const call = newRuntimeCaller(objectNames.Events);
const EmitMethod = 0;
class WailsEvent {
constructor(name, data = null) {
this.name = name;
@ -1316,6 +1317,9 @@ function OnMultiple(eventName, callback, maxCallbacks) {
function On(eventName, callback) {
return OnMultiple(eventName, callback, -1);
}
function Emit(event) {
return call(EmitMethod, event);
}
window._wails = window._wails || {};
window._wails.invoke = invoke;
invoke("wails:runtime:ready");
@ -1329,6 +1333,8 @@ function SetBadge(label) {
}
const setButton = document.getElementById("set");
const removeButton = document.getElementById("remove");
const setButtonUsingGo = document.getElementById("set-go");
const removeButtonUsingGo = document.getElementById("remove-go");
const labelElement = document.getElementById("label");
const timeElement = document.getElementById("time");
setButton.addEventListener("click", () => {
@ -1338,6 +1344,16 @@ setButton.addEventListener("click", () => {
removeButton.addEventListener("click", () => {
RemoveBadge();
});
setButtonUsingGo.addEventListener("click", () => {
let label = labelElement.value;
void Emit({
name: "set:badge",
data: label
});
});
removeButtonUsingGo.addEventListener("click", () => {
void Emit({ name: "remove:badge", data: null });
});
On("time", (time) => {
timeElement.innerText = time.data;
});

View file

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="/style.css"/>
<title>Wails App</title>
<script type="module" crossorigin src="/assets/index-BguIgRNQ.js"></script>
<script type="module" crossorigin src="/assets/index-sXwpgKSV.js"></script>
</head>
<body>
<div class="container">
@ -25,6 +25,8 @@
<input class="input" id="label" type="text" autocomplete="off"/>
<button class="btn" id="set">Set</button>
<button class="btn" id="remove">Remove</button>
<button class="btn" id="set-go">Set using Go</button>
<button class="btn" id="remove-go">Remove using Go</button>
</div>
</div>
<div class="footer">

View file

@ -43,8 +43,6 @@ a:hover {
}
button {
width: 60px;
height: 30px;
line-height: 30px;
border-radius: 3px;
border: none;

View file

@ -24,6 +24,8 @@
<input class="input" id="label" type="text" autocomplete="off"/>
<button class="btn" id="set">Set</button>
<button class="btn" id="remove">Remove</button>
<button class="btn" id="set-go">Set using Go</button>
<button class="btn" id="remove-go">Remove using Go</button>
</div>
</div>
<div class="footer">

View file

@ -43,8 +43,6 @@ a:hover {
}
button {
width: 60px;
height: 30px;
line-height: 30px;
border-radius: 3px;
border: none;

View file

@ -1,8 +1,10 @@
import {Events} from "@wailsio/runtime";
import {SetBadge, RemoveBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service";
import {SetBadge, RemoveBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service";
const setButton = document.getElementById('set')! as HTMLButtonElement;
const removeButton = document.getElementById('remove')! as HTMLButtonElement;
const setButtonUsingGo = document.getElementById('set-go')! as HTMLButtonElement;
const removeButtonUsingGo = document.getElementById('remove-go')! as HTMLButtonElement;
const labelElement : HTMLInputElement = document.getElementById('label')! as HTMLInputElement;
const timeElement = document.getElementById('time')! as HTMLDivElement;
@ -15,6 +17,19 @@ removeButton.addEventListener('click', () => {
RemoveBadge();
});
setButtonUsingGo.addEventListener('click', () => {
let label = (labelElement as HTMLInputElement).value
void Events.Emit({
name: "set:badge",
data: label,
})
})
removeButtonUsingGo.addEventListener('click', () => {
void Events.Emit({name:"remove:badge", data: null})
})
Events.On('time', (time: {data: any}) => {
timeElement.innerText = time.data;
});

View file

@ -27,11 +27,14 @@ func main() {
// 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files.
// 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances.
// 'Mac' options tailor the application when running an macOS.
badgeService := badge.New()
app := application.New(application.Options{
Name: "badge",
Description: "A demo of using raw HTML & CSS",
Services: []application.Service{
application.NewService(badge.New()),
application.NewService(badgeService),
},
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
@ -57,6 +60,21 @@ func main() {
URL: "/",
})
app.OnEvent("remove:badge", func(event *application.CustomEvent) {
err := badgeService.RemoveBadge()
if err != nil {
log.Fatal(err)
}
})
app.OnEvent("set:badge", func(event *application.CustomEvent) {
text := event.Data.(string)
err := badgeService.SetBadge(text)
if err != nil {
log.Fatal(err)
}
})
// Create a goroutine that emits an event containing the current time every second.
// The frontend can listen to this event and update the UI accordingly.
go func() {

View file

@ -35,13 +35,13 @@ require (
github.com/samber/lo v1.49.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/go-webview2 v1.0.21 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.31.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

View file

@ -138,6 +138,7 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf
github.com/wailsapp/go-webview2 v1.0.9 h1:lrU+q0cf1wgLdR69rN+ZnRtMJNaJRrcQ4ELxoO7/xjs=
github.com/wailsapp/go-webview2 v1.0.9/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
@ -153,6 +154,7 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
@ -167,6 +169,7 @@ golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -191,6 +194,7 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=

View file

@ -49,7 +49,6 @@ 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
)
@ -137,7 +136,7 @@ require (
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac
golang.org/x/image v0.24.0 // indirect
golang.org/x/image v0.24.0
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0 // indirect

View file

@ -117,8 +117,6 @@ 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=

View file

@ -5,15 +5,17 @@ package badge
import (
"bytes"
"context"
"fmt"
"golang.org/x/sys/windows/registry"
"image"
"image/color"
"image/png"
"os"
"path/filepath"
"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"
@ -108,9 +110,10 @@ func (t *ITaskbarList3) SetOverlayIcon(hwnd syscall.Handle, hIcon syscall.Handle
}
type windowsBadge struct {
taskbar *ITaskbarList3
badgeImg *image.RGBA
badgeSize int
taskbar *ITaskbarList3
badgeImg *image.RGBA
badgeSize int
fontManager *FontManager
}
func New() *Service {
@ -125,6 +128,7 @@ func (w *windowsBadge) Startup(ctx context.Context, options application.ServiceO
return err
}
w.taskbar = taskbar
w.fontManager = NewFontManager()
return nil
}
@ -227,12 +231,16 @@ func (w *windowsBadge) createBadgeIcon() (w32.HICON, error) {
}
func (w *windowsBadge) createBadgeIconWithText(label string) (w32.HICON, error) {
var err 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
fontPath, err = w.fontManager.FindFont("segoeuib.ttf")
if err != nil {
return 0, err
}
if fontPath == "" {
fontPath, err = w.fontManager.FindFont("arialbd.ttf")
if err != nil {
return 0, err
}
}
if fontPath == "" {
@ -304,3 +312,118 @@ func (w *windowsBadge) createBadge() {
w.badgeImg = img
}
// GetInstalledFonts returns a slice of strings containing paths to all installed fonts on Windows
func GetInstalledFonts() ([]string, error) {
var fontPaths []string
var fontDirs = []string{
filepath.Join(os.Getenv("windir"), "Fonts"),
filepath.Join(os.Getenv("localappdata"), "Microsoft", "Windows", "Fonts"),
}
// Check system fonts from registry
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts`, registry.QUERY_VALUE)
if err != nil {
return nil, fmt.Errorf("error opening system font registry key: %v", err)
}
defer k.Close()
// Add system fonts
valueNames, err := k.ReadValueNames(0)
if err != nil {
return nil, fmt.Errorf("error reading system font registry values: %v", err)
}
systemFontDir := fontDirs[0]
for _, name := range valueNames {
value, _, err := k.GetStringValue(name)
if err != nil {
continue
}
// If value doesn't contain path separator, assume it's in the Windows font directory
if !strings.Contains(value, "\\") {
value = filepath.Join(systemFontDir, value)
}
if fileExists(value) {
fontPaths = append(fontPaths, value)
}
}
// Check user fonts from registry
userK, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts`, registry.QUERY_VALUE)
if err == nil { // It's okay if this fails, some users might not have custom fonts
defer userK.Close()
userValueNames, err := userK.ReadValueNames(0)
if err == nil {
userFontDir := fontDirs[1]
for _, name := range userValueNames {
value, _, err := userK.GetStringValue(name)
if err != nil {
continue
}
// If value doesn't contain path separator, assume it's in the user font directory
if !strings.Contains(value, "\\") {
value = filepath.Join(userFontDir, value)
}
if fileExists(value) {
fontPaths = append(fontPaths, value)
}
}
}
}
// Also check the font directories directly for any fonts not in the registry
for _, dir := range fontDirs {
if !dirExists(dir) {
continue
}
files, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, file := range files {
if file.IsDir() {
continue
}
ext := strings.ToLower(filepath.Ext(file.Name()))
// Common font extensions
if ext == ".ttf" || ext == ".otf" || ext == ".ttc" || ext == ".fon" || ext == ".fnt" {
fontPath := filepath.Join(dir, file.Name())
// Check if this path is already in our list
if !contains(fontPaths, fontPath) {
fontPaths = append(fontPaths, fontPath)
}
}
}
}
return fontPaths, nil
}
// Helper functions
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
func dirExists(path string) bool {
info, err := os.Stat(path)
return err == nil && info.IsDir()
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if strings.EqualFold(s, item) {
return true
}
}
return false
}

View file

@ -0,0 +1,133 @@
package badge
import (
"errors"
"golang.org/x/sys/windows/registry"
"os"
"path/filepath"
"strings"
"sync"
)
// FontManager handles font discovery on Windows with minimal caching
type FontManager struct {
fontCache map[string]string // Maps only requested font filenames to paths
fontDirs []string // Directories to search for fonts
mu sync.RWMutex // Mutex for thread-safe access to the cache
registryPaths []string // Registry paths to search for fonts
}
// NewFontManager creates a new FontManager instance
func NewFontManager() *FontManager {
return &FontManager{
fontCache: make(map[string]string),
fontDirs: []string{
filepath.Join(os.Getenv("windir"), "Fonts"),
filepath.Join(os.Getenv("localappdata"), "Microsoft", "Windows", "Fonts"),
},
registryPaths: []string{
`SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts`,
},
}
}
// FindFont searches for a font by filename and returns its full path
// Only caches fonts that are found
func (fm *FontManager) FindFont(fontFilename string) (string, error) {
fontKey := strings.ToLower(fontFilename)
// Check if already in cache
fm.mu.RLock()
if path, exists := fm.fontCache[fontKey]; exists {
fm.mu.RUnlock()
return path, nil
}
fm.mu.RUnlock()
// If not in cache, search for the font
fontPath, err := fm.searchForFont(fontFilename)
if err != nil {
return "", err
}
// Add to cache only if found
fm.mu.Lock()
fm.fontCache[fontKey] = fontPath
fm.mu.Unlock()
return fontPath, nil
}
// searchForFont looks for a font in all known locations
func (fm *FontManager) searchForFont(fontFilename string) (string, error) {
fontFileLower := strings.ToLower(fontFilename)
// 1. Direct file check in font directories (fastest approach)
for _, dir := range fm.fontDirs {
fontPath := filepath.Join(dir, fontFilename)
if fileExists(fontPath) {
return fontPath, nil
}
}
// 2. Search in registry (can find fonts with different paths)
// System fonts (HKEY_LOCAL_MACHINE)
for _, regPath := range fm.registryPaths {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, regPath, registry.QUERY_VALUE)
if err == nil {
defer k.Close()
// Look for the specific font in registry values
fontPath, found := fm.findFontInRegistry(k, fontFileLower, fm.fontDirs[0])
if found {
return fontPath, nil
}
}
}
// 3. User fonts (HKEY_CURRENT_USER)
for _, regPath := range fm.registryPaths {
k, err := registry.OpenKey(registry.CURRENT_USER, regPath, registry.QUERY_VALUE)
if err == nil {
defer k.Close()
// Look for the specific font in registry values
fontPath, found := fm.findFontInRegistry(k, fontFileLower, fm.fontDirs[1])
if found {
return fontPath, nil
}
}
}
return "", errors.New("font not found: " + fontFilename)
}
// findFontInRegistry searches for a specific font in a registry key
func (fm *FontManager) findFontInRegistry(k registry.Key, fontFileLower string, defaultDir string) (string, bool) {
valueNames, err := k.ReadValueNames(0)
if err != nil {
return "", false
}
for _, name := range valueNames {
value, _, err := k.GetStringValue(name)
if err != nil {
continue
}
// Check if this registry entry corresponds to our font
valueLower := strings.ToLower(value)
if strings.HasSuffix(valueLower, fontFileLower) {
// If it's a relative path, assume it's in the default font directory
if !strings.Contains(value, "\\") {
value = filepath.Join(defaultDir, value)
}
if fileExists(value) {
return value, true
}
}
}
return "", false
}