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
390 lines
9.4 KiB
Go
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)
|
|
}
|