font.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. package painter
  2. import (
  3. "bytes"
  4. "image/color"
  5. "image/draw"
  6. "math"
  7. "strings"
  8. "sync"
  9. "github.com/go-text/render"
  10. "github.com/go-text/typesetting/di"
  11. "github.com/go-text/typesetting/font"
  12. "github.com/go-text/typesetting/shaping"
  13. "golang.org/x/image/math/fixed"
  14. "fyne.io/fyne/v2"
  15. "fyne.io/fyne/v2/internal/cache"
  16. "fyne.io/fyne/v2/theme"
  17. )
  18. const (
  19. // DefaultTabWidth is the default width in spaces
  20. DefaultTabWidth = 4
  21. fontTabSpaceSize = 10
  22. )
  23. // CachedFontFace returns a Font face held in memory. These are loaded from the current theme.
  24. func CachedFontFace(style fyne.TextStyle, fontDP float32, texScale float32) *FontCacheItem {
  25. val, ok := fontCache.Load(style)
  26. if !ok {
  27. var f1, f2 font.Face
  28. switch {
  29. case style.Monospace:
  30. f1 = loadMeasureFont(theme.TextMonospaceFont())
  31. f2 = loadMeasureFont(theme.DefaultTextMonospaceFont())
  32. case style.Bold:
  33. if style.Italic {
  34. f1 = loadMeasureFont(theme.TextBoldItalicFont())
  35. f2 = loadMeasureFont(theme.DefaultTextBoldItalicFont())
  36. } else {
  37. f1 = loadMeasureFont(theme.TextBoldFont())
  38. f2 = loadMeasureFont(theme.DefaultTextBoldFont())
  39. }
  40. case style.Italic:
  41. f1 = loadMeasureFont(theme.TextItalicFont())
  42. f2 = loadMeasureFont(theme.DefaultTextItalicFont())
  43. case style.Symbol:
  44. f1 = loadMeasureFont(theme.SymbolFont())
  45. f2 = loadMeasureFont(theme.DefaultSymbolFont())
  46. default:
  47. f1 = loadMeasureFont(theme.TextFont())
  48. f2 = loadMeasureFont(theme.DefaultTextFont())
  49. }
  50. if f1 == nil {
  51. f1 = f2
  52. }
  53. faces := []font.Face{f1, f2}
  54. if emoji := theme.DefaultEmojiFont(); emoji != nil {
  55. faces = append(faces, loadMeasureFont(emoji))
  56. }
  57. val = &FontCacheItem{Fonts: faces}
  58. fontCache.Store(style, val)
  59. }
  60. return val.(*FontCacheItem)
  61. }
  62. // ClearFontCache is used to remove cached fonts in the case that we wish to re-load Font faces
  63. func ClearFontCache() {
  64. fontCache = &sync.Map{}
  65. }
  66. // DrawString draws a string into an image.
  67. func DrawString(dst draw.Image, s string, color color.Color, f []font.Face, fontSize, scale float32, tabWidth int) {
  68. r := render.Renderer{
  69. FontSize: fontSize,
  70. PixScale: scale,
  71. Color: color,
  72. }
  73. // TODO avoid shaping twice!
  74. sh := &shaping.HarfbuzzShaper{}
  75. out := sh.Shape(shaping.Input{
  76. Text: []rune(s),
  77. RunStart: 0,
  78. RunEnd: len(s),
  79. Face: f[0],
  80. Size: fixed.I(int(fontSize * r.PixScale)),
  81. })
  82. advance := float32(0)
  83. y := int(math.Ceil(float64(fixed266ToFloat32(out.LineBounds.Ascent))))
  84. walkString(f, s, float32ToFixed266(fontSize), tabWidth, &advance, scale, func(run shaping.Output, x float32) {
  85. if len(run.Glyphs) == 1 {
  86. if run.Glyphs[0].GlyphID == 0 {
  87. r.DrawStringAt(string([]rune{0xfffd}), dst, int(x), y, f[0])
  88. return
  89. }
  90. }
  91. r.DrawShapedRunAt(run, dst, int(x), y)
  92. })
  93. }
  94. func loadMeasureFont(data fyne.Resource) font.Face {
  95. loaded, err := font.ParseTTF(bytes.NewReader(data.Content()))
  96. if err != nil {
  97. fyne.LogError("font load error", err)
  98. return nil
  99. }
  100. return loaded
  101. }
  102. // MeasureString returns how far dot would advance by drawing s with f.
  103. // Tabs are translated into a dot location change.
  104. func MeasureString(f []font.Face, s string, textSize float32, tabWidth int) (size fyne.Size, advance float32) {
  105. return walkString(f, s, float32ToFixed266(textSize), tabWidth, &advance, 1, func(shaping.Output, float32) {})
  106. }
  107. // RenderedTextSize looks up how big a string would be if drawn on screen.
  108. // It also returns the distance from top to the text baseline.
  109. func RenderedTextSize(text string, fontSize float32, style fyne.TextStyle) (size fyne.Size, baseline float32) {
  110. size, base := cache.GetFontMetrics(text, fontSize, style)
  111. if base != 0 {
  112. return size, base
  113. }
  114. size, base = measureText(text, fontSize, style)
  115. cache.SetFontMetrics(text, fontSize, style, size, base)
  116. return size, base
  117. }
  118. func fixed266ToFloat32(i fixed.Int26_6) float32 {
  119. return float32(float64(i) / (1 << 6))
  120. }
  121. func float32ToFixed266(f float32) fixed.Int26_6 {
  122. return fixed.Int26_6(float64(f) * (1 << 6))
  123. }
  124. func measureText(text string, fontSize float32, style fyne.TextStyle) (fyne.Size, float32) {
  125. face := CachedFontFace(style, fontSize, 1)
  126. return MeasureString(face.Fonts, text, fontSize, style.TabWidth)
  127. }
  128. func tabStop(spacew, x float32, tabWidth int) float32 {
  129. if tabWidth <= 0 {
  130. tabWidth = DefaultTabWidth
  131. }
  132. tabw := spacew * float32(tabWidth)
  133. tabs, _ := math.Modf(float64((x + tabw) / tabw))
  134. return tabw * float32(tabs)
  135. }
  136. func walkString(faces []font.Face, s string, textSize fixed.Int26_6, tabWidth int, advance *float32, scale float32,
  137. cb func(run shaping.Output, x float32)) (size fyne.Size, base float32) {
  138. s = strings.ReplaceAll(s, "\r", "")
  139. runes := []rune(s)
  140. in := shaping.Input{
  141. Text: []rune{' '},
  142. RunStart: 0,
  143. RunEnd: 1,
  144. Direction: di.DirectionLTR,
  145. Face: faces[0],
  146. Size: textSize,
  147. }
  148. shaper := &shaping.HarfbuzzShaper{}
  149. out := shaper.Shape(in)
  150. in.Text = runes
  151. in.RunStart = 0
  152. in.RunEnd = len(runes)
  153. x := float32(0)
  154. spacew := scale * fontTabSpaceSize
  155. ins := shaping.SplitByFontGlyphs(in, faces)
  156. for _, in := range ins {
  157. inEnd := in.RunEnd
  158. pending := false
  159. for i, r := range in.Text[in.RunStart:in.RunEnd] {
  160. if r == '\t' {
  161. if pending {
  162. in.RunEnd = i
  163. out = shaper.Shape(in)
  164. x = shapeCallback(shaper, in, out, x, scale, cb)
  165. }
  166. x = tabStop(spacew, x, tabWidth)
  167. in.RunStart = i + 1
  168. in.RunEnd = inEnd
  169. pending = false
  170. } else {
  171. pending = true
  172. }
  173. }
  174. x = shapeCallback(shaper, in, out, x, scale, cb)
  175. }
  176. *advance = x
  177. return fyne.NewSize(*advance, fixed266ToFloat32(out.LineBounds.LineHeight())),
  178. fixed266ToFloat32(out.LineBounds.Ascent)
  179. }
  180. func shapeCallback(shaper shaping.Shaper, in shaping.Input, out shaping.Output, x, scale float32, cb func(shaping.Output, float32)) float32 {
  181. out = shaper.Shape(in)
  182. glyphs := out.Glyphs
  183. start := 0
  184. pending := false
  185. adv := fixed.I(0)
  186. for i, g := range out.Glyphs {
  187. if g.GlyphID == 0 {
  188. if pending {
  189. out.Glyphs = glyphs[start:i]
  190. cb(out, x)
  191. x += fixed266ToFloat32(adv) * scale
  192. adv = 0
  193. }
  194. out.Glyphs = glyphs[i : i+1]
  195. cb(out, x)
  196. x += fixed266ToFloat32(glyphs[i].XAdvance) * scale
  197. adv = 0
  198. start = i + 1
  199. pending = false
  200. } else {
  201. pending = true
  202. }
  203. adv += g.XAdvance
  204. }
  205. if pending {
  206. out.Glyphs = glyphs[start:]
  207. cb(out, x)
  208. x += fixed266ToFloat32(adv) * scale
  209. adv = 0
  210. }
  211. return x + fixed266ToFloat32(adv)*scale
  212. }
  213. type FontCacheItem struct {
  214. Fonts []font.Face
  215. }
  216. var fontCache = &sync.Map{} // map[fyne.TextStyle]*FontCacheItem