126 lines
2.8 KiB
Go
126 lines
2.8 KiB
Go
package inc
|
|
|
|
// The HUD's coloured text rendering.
|
|
//
|
|
// inkwell's drawText (asset.text.go) discards its colour argument (`_ = c`) and
|
|
// always renders white through the ebitenutil debug font. The two-channel
|
|
// colour rule — "if something is amber, a tape said it" — cannot work through
|
|
// it, so the custom widgets do not call Game.DrawText: they render glyphs onto
|
|
// a scratch image and blit it tinted with ColorScale. The built-in widgets
|
|
// (StatusLine, DialogBox, TopBar) still render white. See README, "Engine
|
|
// workarounds".
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
"unicode/utf8"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
|
)
|
|
|
|
// glyphW and glyphH are the ebitenutil debug font's cell. The engine's own
|
|
// textWidth uses the same 6px but counts bytes, which mismeasures any
|
|
// non-ASCII text; we count runes.
|
|
const (
|
|
glyphW = 6
|
|
glyphH = 16
|
|
)
|
|
|
|
var scratch *ebiten.Image
|
|
|
|
// textW is the pixel width of a string, counted in runes.
|
|
func textW(s string) int { return utf8.RuneCountInString(s) * glyphW }
|
|
|
|
// drawTextC draws one line in colour c, with the same origin as DebugPrintAt.
|
|
func drawTextC(dst *ebiten.Image, s string, x, y int, c color.Color) {
|
|
if s == "" {
|
|
return
|
|
}
|
|
w, h := textW(s)+glyphW, glyphH
|
|
if scratch == nil || scratch.Bounds().Dx() < w || scratch.Bounds().Dy() < h {
|
|
nw, nh := w, h
|
|
if nw < 320 {
|
|
nw = 320
|
|
}
|
|
scratch = ebiten.NewImage(nw, nh)
|
|
}
|
|
scratch.Clear()
|
|
ebitenutil.DebugPrintAt(scratch, s, 0, 0)
|
|
|
|
if c == nil {
|
|
c = color.White
|
|
}
|
|
r, g, b, a := c.RGBA()
|
|
op := &ebiten.DrawImageOptions{}
|
|
op.GeoM.Translate(float64(x), float64(y))
|
|
op.ColorScale.Scale(
|
|
float32(r)/0xffff, float32(g)/0xffff, float32(b)/0xffff, float32(a)/0xffff,
|
|
)
|
|
dst.DrawImage(scratch.SubImage(image.Rect(0, 0, w, h)).(*ebiten.Image), op)
|
|
}
|
|
|
|
// wrap breaks s at word boundaries into lines no wider than maxPx. The engine's
|
|
// wrapText is unexported and byte-based, hence our own.
|
|
func wrap(s string, maxPx int) []string {
|
|
if maxPx <= glyphW || textW(s) <= maxPx {
|
|
return []string{s}
|
|
}
|
|
var out []string
|
|
line, word := "", ""
|
|
flush := func() {
|
|
if line != "" {
|
|
out = append(out, line)
|
|
line = ""
|
|
}
|
|
}
|
|
emit := func() {
|
|
if word == "" {
|
|
return
|
|
}
|
|
switch {
|
|
case line == "":
|
|
line = word
|
|
case textW(line+" "+word) <= maxPx:
|
|
line += " " + word
|
|
default:
|
|
flush()
|
|
line = word
|
|
}
|
|
word = ""
|
|
}
|
|
for _, r := range s {
|
|
switch r {
|
|
case ' ':
|
|
emit()
|
|
case '\n':
|
|
emit()
|
|
flush()
|
|
default:
|
|
word += string(r)
|
|
}
|
|
}
|
|
emit()
|
|
flush()
|
|
if len(out) == 0 {
|
|
return []string{s}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// clip truncates a string so it fits into maxPx.
|
|
func clip(s string, maxPx int) string {
|
|
r := []rune(s)
|
|
max := maxPx / glyphW
|
|
switch {
|
|
case max <= 0:
|
|
return ""
|
|
case len(r) <= max:
|
|
return s
|
|
case max == 1:
|
|
return string(r[:1])
|
|
default:
|
|
return string(r[:max-1]) + "…"
|
|
}
|
|
}
|