Every change here started life as a workaround in a game and is really the same finding: a fact about an entity had nowhere to live, so the domain built machinery around the gap. Character.Label / Character.Voice — a tape is a character that speaks into the log, not a second spelling of Say. VoiceOf and CharacterLabel read them; Say obeys them. Game.Do — a real action queue, in order, so a widget can start an action. queueAction no longer drops what arrives while the runner is busy; it queues it. Widget.When + Gated + WidgetVisible — a widget declares when it is on screen. Nothing ticks, draws or blocks a click while its condition is false, so a HUD is hidden for a cutscene without a wrapper per widget. TopBar.NoteVar — the title was already discovered; the note beside it no longer needs a domain widget to push it in. Game.UseWithFail — the pair nobody authored is content, and belongs to the domain, exactly like ExitLook and ExitTake. Game.Player / Game.Walkboxes — a scene that names no cast and no floor means "the usual", instead of a defaults pass rewriting every scene. Game.WindowScale — Run's 4× is now a default, not a decision. SceneNav — the dev widget every game was writing. Colour text: DrawText paints in the colour it is handed, and GlyphW/H, TextWidth, WrapText and ClipText are the library's, not each game's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+85
-38
@@ -1,72 +1,119 @@
|
||||
package inkwell
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
)
|
||||
|
||||
// drawText is a minimal text draw using ebiten's built-in debug font.
|
||||
// The glyphs are 6×16-ish; good enough for a SCUMM-style 320×200 demo.
|
||||
// The built-in debug font is a fixed 6×16 cell. Every layout measurement
|
||||
// in the library — and in a domain's own widgets — derives from these.
|
||||
const (
|
||||
GlyphW = 6
|
||||
GlyphH = 16
|
||||
)
|
||||
|
||||
// textScratch is the offscreen the coloured draw goes through. It grows
|
||||
// to fit the widest line ever drawn and is reused after that.
|
||||
var textScratch *ebiten.Image
|
||||
|
||||
// drawText renders s in colour c. ebitenutil.DebugPrintAt only draws
|
||||
// white, so the glyphs go onto an offscreen first and are blitted back
|
||||
// tinted with a ColorScale.
|
||||
func drawText(dst *ebiten.Image, s string, x, y int, c color.Color) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
// ebitenutil.DebugPrintAt only draws white; for color we render to a
|
||||
// tiny offscreen, then ColorScale-tint when blitting. Simpler: just
|
||||
// use the white draw and skip color. (TODO: text/v2 once needed.)
|
||||
_ = c
|
||||
ebitenutil.DebugPrintAt(dst, s, x, y)
|
||||
w, h := TextWidth(s)+GlyphW, GlyphH
|
||||
if textScratch == nil || textScratch.Bounds().Dx() < w || textScratch.Bounds().Dy() < h {
|
||||
nw := w
|
||||
if nw < 320 {
|
||||
nw = 320
|
||||
}
|
||||
textScratch = ebiten.NewImage(nw, h)
|
||||
}
|
||||
textScratch.Clear()
|
||||
ebitenutil.DebugPrintAt(textScratch, 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(textScratch.SubImage(image.Rect(0, 0, w, h)).(*ebiten.Image), op)
|
||||
}
|
||||
|
||||
// textWidth is a coarse pixel-width estimate, used for centering.
|
||||
func textWidth(s string) int { return len(s) * 6 }
|
||||
// TextWidth is the pixel width of s in the built-in font.
|
||||
func TextWidth(s string) int { return utf8.RuneCountInString(s) * GlyphW }
|
||||
|
||||
// wrapText splits s at word boundaries into lines no wider than maxPx.
|
||||
func wrapText(s string, maxPx int) []string {
|
||||
if maxPx <= 0 || textWidth(s) <= maxPx {
|
||||
// WrapText splits s at word boundaries into lines no wider than maxPx.
|
||||
// A newline in s breaks the line where it stands.
|
||||
func WrapText(s string, maxPx int) []string {
|
||||
if maxPx <= GlyphW || TextWidth(s) <= maxPx {
|
||||
return []string{s}
|
||||
}
|
||||
var (
|
||||
out []string
|
||||
line string
|
||||
)
|
||||
var out []string
|
||||
line, word := "", ""
|
||||
flush := func() {
|
||||
if line != "" {
|
||||
out = append(out, line)
|
||||
line = ""
|
||||
}
|
||||
}
|
||||
word := ""
|
||||
for _, r := range s {
|
||||
if r == ' ' || r == '\n' {
|
||||
if line == "" {
|
||||
line = word
|
||||
} else if textWidth(line+" "+word) <= maxPx {
|
||||
line += " " + word
|
||||
} else {
|
||||
flush()
|
||||
line = word
|
||||
}
|
||||
word = ""
|
||||
if r == '\n' {
|
||||
flush()
|
||||
}
|
||||
continue
|
||||
emit := func() {
|
||||
if word == "" {
|
||||
return
|
||||
}
|
||||
word += string(r)
|
||||
}
|
||||
if word != "" {
|
||||
if line == "" {
|
||||
switch {
|
||||
case line == "":
|
||||
line = word
|
||||
} else if textWidth(line+" "+word) <= maxPx {
|
||||
case TextWidth(line+" "+word) <= maxPx:
|
||||
line += " " + word
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
// ClipText cuts s to maxPx pixels, ending in an ellipsis when it had to
|
||||
// cut. A width with no room at all returns the empty string.
|
||||
func ClipText(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]) + "…"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user