ci/woodpecker/push/woodpecker Pipeline was successful
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>
93 lines
2.2 KiB
Go
93 lines
2.2 KiB
Go
package inkwell
|
|
|
|
import (
|
|
"image/color"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
|
)
|
|
|
|
// ChatLog renders the in-game message log (player commands + in-world
|
|
// replies) as a scrolling box. The newest message is at the bottom; older
|
|
// messages scroll up out of view as the buffer fills.
|
|
type ChatLog struct {
|
|
Name string
|
|
Bounds Rectangle
|
|
LineHeight int
|
|
Padding int
|
|
// ShowBorder draws a thin border around the box.
|
|
ShowBorder bool
|
|
}
|
|
|
|
func (c *ChatLog) GetName() string { return c.Name }
|
|
func (c *ChatLog) Layer() Layer { return LayerHUD }
|
|
func (c *ChatLog) Tick(ctx *UICtx) {}
|
|
|
|
func (c *ChatLog) Draw(dst *ebiten.Image, ctx *UICtx) {
|
|
g := ctx.Game
|
|
th := g.Theme()
|
|
b := c.Bounds
|
|
if b.W <= 0 || b.H <= 0 {
|
|
return
|
|
}
|
|
lh := c.LineHeight
|
|
if lh <= 0 {
|
|
lh = 10
|
|
}
|
|
pad := c.Padding
|
|
if pad <= 0 {
|
|
pad = 3
|
|
}
|
|
|
|
vector.DrawFilledRect(dst, float32(b.X), float32(b.Y), float32(b.W), float32(b.H), th.ChatLogBG, false)
|
|
if c.ShowBorder {
|
|
vector.StrokeRect(dst, float32(b.X), float32(b.Y), float32(b.W), float32(b.H), 1, th.CharacterPanelBorder, false)
|
|
}
|
|
|
|
msgs := g.Messages()
|
|
maxLines := int((b.H - float64(2*pad)) / float64(lh))
|
|
if maxLines <= 0 {
|
|
return
|
|
}
|
|
|
|
// Wrap each message; collect rendered lines (color + text).
|
|
type line struct {
|
|
text string
|
|
col color.Color
|
|
}
|
|
wrapW := int(b.W) - pad*2
|
|
var rendered []line
|
|
for _, m := range msgs {
|
|
var col color.Color
|
|
switch m.Kind {
|
|
case LogAction:
|
|
col = th.ChatLogPrompt
|
|
case LogResponse:
|
|
col = th.ChatLogResponse
|
|
default:
|
|
col = th.ChatLogSystem
|
|
}
|
|
txt := m.Text
|
|
if m.Kind == LogResponse && m.Speaker != "" {
|
|
txt = m.Speaker + ": " + m.Text
|
|
}
|
|
for _, w := range wrapText(txt, wrapW) {
|
|
rendered = append(rendered, line{text: w, col: col})
|
|
}
|
|
}
|
|
|
|
// Show the LAST maxLines wrapped lines.
|
|
start := len(rendered) - maxLines
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
for i, ln := range rendered[start:] {
|
|
y := int(b.Y) + pad + i*lh
|
|
g.DrawText(dst, ln.text, int(b.X)+pad, y-1, ln.col)
|
|
}
|
|
}
|
|
|
|
// BlocksClickAt — the log is non-interactive, but it sits over the bottom
|
|
// of the scene; treat it as opaque so RadialVerbs doesn't pop up over it.
|
|
func (c *ChatLog) BlocksClickAt(p Point) bool { return c.Bounds.Contains(p) }
|