Compare commits

...
17 Commits
Author SHA1 Message Date
Jean-André Santoni d535c9034d Width should have no side effects 2026-05-17 16:07:42 +02:00
Jean-André Santoni 9a5746ad6f Fix 2026-05-17 16:00:14 +02:00
Jean-André Santoni 2c9b3785ef Fill atlas gradually 2026-05-17 15:51:06 +02:00
Jean-André Santoni 46274166ef Use 3 font sizes 2026-05-17 14:53:32 +02:00
Jean-André Santoni dffa1cca74 Merge remote-tracking branch 'origin/master' into kivutar/utf8 2026-05-17 14:28:23 +02:00
Jean-André Santoni aa687155bc Fix glyph bounds 2026-05-06 12:53:48 +02:00
Jean-André Santoni b0281fe2ba Tweak font rendering 2026-05-06 12:12:52 +02:00
Jean-André Santoni 8c81a1f89b Another way to place the glyph. Also remove mipmaps 2026-05-02 15:08:42 +02:00
Jean-André Santoni 55cc8436d7 Handle more of CJK range 2026-05-02 14:36:16 +02:00
Jean-André Santoni c3e1f90e33 Merge remote-tracking branch 'origin/master' into kivutar/utf8 2026-05-02 14:25:53 +02:00
Jean-André Santoni b82d4c1acb Merge remote-tracking branch 'origin/master' into kivutar/utf8 2026-05-02 14:25:37 +02:00
Jean-André Santoni 988afe9bbb Add special characters 2026-04-11 10:43:56 +02:00
Jean-André Santoni f1d2ed0896 Merge remote-tracking branch 'origin/master' into kivutar/utf8 2026-04-11 10:42:57 +02:00
Jean-André Santoni 5352ab2bca Display kanjis 2026-01-05 20:28:09 +01:00
Jean-André Santoni ea3873befa Options 2025-08-10 23:51:38 +07:00
Jean-André Santoni 65c7f62eb0 Try to display more stuff 2025-08-10 23:41:01 +07:00
Jean-André Santoni 9cc51e50ff UTF-8 2025-08-10 22:58:32 +07:00
16 changed files with 407 additions and 182 deletions

No files matched your search

+4 -4
View File
@@ -9,16 +9,16 @@ func stackHintLeft(stack *float32, icon uint32, label string, h int) {
menu.Font.SetColor(hintTextColor)
menu.DrawImage(icon, *stack, float32(h)-79*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, 0, hintTextColor)
*stack += 70 * menu.ratio
menu.Font.Print(*stack, float32(h)-30*menu.ratio, 0.5*menu.ratio, label)
*stack += menu.Font.Width(0.5*menu.ratio, label)
menu.Font.Print(*stack, float32(h)-30*menu.ratio, menu.ratio, label)
*stack += menu.Font.Width(menu.ratio, label)
*stack += 32 * menu.ratio
}
// Used to easily compose different hint bars based on the context.
func stackHintRight(stack *float32, icon uint32, label string, h int) {
*stack -= menu.Font.Width(0.5*menu.ratio, label)
*stack -= menu.Font.Width(menu.ratio, label)
menu.Font.SetColor(hintTextColor)
menu.Font.Print(*stack, float32(h)-30*menu.ratio, 0.5*menu.ratio, label)
menu.Font.Print(*stack, float32(h)-30*menu.ratio, menu.ratio, label)
*stack -= 70 * menu.ratio
menu.DrawImage(icon, *stack, float32(h)-79*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, 0, hintTextColor)
*stack -= 32 * menu.ratio
+1 -1
View File
@@ -143,7 +143,7 @@ func genericInput(list *entry, dt float32) {
// indexed allows jumping directly to the next letter in playlists
func indexed(list *entry, offset int) int {
curr := list.children[list.ptr].label[0]
curr := firstRune(list.children[list.ptr].label)
for i, t := range list.indexes {
if curr == t.Char {
if i+offset < 0 {
+2 -2
View File
@@ -20,7 +20,7 @@ func (m *Menu) RenderNotifications() {
fading = 1
}
offset := fading*h - h
lw := m.Font.Width(0.5*m.ratio, n.Message)
lw := m.Font.Width(m.ratio, n.Message)
fg := severityFgColor[n.Severity]
bg := severityBgColor[n.Severity]
m.DrawRect(
@@ -35,7 +35,7 @@ func (m *Menu) RenderNotifications() {
m.Font.Print(
45*m.ratio,
(stack+offset)*m.ratio,
0.5*m.ratio,
m.ratio,
n.Message,
)
stack += h + offset
+12 -5
View File
@@ -35,7 +35,7 @@ type entry struct {
children []entry // children entries
ptr int // index of the active child
indexes []struct {
Char byte
Char rune
Index int
}
}
@@ -53,6 +53,13 @@ type Scene interface {
Entry() *entry
}
func firstRune(s string) rune {
for _, r := range s {
return r
}
return 0
}
// genericSegueMount is the smooth transition of the menu entries first appearance
func genericSegueMount(list *entry) {
for i := range list.children {
@@ -198,21 +205,21 @@ func genericRender(list *entry) {
menu.Font.Print(
(670+1)*menu.ratio,
float32(h)*e.yp+fontOffset+1*menu.ratio,
0.5*menu.ratio, e.label)
menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
670*menu.ratio,
float32(h)*e.yp+fontOffset,
0.5*menu.ratio, e.label)
menu.ratio, e.label)
if e.widget != nil {
e.widget(&e)
} else if e.stringValue != nil {
lw := menu.Font.Width(0.5*menu.ratio, e.stringValue())
lw := menu.Font.Width(menu.ratio, e.stringValue())
menu.Font.Print(
float32(w)-lw-128*menu.ratio,
float32(h)*e.yp+fontOffset,
0.5*menu.ratio, e.stringValue())
menu.ratio, e.stringValue())
}
}
}
+2 -2
View File
@@ -28,11 +28,11 @@ func buildCoreOptions() Scene {
for _, v := range core.Options.Vars {
v := v
list.children = append(list.children, entry{
label: strings.Replace(v.Desc, "%", "%%", -1),
label: strings.ReplaceAll(v.Desc, "%", "%%"),
icon: "subsetting",
stringValue: func() string {
val := v.Choices[v.Choice]
return strings.Replace(val, "%", "%%", -1)
return "◀ " + strings.ReplaceAll(val, "%", "%%") + " ▶"
},
incr: func(direction int) {
v.Choice += direction
+12 -12
View File
@@ -70,16 +70,16 @@ func (s *sceneDialog) render() {
white,
)
menu.Font.SetColor(titleTextColor)
lw1 := menu.Font.Width(0.7*menu.ratio, s.title)
menu.Font.Print(fw/2-lw1/2, fh/2-120*menu.ratio+20*menu.ratio, 0.7*menu.ratio, s.title)
menu.FontLg.SetColor(titleTextColor)
lw1 := menu.FontLg.Width(menu.ratio, s.title)
menu.FontLg.Print(fw/2-lw1/2, fh/2-120*menu.ratio+20*menu.ratio, menu.ratio, s.title)
menu.Font.SetColor(dialogTextColor)
lw2 := menu.Font.Width(0.5*menu.ratio, s.line1)
menu.Font.Print(fw/2-lw2/2, fh/2-30*menu.ratio+20*menu.ratio, 0.5*menu.ratio, s.line1)
lw3 := menu.Font.Width(0.5*menu.ratio, s.line2)
menu.Font.Print(fw/2-lw3/2, fh/2+30*menu.ratio+20*menu.ratio, 0.5*menu.ratio, s.line2)
lw2 := menu.Font.Width(menu.ratio, s.line1)
menu.Font.Print(fw/2-lw2/2, fh/2-30*menu.ratio+20*menu.ratio, menu.ratio, s.line1)
lw3 := menu.Font.Width(menu.ratio, s.line2)
menu.Font.Print(fw/2-lw3/2, fh/2+30*menu.ratio+20*menu.ratio, menu.ratio, s.line2)
menu.Font.SetColor(dialogTextColor)
menu.FontSm.SetColor(dialogTextColor)
var margin float32 = 15
@@ -90,10 +90,10 @@ func (s *sceneDialog) render() {
fw/2-width/2*menu.ratio+margin*menu.ratio,
fh/2+height/2*menu.ratio-70*menu.ratio-margin*menu.ratio,
70*menu.ratio, 70*menu.ratio, 1.0, 0, dialogTextColor)
menu.Font.Print(
menu.FontSm.Print(
fw/2-width/2*menu.ratio+margin*menu.ratio+70*menu.ratio,
fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio,
0.4*menu.ratio,
menu.ratio,
"NO")
menu.DrawImage(
@@ -101,10 +101,10 @@ func (s *sceneDialog) render() {
fw/2+width/2*menu.ratio-150*menu.ratio-margin*menu.ratio,
fh/2+height/2*menu.ratio-70*menu.ratio-margin*menu.ratio,
70*menu.ratio, 70*menu.ratio, 1.0, 0, dialogTextColor)
menu.Font.Print(
menu.FontSm.Print(
fw/2+width/2*menu.ratio-150*menu.ratio-margin*menu.ratio+70*menu.ratio,
fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio,
0.4*menu.ratio,
menu.ratio,
"YES")
}
+1 -1
View File
@@ -75,7 +75,7 @@ func explorerIcon(f os.FileInfo) string {
func appendNode(list *sceneExplorer, fullPath string, name string, f os.FileInfo, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) {
// Check whether or not we are to display hidden files.
if name[:1] == "." && !settings.Current.ShowHiddenFiles {
if strings.HasPrefix(name, ".") && !settings.Current.ShowHiddenFiles {
return
}
+6 -6
View File
@@ -203,13 +203,13 @@ func (s *sceneHistory) render() {
menu.Font.Print(
(840+1)*menu.ratio,
float32(h)*e.yp+fontOffset-slOffset+1*menu.ratio,
0.5*menu.ratio, e.label)
menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset-slOffset,
0.5*menu.ratio, e.label)
stack += float32(int(menu.Font.Width(0.5*menu.ratio, e.label)))
menu.ratio, e.label)
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
stack += 10
for _, tag := range e.tags {
@@ -225,11 +225,11 @@ func (s *sceneHistory) render() {
}
}
menu.Font.SetColor(mutedTextColor.Alpha(e.subLabelAlpha))
menu.Font.Print(
menu.FontSm.SetColor(mutedTextColor.Alpha(e.subLabelAlpha))
menu.FontSm.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset+60*menu.ratio-slOffset,
0.5*menu.ratio, e.subLabel)
menu.ratio, e.subLabel)
}
}
+6 -6
View File
@@ -57,12 +57,12 @@ func buildPlaylist(path string) Scene {
// Index first letters of entries to allow quick jump to the next or previous
// letter
func buildIndexes(list *entry) {
var last byte
var last rune
for i := 0; i < len(list.children); i++ {
char := list.children[i].label[0]
char := firstRune(list.children[i].label)
if char != last {
list.indexes = append(list.indexes, struct {
Char byte
Char rune
Index int
}{char, i})
last = char
@@ -241,14 +241,14 @@ func (s *scenePlaylist) render() {
menu.Font.Print(
(840+1)*menu.ratio,
float32(h)*e.yp+fontOffset+1*menu.ratio,
0.5*menu.ratio, e.label)
menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset,
0.5*menu.ratio, e.label)
menu.ratio, e.label)
stack += float32(int(menu.Font.Width(0.5*menu.ratio, e.label)))
stack += float32(int(menu.Font.Width(menu.ratio, e.label)))
stack += 10
for _, tag := range e.tags {
+2 -2
View File
@@ -157,13 +157,13 @@ func (s *sceneSavestates) render() {
menu.Font.Print(
(840+1)*menu.ratio,
float32(h)*e.yp+fontOffset+1*menu.ratio,
0.5*menu.ratio, e.label)
menu.ratio, e.label)
menu.Font.SetColor(textColor.Alpha(e.labelAlpha))
menu.Font.Print(
840*menu.ratio,
float32(h)*e.yp+fontOffset,
0.5*menu.ratio, e.label)
menu.ratio, e.label)
}
}
}
+1 -1
View File
@@ -60,7 +60,7 @@ func buildSettings() Scene {
icon: "folder",
value: f.Value,
stringValue: func() string {
return "[" + utils.FileName(f.Value().(string)) + "]"
return "【" + utils.FileName(f.Value().(string)) + "】"
},
widget: widgets[f.Tag("widget")],
callbackOK: func() {
+8 -8
View File
@@ -298,16 +298,16 @@ func (tabs sceneTabs) render() {
stackWidth += e.width*menu.ratio + e.margin*menu.ratio
if e.labelAlpha > 0 {
lw := menu.Font.Width(0.5*menu.ratio, e.label)
lw := menu.Font.Width(menu.ratio, e.label)
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(250+1)*menu.ratio)), 0.5*menu.ratio, e.label)
menu.Font.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(250+1)*menu.ratio)), menu.ratio, e.label)
menu.Font.SetColor(lablColor.Alpha(e.labelAlpha))
menu.Font.Print(x-lw/2, float32(int(float32(h)/2+250*menu.ratio)), 0.5*menu.ratio, e.label)
lw = menu.Font.Width(0.4*menu.ratio, e.subLabel)
menu.Font.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.Font.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(330+1)*menu.ratio)), 0.4*menu.ratio, e.subLabel)
menu.Font.SetColor(lablColor.Alpha(e.labelAlpha))
menu.Font.Print(x-lw/2, float32(int(float32(h)/2+330*menu.ratio)), 0.4*menu.ratio, e.subLabel)
menu.Font.Print(x-lw/2, float32(int(float32(h)/2+250*menu.ratio)), menu.ratio, e.label)
lw = menu.FontSm.Width(menu.ratio, e.subLabel)
menu.FontSm.SetColor(textShadowColor.Alpha(e.labelAlpha / 2))
menu.FontSm.Print(x-lw/2+1*menu.ratio, float32(int(float32(h)/2+(330+1)*menu.ratio)), menu.ratio, e.subLabel)
menu.FontSm.SetColor(lablColor.Alpha(e.labelAlpha))
menu.FontSm.Print(x-lw/2, float32(int(float32(h)/2+330*menu.ratio)), menu.ratio, e.subLabel)
}
menu.DrawImage(menu.icons["hexagon"],
+3 -3
View File
@@ -22,10 +22,10 @@ import (
// Widget sets the graphical representation of the value.
type Settings struct {
VideoFullscreen bool `hide:"ludos" toml:"video_fullscreen" label:"Video Fullscreen" fmt:"%t" widget:"switch"`
VideoMonitorIndex int `toml:"video_monitor_index" label:"Video Monitor Index" fmt:"%d"`
VideoFilter string `toml:"video_filter" label:"Video Filter" fmt:"<%s>"`
VideoMonitorIndex int `toml:"video_monitor_index" label:"Video Monitor Index" fmt:"◀ %d ▶"`
VideoFilter string `toml:"video_filter" label:"Video Filter" fmt:"◀ %s ▶"`
VideoDarkMode bool `toml:"video_dark_mode" label:"Video Dark Mode" fmt:"%t" widget:"switch"`
VideoTheme string `toml:"video_theme" label:"Video Theme" fmt:"<%s>"`
VideoTheme string `toml:"video_theme" label:"Video Theme" fmt:"◀ %s ▶"`
AudioVolume float32 `toml:"audio_volume" label:"Audio Volume" fmt:"%.1f" widget:"range"`
+325 -128
View File
@@ -5,6 +5,7 @@ import (
"image"
"image/draw"
"io"
"log"
"os"
"github.com/go-gl/gl/v2.1/gl"
@@ -14,7 +15,7 @@ import (
"golang.org/x/image/math/fixed"
)
type character struct {
type glyph struct {
x, y int
width int // Glyph width
height int // Glyph height
@@ -33,9 +34,11 @@ const (
TopToBottom // E.g.: Chinese
)
const fontRenderScale = 1
// A Font allows rendering of text to an OpenGL context.
type Font struct {
fontChar []*character
glyphs map[rune]*glyph
vao uint32
vbo uint32
program uint32
@@ -43,19 +46,248 @@ type Font struct {
color Color
atlasWidth float32
atlasHeight float32
atlas *image.RGBA
face font.Face
ttf *truetype.Font
scale int32
lineHeight float32
margin int
nextX int
nextY int
}
type point [4]float32
func max(a, b float32) float32 {
if a > b {
return a
type glyphMetrics struct {
bounds fixed.Rectangle26_6
advance fixed.Int26_6
width int
height int
ascent int
descent int
}
func ceilFixed26_6(v fixed.Int26_6) int32 {
return int32((v + 63) >> 6)
}
func appendRange(runes []rune, start, end rune) []rune {
for r := start; r <= end; r++ {
runes = append(runes, r)
}
return b
return runes
}
func defaultRunes() []rune {
runes := make([]rune, 0, 512)
runes = appendRange(runes, 32, 126)
runes = append(runes,
'◀', '▶', '【', '】',
'「', '」', '『', '』',
'。', '、', '・', 'ー',
'!', '?',
'(', ')', '[', ']', '{', '}',
)
runes = appendRange(runes, 0xFF01, 0xFF0F) // fullwidth punctuation
runes = appendRange(runes, 0xFF10, 0xFF19) // fullwidth digits
runes = appendRange(runes, 0xFF1A, 0xFF20) // fullwidth : ; < = > ? @
runes = appendRange(runes, 0xFF21, 0xFF3A) // fullwidth A-Z
runes = appendRange(runes, 0xFF3B, 0xFF40) // fullwidth [ \ ] ^ _
runes = appendRange(runes, 0xFF41, 0xFF5A) // fullwidth a-z
runes = appendRange(runes, 0xFF5B, 0xFF65) // fullwidth { | } ~ and katakana punctuation
runes = appendRange(runes, 0x00A0, 0x017F)
runes = appendRange(runes, 0x3000, 0x303F)
runes = appendRange(runes, 0x3040, 0x30FF)
return runes
}
func getGlyphMetrics(face font.Face, ttf *truetype.Font, ch rune, scale int32) (glyphMetrics, error) {
gBnd, gAdv, ok := face.GlyphBounds(ch)
if !ok {
return glyphMetrics{}, fmt.Errorf("ttf face glyphBounds error")
}
gh := ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
gw := ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
// If gylph has no dimensions set to a max value
if gw == 0 || gh == 0 {
gBnd = ttf.Bounds(fixed.Int26_6(scale))
gw = ceilFixed26_6(gBnd.Max.X - gBnd.Min.X)
gh = ceilFixed26_6(gBnd.Max.Y - gBnd.Min.Y)
// Above can sometimes yield 0 for font smaller than 48pt, 1 is minimum
if gw == 0 || gh == 0 {
gw = 1
gh = 1
}
}
// The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y.
gAscent := int(ceilFixed26_6(-gBnd.Min.Y))
gdescent := int(ceilFixed26_6(gBnd.Max.Y))
return glyphMetrics{
bounds: gBnd,
advance: gAdv,
width: int(gw),
height: int(gh),
ascent: gAscent,
descent: gdescent,
}, nil
}
func wrapGlyph(x, y *int, glyphWidth, lineHeight, atlasWidth, margin int) {
if *x+glyphWidth+margin > atlasWidth {
*x = margin
*y += lineHeight + margin
}
}
func appendGlyph(face font.Face, ttf *truetype.Font, ch rune, x, y *int, lineHeight, atlasWidth float32, atlas *image.RGBA, fg *image.Uniform, scale int32, margin int) (*glyph, error) {
char := new(glyph)
metrics, err := getGlyphMetrics(face, ttf, ch, scale)
if err != nil {
return nil, err
}
wrapGlyph(x, y, metrics.width, int(lineHeight), int(atlasWidth), margin)
if *y+metrics.height+margin > atlas.Rect.Dy() {
return nil, fmt.Errorf("glyph atlas full")
}
// Set w,h and adv, bearing V and bearing H in char
char.x = *x
char.y = *y
char.width = metrics.width
char.height = metrics.height
char.advance = int(metrics.advance)
char.bearingV = metrics.descent
char.bearingH = (int(metrics.bounds.Min.X) >> 6)
clip := image.Rect(*x, *y, *x+metrics.width, *y+metrics.height)
// Create a freetype context for drawing
c := freetype.NewContext()
c.SetDPI(72)
c.SetFont(ttf)
c.SetFontSize(float64(scale))
c.SetClip(clip)
c.SetDst(atlas)
c.SetSrc(fg)
c.SetHinting(font.HintingFull)
// Set the glyph dot
px := 0 - (int(metrics.bounds.Min.X) >> 6) + *x
py := (metrics.ascent) + *y
pt := freetype.Pt(px, py)
// Draw the text from mask to image
_, err = c.DrawString(string(ch), pt)
*x += metrics.width + margin
return char, err
}
func getMaxTextureSize() int32 {
var maxTextureSize int32
gl.GetIntegerv(gl.MAX_TEXTURE_SIZE, &maxTextureSize)
if maxTextureSize == 0 {
maxTextureSize = 4096
}
return maxTextureSize
}
func calculateAtlasSize(face font.Face, ttf *truetype.Font, runes []rune, scale int32, margin int) (float32, float32, float32, error) {
maxTextureSize := getMaxTextureSize()
lineHeight := float32(face.Metrics().Height.Ceil())
if lineHeight < 1 {
lineHeight = 1
}
width := 2048
if width > int(maxTextureSize) {
width = int(maxTextureSize)
}
for {
x := margin
y := margin
maxY := 0
for _, r := range runes {
metrics, err := getGlyphMetrics(face, ttf, r, scale)
if err != nil {
continue
}
wrapGlyph(&x, &y, metrics.width, int(lineHeight), width, margin)
x += metrics.width + margin
if y+int(lineHeight) > maxY {
maxY = y + int(lineHeight)
}
}
height := maxY + margin
if height <= int(maxTextureSize) || width >= int(maxTextureSize) {
if height > int(maxTextureSize) {
return 0, 0, 0, fmt.Errorf("glyph atlas %dx%d exceeds max texture size %d", width, height, maxTextureSize)
}
return float32(width), float32(height), lineHeight, nil
}
width *= 2
if width > int(maxTextureSize) {
width = int(maxTextureSize)
}
}
}
func reserveAtlasHeight(baseHeight int, lineHeight float32, margin int, maxTextureSize int32) int {
height := baseHeight * 2
extraRowsHeight := baseHeight + 32*(int(lineHeight)+margin)
if extraRowsHeight > height {
height = extraRowsHeight
}
if height > int(maxTextureSize) {
height = int(maxTextureSize)
}
return height
}
func (f *Font) uploadGlyphRect(g *glyph) {
if f.atlas == nil || g == nil {
return
}
offset := g.y*f.atlas.Stride + g.x*4
if offset < 0 || offset >= len(f.atlas.Pix) {
return
}
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, int32(f.atlas.Stride/4))
gl.TexSubImage2D(
gl.TEXTURE_2D,
0,
int32(g.x),
int32(g.y),
int32(g.width),
int32(g.height),
gl.RGBA,
gl.UNSIGNED_BYTE,
gl.Ptr(f.atlas.Pix[offset:]),
)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, 0)
gl.BindTexture(gl.TEXTURE_2D, 0)
}
// LoadTrueTypeFont builds a set of textures based on a ttf files gylphs
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune, dir Direction) (*Font, error) {
func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, dir Direction) (*Font, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
@@ -69,120 +301,63 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune,
// Make Font stuct type
f := new(Font)
f.fontChar = make([]*character, 0, high-low+1)
f.glyphs = make(map[rune]*glyph)
f.program = program // Set shader program
f.SetColor(Color{R: 1, G: 1, B: 1, A: 1}) // Set default white
f.ttf = ttf
f.scale = scale
// Create new face
ttfFace := truetype.NewFace(ttf, &truetype.Options{
face := truetype.NewFace(ttf, &truetype.Options{
Size: float64(scale),
DPI: 72,
Hinting: font.HintingFull,
})
f.face = face
margin := 4
runes := defaultRunes()
var lineHeight float32
f.atlasWidth = 1024
f.atlasHeight = 1024
for ch := low; ch <= high; ch++ {
gBnd, _, ok := ttfFace.GlyphBounds(ch)
if !ok {
return nil, fmt.Errorf("ttf face glyphBounds error")
}
gh := int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
lineHeight = max(lineHeight, float32(gh))
f.atlasWidth, f.atlasHeight, lineHeight, err = calculateAtlasSize(face, ttf, runes, scale, margin)
if err != nil {
return nil, err
}
f.atlasHeight = float32(reserveAtlasHeight(int(f.atlasHeight), lineHeight, margin, getMaxTextureSize()))
f.lineHeight = lineHeight
f.margin = margin
// Create image to draw glyph
fg, bg := image.White, image.Black
rect := image.Rect(0, 0, int(f.atlasWidth), int(f.atlasHeight))
rgba := image.NewRGBA(rect)
draw.Draw(rgba, rgba.Bounds(), bg, image.Point{}, draw.Src)
atlas := image.NewRGBA(rect)
draw.Draw(atlas, atlas.Bounds(), bg, image.Point{}, draw.Src)
f.atlas = atlas
margin := 4
x := margin
y := margin
// Make each gylph
for ch := low; ch <= high; ch++ {
char := new(character)
gBnd, gAdv, ok := ttfFace.GlyphBounds(ch)
if !ok {
return nil, fmt.Errorf("ttf face glyphBounds error")
}
gh := int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
gw := int32((gBnd.Max.X - gBnd.Min.X) >> 6)
// If gylph has no dimensions set to a max value
if gw == 0 || gh == 0 {
gBnd = ttf.Bounds(fixed.Int26_6(scale))
gw = int32((gBnd.Max.X - gBnd.Min.X) >> 6)
gh = int32((gBnd.Max.Y - gBnd.Min.Y) >> 6)
// Above can sometimes yield 0 for font smaller than 48pt, 1 is minimum
if gw == 0 || gh == 0 {
gw = 1
gh = 1
}
}
// The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y.
gAscent := int(-gBnd.Min.Y) >> 6
gdescent := int(gBnd.Max.Y) >> 6
// Set w,h and adv, bearing V and bearing H in char
char.x = x
char.y = y
char.width = int(gw)
char.height = int(gh)
char.advance = int(gAdv)
char.bearingV = gdescent
char.bearingH = (int(gBnd.Min.X) >> 6)
clip := image.Rect(x, y, x+int(gw), y+int(gh))
// Create a freetype context for drawing
c := freetype.NewContext()
c.SetDPI(72)
c.SetFont(ttf)
c.SetFontSize(float64(scale))
c.SetClip(clip)
c.SetDst(rgba)
c.SetSrc(fg)
c.SetHinting(font.HintingFull)
// Set the glyph dot
px := 0 - (int(gBnd.Min.X) >> 6) + x
py := (gAscent) + y
pt := freetype.Pt(px, py)
x += int(gw) + margin
if x+int(gw)+margin > int(f.atlasWidth) {
x = 0
y += int(lineHeight) + margin
}
// Draw the text from mask to image
_, err = c.DrawString(string(ch), pt)
for _, r := range runes {
glyph, err := appendGlyph(face, ttf, r, &x, &y, lineHeight, f.atlasWidth, atlas, fg, scale, margin)
if err != nil {
return nil, err
continue
}
// Add char to fontChar list
f.fontChar = append(f.fontChar, char)
f.glyphs[r] = glyph
}
f.nextX = x
f.nextY = y
// Generate texture
gl.GenTextures(1, &f.textureID)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, f.textureID)
gl.PixelStorei(gl.UNPACK_ALIGNMENT, 1)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(rgba.Rect.Dx()), int32(rgba.Rect.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(rgba.Pix))
gl.GenerateMipmap(gl.TEXTURE_2D)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(atlas.Rect.Dx()), int32(atlas.Rect.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(atlas.Pix))
gl.BindTexture(gl.TEXTURE_2D, 0)
// Configure VAO/VBO for texture quads
@@ -205,6 +380,25 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale int32, low, high rune,
return f, nil
}
func (f *Font) ensureGlyph(r rune) *glyph {
if glyph, ok := f.glyphs[r]; ok {
return glyph
}
if f.atlas == nil || f.face == nil || f.ttf == nil {
return nil
}
glyph, err := appendGlyph(f.face, f.ttf, r, &f.nextX, &f.nextY, f.lineHeight, f.atlasWidth, f.atlas, image.White, f.scale, f.margin)
if err != nil {
log.Printf("font: failed to append U+%04X %q: %v", r, string(r), err)
return nil
}
f.glyphs[r] = glyph
f.uploadGlyphRect(glyph)
return glyph
}
// LoadFont loads the specified font at the given scale.
func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Font, error) {
fd, err := os.Open(file)
@@ -226,7 +420,16 @@ func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Fon
resUniform := gl.GetUniformLocation(program, gl.Str("resolution\x00"))
gl.Uniform2f(resUniform, float32(windowWidth), float32(windowHeight))
return LoadTrueTypeFont(program, fd, scale, 32, 256, LeftToRight)
atlasScale := int32(float32(scale) / fontRenderScale)
if atlasScale < 1 {
atlasScale = 1
}
font, err := LoadTrueTypeFont(program, fd, atlasScale, LeftToRight)
if err != nil {
return nil, err
}
return font, nil
}
// SetColor allows you to set the text color to be used when you draw the text
@@ -245,13 +448,14 @@ func (f *Font) UpdateResolution(windowWidth int, windowHeight int) {
// Print draws a string to the screen.
func (f *Font) Print(x, y float32, scale float32, text string) error {
indices := []rune(text)
scale *= fontRenderScale
x = float32(int(x + 0.5))
y = float32(int(y + 0.5))
if len(indices) == 0 {
return nil
}
lowChar := rune(32)
// Setup blending mode
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
@@ -263,26 +467,23 @@ func (f *Font) Print(x, y float32, scale float32, text string) error {
var coords []point
// Iterate through all characters in string
// Iterate through all glyphs in string
for i := range indices {
// Get rune
runeIndex := indices[i]
var ch *character
// Skip runes that are not in font chacter range
if int(runeIndex)-int(lowChar) > len(f.fontChar) || runeIndex < lowChar {
// Print a ?
ch = f.fontChar[int(rune('?'))-int(lowChar)]
} else {
// Find rune in fontChar list
ch = f.fontChar[runeIndex-lowChar]
var ch *glyph
// Skip runes that are not in font glyph map
ch = f.ensureGlyph(runeIndex)
if ch == nil {
ch = f.glyphs['?'] // fallback
}
// Calculate position and size for current rune
xpos := x - 1 + float32(ch.bearingH)*scale
ypos := y - 2 - float32(ch.height-ch.bearingV)*scale
w := float32(ch.width+2) * scale
h := float32(ch.height+2) * scale
xpos := x + float32(ch.bearingH)*scale
ypos := y - float32(ch.height-ch.bearingV)*scale
w := float32(ch.width) * scale
h := float32(ch.height) * scale
// Set quad positions
var x1 = xpos
@@ -290,12 +491,12 @@ func (f *Font) Print(x, y float32, scale float32, text string) error {
var y1 = ypos
var y2 = ypos + h
coords = append(coords, point{x1, y1, float32(ch.x-1) / f.atlasWidth, float32(ch.y-1) / f.atlasHeight})
coords = append(coords, point{x2, y1, float32(ch.x+ch.width+1) / f.atlasWidth, float32(ch.y-1) / f.atlasHeight})
coords = append(coords, point{x1, y2, float32(ch.x-1) / f.atlasWidth, float32(ch.y+ch.height+1) / f.atlasHeight})
coords = append(coords, point{x2, y1, float32(ch.x+ch.width+1) / f.atlasWidth, float32(ch.y-1) / f.atlasHeight})
coords = append(coords, point{x1, y2, float32(ch.x-1) / f.atlasWidth, float32(ch.y+ch.height+1) / f.atlasHeight})
coords = append(coords, point{x2, y2, float32(ch.x+ch.width+1) / f.atlasWidth, float32(ch.y+ch.height+1) / f.atlasHeight})
coords = append(coords, point{x1, y1, float32(ch.x) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
coords = append(coords, point{x2, y1, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
coords = append(coords, point{x1, y2, float32(ch.x) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
coords = append(coords, point{x2, y1, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y) / f.atlasHeight})
coords = append(coords, point{x1, y2, float32(ch.x) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
coords = append(coords, point{x2, y2, float32(ch.x+ch.width) / f.atlasWidth, float32(ch.y+ch.height) / f.atlasHeight})
// Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
x += float32((ch.advance >> 6)) * scale // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
@@ -323,6 +524,7 @@ func (f *Font) Printf(x, y float32, scale float32, format string, argv ...interf
// Width returns the width of a piece of text in pixels.
func (f *Font) Width(scale float32, text string) float32 {
var width float32
scale *= fontRenderScale
indices := []rune(text)
@@ -330,26 +532,21 @@ func (f *Font) Width(scale float32, text string) float32 {
return 0
}
lowChar := rune(32)
// Iterate through all characters in string
// Iterate through all glyphs in string
for i := range indices {
// Get rune
runeIndex := indices[i]
// Skip runes that are not in font chacter range
if int(runeIndex)-int(lowChar) > len(f.fontChar) || runeIndex < lowChar {
fmt.Printf("%c %d\n", runeIndex, runeIndex)
continue
if f.face != nil {
if advance, ok := f.face.GlyphAdvance(runeIndex); ok {
width += float32(advance.Round()) * scale
continue
}
}
// Find rune in fontChar list
ch := f.fontChar[runeIndex-lowChar]
// Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
width += float32((ch.advance >> 6)) * scale // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
if ch, ok := f.glyphs['?']; ok {
width += float32((ch.advance >> 6)) * scale
}
}
return width
+1
View File
@@ -219,6 +219,7 @@ func textureLoad(nrgba *image.NRGBA) uint32 {
gl.RGBA,
gl.UNSIGNED_BYTE,
gl.Ptr(nrgba.Pix))
gl.ActiveTexture(gl.TEXTURE0)
return texture
}
+21 -1
View File
@@ -20,6 +20,8 @@ type Video struct {
Window *glfw.Window
Geom libretro.GameGeometry
Font *Font
FontSm *Font
FontLg *Font
program uint32 // current program used for the game quad
defaultProgram uint32 // default program used for the game quad
@@ -128,7 +130,15 @@ func (video *Video) Configure(fullscreen bool) {
// LoadFont (fontfile, font scale, window width, window height)
fontPath := filepath.Join(settings.Current.AssetsDirectory, "font.ttf")
video.Font, err = LoadFont(fontPath, int32(36*2), fbw, fbh)
video.Font, err = LoadFont(fontPath, int32(36), fbw, fbh)
if err != nil {
panic(err)
}
video.FontSm, err = LoadFont(fontPath, int32(29), fbw, fbh)
if err != nil {
panic(err)
}
video.FontLg, err = LoadFont(fontPath, int32(50), fbw, fbh)
if err != nil {
panic(err)
}
@@ -309,6 +319,15 @@ func (video *Video) coreRatioViewport(fbWidth int, fbHeight int) (x, y, w, h flo
func (video *Video) ResizeViewport() {
fbw, fbh := video.Window.GetFramebufferSize()
gl.Viewport(0, 0, int32(fbw), int32(fbh))
if video.Font != nil {
video.Font.UpdateResolution(fbw, fbh)
}
if video.FontSm != nil {
video.FontSm.UpdateResolution(fbw, fbh)
}
if video.FontLg != nil {
video.FontLg.UpdateResolution(fbw, fbh)
}
}
// Render the current frame
@@ -357,6 +376,7 @@ func (video *Video) uploadTexture() {
return
}
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, video.texID)
gl.PixelStorei(gl.UNPACK_ROW_LENGTH, video.pitch/video.bpp)