From 416576f940f747df97f0bc17a8fac2b084201d1b Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Tue, 23 Apr 2024 13:05:39 -0700 Subject: [PATCH] Allow fonts to be specified directly This will enable etk to auto-scale text to fit within a bounded area. Relates to #5. --- button.go | 9 ++++----- game.go | 39 ++++++++++++++++++++++++++++++++++++++- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- input.go | 15 +++++++-------- style.go | 38 ++++++++++++++++---------------------- text.go | 9 ++++----- window.go | 25 ++++++++++++------------- 8 files changed, 102 insertions(+), 75 deletions(-) diff --git a/button.go b/button.go index 226c565..822ac24 100644 --- a/button.go +++ b/button.go @@ -3,11 +3,10 @@ package etk import ( "image" "image/color" - "sync" "code.rocket9labs.com/tslocum/etk/messeji" "github.com/hajimehoshi/ebiten/v2" - "golang.org/x/image/font" + "golang.org/x/image/font/sfnt" ) // Button is a clickable button. @@ -96,12 +95,12 @@ func (b *Button) SetText(text string) { b.field.SetText(text) } -// SetFont sets the font face of the text within the field. -func (b *Button) SetFont(face font.Face, mutex *sync.Mutex) { +// SetFont sets the font and text size of button label. Scaling is not applied. +func (b *Button) SetFont(fnt *sfnt.Font, size int) { b.Lock() defer b.Unlock() - b.field.SetFont(face, mutex) + b.field.SetFont(FontFace(fnt, size), fontMutex) } // HandleKeyboard is called when a keyboard event occurs. diff --git a/game.go b/game.go index 01cb7ac..d267716 100644 --- a/game.go +++ b/game.go @@ -4,15 +4,19 @@ import ( "fmt" "image" "image/color" + "log" "math" "runtime/debug" "strings" + "sync" "time" "code.rocket9labs.com/tslocum/etk/messeji" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" "golang.org/x/image/font" + "golang.org/x/image/font/opentype" + "golang.org/x/image/font/sfnt" "golang.org/x/image/math/fixed" ) @@ -50,6 +54,8 @@ var ( keyBuffer []ebiten.Key runeBuffer []rune + + fontMutex = &sync.Mutex{} ) var debugColor = color.RGBA{0, 0, 255, 255} @@ -80,6 +86,37 @@ func Scale(v int) int { return int(float64(v) * deviceScale) } +var ( + fontCache = make(map[string]font.Face) + fontCacheLock sync.Mutex +) + +// FontFace returns a face for the provided font and size. Scaling is not applied. +func FontFace(fnt *sfnt.Font, size int) font.Face { + id := fmt.Sprintf("%p/%d", fnt, size) + + fontCacheLock.Lock() + defer fontCacheLock.Unlock() + + f := fontCache[id] + if f != nil { + return f + } + + const dpi = 72 + f, err := opentype.NewFace(fnt, &opentype.FaceOptions{ + Size: float64(size), + DPI: dpi, + Hinting: font.HintingFull, + }) + if err != nil { + log.Fatal(err) + } + + fontCache[id] = f + return f +} + // SetRoot sets the root widget. The root widget and all of its children will // be drawn on the screen and receive user input. The root widget will also be // focused. To temporarily disable etk, set a nil root widget. @@ -412,7 +449,7 @@ func draw(w Widget, screen *ebiten.Image) error { } func newText() *messeji.TextField { - f := messeji.NewTextField(Style.TextFont, Style.TextFontMutex) + f := messeji.NewTextField(FontFace(Style.TextFont, Scale(Style.TextSize)), fontMutex) f.SetForegroundColor(Style.TextColorLight) f.SetBackgroundColor(transparent) f.SetScrollBarColors(Style.ScrollAreaColor, Style.ScrollHandleColor) diff --git a/go.mod b/go.mod index cb1ac72..4042ec6 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,18 @@ module code.rocket9labs.com/tslocum/etk go 1.18 require ( - github.com/hajimehoshi/ebiten/v2 v2.6.5 - github.com/llgcode/draw2d v0.0.0-20231212091825-f55e0c776b44 + github.com/hajimehoshi/ebiten/v2 v2.7.2 + github.com/llgcode/draw2d v0.0.0-20240322162412-ee6987bd01dc golang.org/x/image v0.15.0 ) require ( - github.com/ebitengine/purego v0.6.0 // indirect + github.com/ebitengine/gomobile v0.0.0-20240329170434-1771503ff0a8 // indirect + github.com/ebitengine/hideconsole v1.0.0 // indirect + github.com/ebitengine/purego v0.7.1 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/jezek/xgb v1.1.1 // indirect - golang.org/x/exp/shiny v0.0.0-20240205201215-2c58cdc269a3 // indirect - golang.org/x/mobile v0.0.0-20240112133503-c713f31d574b // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.17.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index e60242f..15c8653 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,24 @@ -github.com/ebitengine/purego v0.6.0 h1:Yo9uBc1x+ETQbfEaf6wcBsjrQfCEnh/gaGUg7lguEJY= -github.com/ebitengine/purego v0.6.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/ebitengine/gomobile v0.0.0-20240329170434-1771503ff0a8 h1:5e8X7WEdOWrjrKvgaWF6PRnDvJicfrkEnwAkWtMN74g= +github.com/ebitengine/gomobile v0.0.0-20240329170434-1771503ff0a8/go.mod h1:tWboRRNagZwwwis4QIgEFG1ZNFwBJ3LAhSLAXAAxobQ= +github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= +github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= +github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= +github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/hajimehoshi/bitmapfont/v3 v3.0.0 h1:r2+6gYK38nfztS/et50gHAswb9hXgxXECYgE8Nczmi4= -github.com/hajimehoshi/ebiten/v2 v2.6.5 h1:lALv+qhEK3CBWViyiGpz4YcR6slVJEjCiS7sExKZ9OE= -github.com/hajimehoshi/ebiten/v2 v2.6.5/go.mod h1:TZtorL713an00UW4LyvMeKD8uXWnuIuCPtlH11b0pgI= +github.com/hajimehoshi/ebiten/v2 v2.7.2 h1:5HcWAjxhGMBocJh0jH/61Kx4QJ91HkzYtSeSucvVg7o= +github.com/hajimehoshi/ebiten/v2 v2.7.2/go.mod h1:1vjyPw+h3n30rfTOpIsbWRXSxZ0Oz1cYc6Tq/2DKoQg= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= -github.com/llgcode/draw2d v0.0.0-20231212091825-f55e0c776b44 h1:1ad70i0s40IpMtRm2ST+Nvr03X7mlHWtdALYkFNrlxk= -github.com/llgcode/draw2d v0.0.0-20231212091825-f55e0c776b44/go.mod h1:muweRyJCZ1mZSMiCgYbAicfnwZFoeHpNr6A6QBu+rBg= +github.com/llgcode/draw2d v0.0.0-20240322162412-ee6987bd01dc h1:lorg2FaIDdlahOHekjnQjItP2oCtkVlYc0QNekubCLk= +github.com/llgcode/draw2d v0.0.0-20240322162412-ee6987bd01dc/go.mod h1:muweRyJCZ1mZSMiCgYbAicfnwZFoeHpNr6A6QBu+rBg= github.com/llgcode/ps v0.0.0-20210114104736-f4b0c5d1e02e h1:ZAvbj5hI/G/EbAYAcj4yCXUNiFKefEhH0qfImDDD0/8= -golang.org/x/exp/shiny v0.0.0-20240205201215-2c58cdc269a3 h1:tImqKNm/Iclm3Rqb6GffLiURSp3m1iRx/C4mturH8Ys= -golang.org/x/exp/shiny v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/mobile v0.0.0-20240112133503-c713f31d574b h1:kfWLZgb8iUBHdE9WydD5V5dHIS/F6HjlBZNyJfn2bs4= -golang.org/x/mobile v0.0.0-20240112133503-c713f31d574b/go.mod h1:4efzQnuA1nICq6h4kmZRMGzbPiP06lZvgADUu1VpJCE= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/input.go b/input.go index 7bb0754..43cfaea 100644 --- a/input.go +++ b/input.go @@ -3,11 +3,10 @@ package etk import ( "image" "image/color" - "sync" "code.rocket9labs.com/tslocum/etk/messeji" "github.com/hajimehoshi/ebiten/v2" - "golang.org/x/image/font" + "golang.org/x/image/font/sfnt" ) // Input is a text input widget. The Input widget is simply a Text widget that @@ -26,7 +25,7 @@ func NewInput(text string, onSelected func(text string) (handled bool)) *Input { textColor = Style.InputColor }*/ - f := messeji.NewInputField(Style.TextFont, Style.TextFontMutex) + f := messeji.NewInputField(FontFace(Style.TextFont, Scale(Style.TextSize)), fontMutex) f.SetForegroundColor(textColor) f.SetBackgroundColor(transparent) f.SetScrollBarColors(Style.ScrollAreaColor, Style.ScrollHandleColor) @@ -169,12 +168,12 @@ func (i *Input) SetAutoHideScrollBar(autoHide bool) { i.field.SetAutoHideScrollBar(autoHide) } -// SetFont sets the font face of the text within the field. -func (i *Input) SetFont(face font.Face, mutex *sync.Mutex) { - i.Lock() - defer i.Unlock() +// SetFont sets the font and text size of the field. Scaling is not applied. +func (t *Input) SetFont(fnt *sfnt.Font, size int) { + t.Lock() + defer t.Unlock() - i.field.SetFont(face, mutex) + t.field.SetFont(FontFace(fnt, size), fontMutex) } // Padding returns the amount of padding around the text within the field. diff --git a/style.go b/style.go index 94c36d4..e7cdeed 100644 --- a/style.go +++ b/style.go @@ -3,43 +3,34 @@ package etk import ( "image/color" "log" - "sync" "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts" - "golang.org/x/image/font" "golang.org/x/image/font/opentype" + "golang.org/x/image/font/sfnt" ) var transparent = color.RGBA{0, 0, 0, 0} -func defaultFont() font.Face { - tt, err := opentype.Parse(fonts.MPlus1pRegular_ttf) +func defaultFont() *sfnt.Font { + f, err := opentype.Parse(fonts.MPlus1pRegular_ttf) if err != nil { log.Fatal(err) } - const dpi = 72 - defaultFont, err := opentype.NewFace(tt, &opentype.FaceOptions{ - Size: 32, - DPI: dpi, - Hinting: font.HintingFull, - }) - if err != nil { - log.Fatal(err) - } - return defaultFont + return f } // Attributes represents a default attribute configuration. Integer values will be scaled. type Attributes struct { - TextFont font.Face - TextFontMutex *sync.Mutex + TextFont *sfnt.Font + TextSize int TextColorLight color.RGBA TextColorDark color.RGBA TextBgColor color.RGBA - BorderSize int + BorderSize int + BorderColorTop color.RGBA BorderColorRight color.RGBA BorderColorBottom color.RGBA @@ -48,7 +39,8 @@ type Attributes struct { ScrollAreaColor color.RGBA ScrollHandleColor color.RGBA - ScrollBorderSize int + ScrollBorderSize int + ScrollBorderColorTop color.RGBA ScrollBorderColorRight color.RGBA ScrollBorderColorBottom color.RGBA @@ -63,15 +55,16 @@ type Attributes struct { // Style is the current default attribute configuration. Integer values will be scaled. var Style = &Attributes{ - TextFont: defaultFont(), - TextFontMutex: &sync.Mutex{}, + TextFont: defaultFont(), + TextSize: 32, TextColorLight: color.RGBA{255, 255, 255, 255}, TextColorDark: color.RGBA{0, 0, 0, 255}, TextBgColor: transparent, - BorderSize: 4, + BorderSize: 4, + BorderColorTop: color.RGBA{220, 220, 220, 255}, BorderColorRight: color.RGBA{0, 0, 0, 255}, BorderColorBottom: color.RGBA{0, 0, 0, 255}, @@ -80,7 +73,8 @@ var Style = &Attributes{ ScrollAreaColor: color.RGBA{200, 200, 200, 255}, ScrollHandleColor: color.RGBA{108, 108, 108, 255}, - ScrollBorderSize: 2, + ScrollBorderSize: 2, + ScrollBorderColorTop: color.RGBA{240, 240, 240, 255}, ScrollBorderColorRight: color.RGBA{0, 0, 0, 255}, ScrollBorderColorBottom: color.RGBA{0, 0, 0, 255}, diff --git a/text.go b/text.go index 372174d..6910a2a 100644 --- a/text.go +++ b/text.go @@ -3,11 +3,10 @@ package etk import ( "image" "image/color" - "sync" "code.rocket9labs.com/tslocum/etk/messeji" "github.com/hajimehoshi/ebiten/v2" - "golang.org/x/image/font" + "golang.org/x/image/font/sfnt" ) // Text is a text display widget. @@ -155,12 +154,12 @@ func (t *Text) SetAutoHideScrollBar(autoHide bool) { t.field.SetAutoHideScrollBar(autoHide) } -// SetFont sets the font face of the text within the field. -func (t *Text) SetFont(face font.Face, mutex *sync.Mutex) { +// SetFont sets the font and text size of the field. Scaling is not applied. +func (t *Text) SetFont(fnt *sfnt.Font, size int) { t.Lock() defer t.Unlock() - t.field.SetFont(face, mutex) + t.field.SetFont(FontFace(fnt, size), fontMutex) } // Padding returns the amount of padding around the text within the field. diff --git a/window.go b/window.go index ad8c4d6..290e07c 100644 --- a/window.go +++ b/window.go @@ -3,17 +3,16 @@ package etk import ( "image" "image/color" - "sync" "github.com/hajimehoshi/ebiten/v2" - "golang.org/x/image/font" + "golang.org/x/image/font/sfnt" ) // Window displays child widgets in floating or maximized windows. type Window struct { *Box - fontFace font.Face - fontMutex *sync.Mutex + font *sfnt.Font + fontSize int frameSize int titleSize int titles []*Text @@ -26,8 +25,8 @@ type Window struct { func NewWindow() *Window { return &Window{ Box: NewBox(), - fontFace: Style.TextFont, - fontMutex: Style.TextFontMutex, + font: Style.TextFont, + fontSize: Scale(Style.TextSize), frameSize: Scale(4), titleSize: Scale(40), } @@ -42,16 +41,16 @@ func (w *Window) SetRect(r image.Rectangle) { w.modified = true } -// SetFont sets the font face of the window titles. -func (w *Window) SetFont(face font.Face, mutex *sync.Mutex) { +// SetFont sets the font and text size of the window titles. Scaling is not applied. +func (w *Window) SetFont(fnt *sfnt.Font, size int) { w.Lock() defer w.Unlock() - w.fontFace = face - w.fontMutex = mutex + w.font = fnt + w.fontSize = size for _, title := range w.titles { - title.SetFont(w.fontFace, w.fontMutex) + title.SetFont(w.font, w.fontSize) } } @@ -158,7 +157,7 @@ func (w *Window) AddChild(wgt ...Widget) { for _, widget := range wgt { t := NewText("") - t.SetFont(w.fontFace, w.fontMutex) + t.SetFont(w.font, w.fontSize) w.children = append(w.children, &windowWidget{NewBox(), t, widget, w}) w.titles = append(w.titles, t) @@ -173,7 +172,7 @@ func (w *Window) AddChildWithTitle(wgt Widget, title string) int { defer w.Unlock() t := NewText(title) - t.SetFont(w.fontFace, w.fontMutex) + t.SetFont(w.font, w.fontSize) w.children = append(w.children, &windowWidget{NewBox(), t, wgt, w}) w.titles = append(w.titles, t)