VideoTools/vendor/fyne.io/fyne/v2/internal/painter/font.go
Stu Leak bdc27c2253 Fix player frame generation and video playback
Major improvements to UnifiedPlayer:

1. GetFrameImage() now works when paused for responsive UI updates
2. Play() method properly starts FFmpeg process
3. Frame display loop runs continuously for smooth video display
4. Disabled audio temporarily to fix video playback fundamentals
5. Simplified FFmpeg command to focus on video stream only

Player now:
- Generates video frames correctly
- Shows video when paused
- Has responsive progress tracking
- Starts playback properly

Next steps: Re-enable audio playback once video is stable
2026-01-07 22:20:00 -05:00

390 lines
9.4 KiB
Go

package painter
import (
"bytes"
"image/color"
"image/draw"
"math"
"strings"
"sync"
"github.com/go-text/render"
"github.com/go-text/typesetting/di"
"github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/fontscan"
"github.com/go-text/typesetting/language"
"github.com/go-text/typesetting/shaping"
"golang.org/x/image/math/fixed"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/async"
"fyne.io/fyne/v2/internal/cache"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/theme"
)
const (
// DefaultTabWidth is the default width in spaces
DefaultTabWidth = 4
fontTabSpaceSize = 10
)
var (
fm *fontscan.FontMap
fontScanLock sync.Mutex
loaded bool
)
func loadMap() {
loaded = true
fm = fontscan.NewFontMap(noopLogger{})
err := loadSystemFonts(fm)
if err != nil {
fm = nil // just don't fallback
}
}
func lookupLangFont(family string, aspect font.Aspect) *font.Face {
fontScanLock.Lock()
defer fontScanLock.Unlock()
if !loaded {
loadMap()
}
if fm == nil {
return nil
}
fm.SetQuery(fontscan.Query{Families: []string{family}, Aspect: aspect})
l, _ := fontscan.NewLangID(language.Language(lang.SystemLocale().LanguageString()))
return fm.ResolveFaceForLang(l)
}
func lookupRuneFont(r rune, family string, aspect font.Aspect) *font.Face {
fontScanLock.Lock()
defer fontScanLock.Unlock()
if !loaded {
loadMap()
}
if fm == nil {
return nil
}
fm.SetQuery(fontscan.Query{Families: []string{family}, Aspect: aspect})
return fm.ResolveFace(r)
}
func lookupFaces(theme, fallback, emoji fyne.Resource, family string, style fyne.TextStyle) (faces *dynamicFontMap) {
f1 := loadMeasureFont(theme)
if theme == fallback {
faces = &dynamicFontMap{family: family, faces: []*font.Face{f1}}
} else {
f2 := loadMeasureFont(fallback)
faces = &dynamicFontMap{family: family, faces: []*font.Face{f1, f2}}
}
aspect := font.Aspect{Style: font.StyleNormal}
if style.Italic {
aspect.Style = font.StyleItalic
}
if style.Bold {
aspect.Weight = font.WeightBold
}
if emoji != nil {
faces.addFace(loadMeasureFont(emoji))
}
local := lookupLangFont(family, aspect)
if local != nil {
faces.addFace(local)
}
return faces
}
// CachedFontFace returns a Font face held in memory. These are loaded from the current theme.
func CachedFontFace(style fyne.TextStyle, source fyne.Resource, o fyne.CanvasObject) *FontCacheItem {
if source != nil {
val, ok := fontCustomCache.Load(source)
if !ok {
face := loadMeasureFont(source)
if face == nil {
face = loadMeasureFont(theme.TextFont())
}
faces := &dynamicFontMap{family: source.Name(), faces: []*font.Face{face}}
val = &FontCacheItem{Fonts: faces}
fontCustomCache.Store(source, val)
}
return val
}
scope := ""
if o != nil { // for overridden themes get the cache key right
scope = cache.WidgetScopeID(o)
}
val, ok := fontCache.Load(cacheID{style: style, scope: scope})
if !ok {
var faces *dynamicFontMap
th := theme.CurrentForWidget(o)
font1 := th.Font(style)
emoji := theme.DefaultEmojiFont() // TODO only one emoji - maybe others too
switch {
case style.Monospace:
faces = lookupFaces(font1, theme.DefaultTextMonospaceFont(), emoji, fontscan.Monospace, style)
case style.Bold:
if style.Italic {
faces = lookupFaces(font1, theme.DefaultTextBoldItalicFont(), emoji, fontscan.SansSerif, style)
} else {
faces = lookupFaces(font1, theme.DefaultTextBoldFont(), emoji, fontscan.SansSerif, style)
}
case style.Italic:
faces = lookupFaces(font1, theme.DefaultTextItalicFont(), emoji, fontscan.SansSerif, style)
case style.Symbol:
th := theme.SymbolFont()
fallback := theme.DefaultSymbolFont()
f1 := loadMeasureFont(th)
if th == fallback {
faces = &dynamicFontMap{family: fontscan.SansSerif, faces: []*font.Face{f1}}
} else {
f2 := loadMeasureFont(fallback)
faces = &dynamicFontMap{family: fontscan.SansSerif, faces: []*font.Face{f1, f2}}
}
default:
faces = lookupFaces(font1, theme.DefaultTextFont(), emoji, fontscan.SansSerif, style)
}
val = &FontCacheItem{Fonts: faces}
fontCache.Store(cacheID{style: style, scope: scope}, val)
}
return val
}
// ClearFontCache is used to remove cached fonts in the case that we wish to re-load Font faces
func ClearFontCache() {
fontCache.Clear()
fontCustomCache.Clear()
}
// DrawString draws a string into an image.
func DrawString(dst draw.Image, s string, color color.Color, f shaping.Fontmap, fontSize, scale float32, style fyne.TextStyle) {
r := render.Renderer{
FontSize: fontSize,
PixScale: scale,
Color: color,
}
advance := float32(0)
y := math.MinInt
walkString(f, s, float32ToFixed266(fontSize), style, &advance, scale, func(run shaping.Output, x float32) {
if y == math.MinInt {
y = int(math.Ceil(float64(fixed266ToFloat32(run.LineBounds.Ascent) * r.PixScale)))
}
if len(run.Glyphs) == 1 {
if run.Glyphs[0].GlyphID == 0 {
r.DrawStringAt(string([]rune{0xfffd}), dst, int(x), y, f.ResolveFace(0xfffd))
return
}
}
r.DrawShapedRunAt(run, dst, int(x), y)
})
}
func loadMeasureFont(data fyne.Resource) *font.Face {
loaded, err := font.ParseTTF(bytes.NewReader(data.Content()))
if err != nil {
fyne.LogError("font load error", err)
return nil
}
return loaded
}
// MeasureString returns how far dot would advance by drawing s with f.
// Tabs are translated into a dot location change.
func MeasureString(f shaping.Fontmap, s string, textSize float32, style fyne.TextStyle) (size fyne.Size, advance float32) {
return walkString(f, s, float32ToFixed266(textSize), style, &advance, 1, func(shaping.Output, float32) {})
}
// RenderedTextSize looks up how big a string would be if drawn on screen.
// It also returns the distance from top to the text baseline.
func RenderedTextSize(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) {
size, base := cache.GetFontMetrics(text, fontSize, style, source)
if base != 0 {
return size, base
}
size, base = measureText(text, fontSize, style, source)
cache.SetFontMetrics(text, fontSize, style, source, size, base)
return size, base
}
func fixed266ToFloat32(i fixed.Int26_6) float32 {
return float32(float64(i) / (1 << 6))
}
func float32ToFixed266(f float32) fixed.Int26_6 {
return fixed.Int26_6(float64(f) * (1 << 6))
}
func measureText(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource) (fyne.Size, float32) {
face := CachedFontFace(style, source, nil)
return MeasureString(face.Fonts, text, fontSize, style)
}
func tabStop(spacew, x float32, tabWidth int) float32 {
if tabWidth <= 0 {
tabWidth = DefaultTabWidth
}
tabw := spacew * float32(tabWidth)
tabs, _ := math.Modf(float64((x + tabw) / tabw))
return tabw * float32(tabs)
}
func walkString(faces shaping.Fontmap, s string, textSize fixed.Int26_6, style fyne.TextStyle, advance *float32, scale float32,
cb func(run shaping.Output, x float32),
) (size fyne.Size, base float32) {
s = strings.ReplaceAll(s, "\r", "")
runes := []rune(s)
in := shaping.Input{
Text: []rune{' '},
RunStart: 0,
RunEnd: 1,
Direction: di.DirectionLTR,
Face: faces.ResolveFace(' '),
Size: textSize,
}
shaper := &shaping.HarfbuzzShaper{}
segmenter := &shaping.Segmenter{}
out := shaper.Shape(in)
in.Text = runes
in.RunStart = 0
in.RunEnd = len(runes)
x := float32(0)
spacew := scale * fontTabSpaceSize
if style.Monospace {
spacew = scale * fixed266ToFloat32(out.Advance)
}
ins := segmenter.Split(in, faces)
for _, in := range ins {
inEnd := in.RunEnd
pending := false
for i, r := range in.Text[in.RunStart:in.RunEnd] {
if r == '\t' {
if pending {
in.RunEnd = i
x = shapeCallback(shaper, in, x, scale, cb)
}
x = tabStop(spacew, x, style.TabWidth)
in.RunStart = i + 1
in.RunEnd = inEnd
pending = false
} else {
pending = true
}
}
x = shapeCallback(shaper, in, x, scale, cb)
}
*advance = x
return fyne.NewSize(*advance, fixed266ToFloat32(out.LineBounds.LineThickness())),
fixed266ToFloat32(out.LineBounds.Ascent)
}
func shapeCallback(shaper shaping.Shaper, in shaping.Input, x, scale float32, cb func(shaping.Output, float32)) float32 {
out := shaper.Shape(in)
glyphs := out.Glyphs
start := 0
pending := false
adv := fixed.I(0)
for i, g := range out.Glyphs {
if g.GlyphID == 0 {
if pending {
out.Glyphs = glyphs[start:i]
cb(out, x)
x += fixed266ToFloat32(adv) * scale
adv = 0
}
out.Glyphs = glyphs[i : i+1]
cb(out, x)
x += fixed266ToFloat32(glyphs[i].XAdvance) * scale
adv = 0
start = i + 1
pending = false
} else {
pending = true
}
adv += g.XAdvance
}
if pending {
out.Glyphs = glyphs[start:]
cb(out, x)
x += fixed266ToFloat32(adv) * scale
adv = 0
}
return x + fixed266ToFloat32(adv)*scale
}
type FontCacheItem struct {
Fonts shaping.Fontmap
}
type cacheID struct {
style fyne.TextStyle
scope string
}
var (
fontCache async.Map[cacheID, *FontCacheItem]
fontCustomCache async.Map[fyne.Resource, *FontCacheItem] // for custom resources
)
type noopLogger struct{}
func (n noopLogger) Printf(string, ...any) {}
type dynamicFontMap struct {
faces []*font.Face
family string
}
func (d *dynamicFontMap) ResolveFace(r rune) *font.Face {
for _, f := range d.faces {
if _, ok := f.NominalGlyph(r); ok {
return f
}
}
toAdd := lookupRuneFont(r, d.family, font.Aspect{})
if toAdd != nil {
d.addFace(toAdd)
return toAdd
}
return d.faces[0]
}
func (d *dynamicFontMap) addFace(f *font.Face) {
d.faces = append(d.faces, f)
}