Files
inkwell/ui.speech.go
T
mr.zeroandClaude Opus 5 2429ada090
ci/woodpecker/push/woodpecker Pipeline was successful
A widget declares its layer
Registration order was the only thing deciding what a widget was drawn
over, which forced a domain to keep one ordered list of every widget it
owns — the one shape that cannot be split into a file per widget.

A widget now says where it belongs: Layer, the eight LayerScene..LayerCursor
constants, the optional Layered interface and LayerOf for wrappers. Draw
runs from the bottom layer up, Tick from the top down, and registration
order only breaks ties inside a layer. Every built-in declares its own;
anything that stays quiet sits on LayerHUD, so existing HUDs come out where
they were.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 22:19:17 +02:00

82 lines
1.6 KiB
Go

package inkwell
import (
"image/color"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
)
// SpeechBubble renders the line set by the Say action above the speaking
// character (or at FallbackY if the speaker has no on-screen position).
type SpeechBubble struct {
Name string
MaxWidth int
Padding int
OffsetY int
FallbackY int
}
func (s *SpeechBubble) GetName() string { return s.Name }
func (s *SpeechBubble) Layer() Layer { return LayerSpeech }
func (s *SpeechBubble) Tick(ctx *UICtx) {}
func (s *SpeechBubble) Draw(dst *ebiten.Image, ctx *UICtx) {
g := ctx.Game
sp := g.speech
if !sp.Active {
return
}
maxW := s.MaxWidth
if maxW <= 0 {
maxW = 200
}
pad := s.Padding
if pad <= 0 {
pad = 3
}
lines := wrapText(sp.Text, maxW)
w := 0
for _, ln := range lines {
if t := textWidth(ln); t > w {
w = t
}
}
w += pad * 2
h := len(lines)*16 + pad*2
// position above speaker if known
cx, cy := g.Width/2, s.FallbackY
if cy == 0 {
cy = 14
}
if c, ok := g.runtimeChar(sp.Speaker); ok {
cx = int(c.pos.X)
cy = int(c.pos.Y-c.def.H) - s.OffsetY
}
x := cx - w/2
y := cy - h
if x < 2 {
x = 2
}
if x+w > g.Width-2 {
x = g.Width - 2 - w
}
if y < 2 {
y = 2
}
th := g.Theme()
vector.DrawFilledRect(dst, float32(x), float32(y), float32(w), float32(h), th.SpeechBubbleBG, false)
textCol := th.SpeechDefaultText
if c, ok := g.CharacterManager.Get(sp.Speaker); ok {
if rgba, ok := c.SpeechColor.(color.RGBA); ok && rgba.A > 0 {
textCol = rgba
}
}
for i, ln := range lines {
g.DrawText(dst, ln, x+pad, y+pad+i*16-2, textCol)
}
}