Files
realworld/inc/ui.text.go
T
2026-08-29 23:25:36 +02:00

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]) + "…"
}
}