Files
MyGoNavi/internal/app/methods_fonts.go
Syngnat a7f8ce36df feat(font): 新增系统字体枚举与全局字体配置能力
- 新增 Go 侧已安装字体扫描接口,支持前端读取系统真实字体列表
- 接入 Wails 字体查询导出,补齐 App.d.ts 与 App.js 调用声明
- 新增字体选项构建与匹配工具,区分 UI 字体与等宽字体候选
- 外观设置支持按平台加载字体列表,并支持搜索匹配与默认字体回退
- Store 增加自定义 UI 字体与代码字体配置,持久化全局字体选择
2026-05-29 14:41:56 +08:00

173 lines
3.6 KiB
Go

package app
import (
"io/fs"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"GoNavi-Wails/internal/connection"
"golang.org/x/image/font/sfnt"
)
const maxScannedFontFiles = 4096
type installedFontFamily struct {
Family string `json:"family"`
Path string `json:"path,omitempty"`
}
func (a *App) ListInstalledFontFamilies() connection.QueryResult {
families, err := listInstalledFontFamilies()
if err != nil {
return connection.QueryResult{
Success: false,
Message: err.Error(),
}
}
return connection.QueryResult{
Success: true,
Data: families,
}
}
func listInstalledFontFamilies() ([]installedFontFamily, error) {
fontPaths := resolveSystemFontDirs()
if len(fontPaths) == 0 {
return []installedFontFamily{}, nil
}
seenDirs := make(map[string]struct{}, len(fontPaths))
familyByName := make(map[string]installedFontFamily)
scannedFiles := 0
for _, rawDir := range fontPaths {
dir := strings.TrimSpace(rawDir)
if dir == "" {
continue
}
absDir, err := filepath.Abs(dir)
if err != nil {
absDir = dir
}
cleanDir := filepath.Clean(absDir)
if _, ok := seenDirs[cleanDir]; ok {
continue
}
seenDirs[cleanDir] = struct{}{}
info, err := os.Stat(cleanDir)
if err != nil || !info.IsDir() {
continue
}
walkErr := filepath.WalkDir(cleanDir, func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil {
return nil
}
if d.IsDir() {
return nil
}
if scannedFiles >= maxScannedFontFiles {
return fs.SkipAll
}
if !isFontFile(path) {
return nil
}
scannedFiles++
family := readFontFamilyName(path)
if family == "" {
return nil
}
if _, exists := familyByName[family]; exists {
return nil
}
familyByName[family] = installedFontFamily{
Family: family,
Path: path,
}
return nil
})
if walkErr == fs.SkipAll {
break
}
}
result := make([]installedFontFamily, 0, len(familyByName))
for _, item := range familyByName {
result = append(result, item)
}
sort.Slice(result, func(i, j int) bool {
return strings.ToLower(result[i].Family) < strings.ToLower(result[j].Family)
})
return result, nil
}
func resolveSystemFontDirs() []string {
switch runtime.GOOS {
case "darwin":
home, _ := os.UserHomeDir()
return []string{
"/System/Library/Fonts",
"/Library/Fonts",
filepath.Join(home, "Library", "Fonts"),
}
case "windows":
winDir := os.Getenv("WINDIR")
localAppData := os.Getenv("LOCALAPPDATA")
return []string{
filepath.Join(winDir, "Fonts"),
filepath.Join(localAppData, "Microsoft", "Windows", "Fonts"),
}
default:
home, _ := os.UserHomeDir()
return []string{
"/usr/share/fonts",
"/usr/local/share/fonts",
filepath.Join(home, ".fonts"),
filepath.Join(home, ".local", "share", "fonts"),
}
}
}
func isFontFile(path string) bool {
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".ttf", ".otf", ".ttc", ".otc":
return true
default:
return false
}
}
func readFontFamilyName(path string) string {
file, err := os.Open(path)
if err != nil {
return ""
}
defer func() { _ = file.Close() }()
collection, err := sfnt.ParseCollectionReaderAt(file)
if err != nil {
return ""
}
var buf sfnt.Buffer
fontCount := collection.NumFonts()
for i := range fontCount {
font, fontErr := collection.Font(i)
if fontErr != nil {
continue
}
for _, nameID := range []sfnt.NameID{sfnt.NameIDTypographicFamily, sfnt.NameIDFamily} {
family, nameErr := font.Name(&buf, nameID)
if nameErr == nil && strings.TrimSpace(family) != "" {
return strings.TrimSpace(family)
}
}
}
return ""
}