Files
inkwell/ui.dialog_box.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

136 lines
3.4 KiB
Go

package inkwell
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
)
// DialogBox renders the active conversation: one line at a time (advanced
// on click) followed by the player's choices. The widget is dormant
// unless ctx.Game.dialogueActive() — and while active it consumes input
// so nothing underneath reacts.
type DialogBox struct {
Name string
Bounds Rectangle
LineHeight int
Padding int
}
func (d *DialogBox) GetName() string { return d.Name }
func (d *DialogBox) Layer() Layer { return LayerDialog }
func (d *DialogBox) Tick(ctx *UICtx) {
g := ctx.Game
if !g.dialogueActive() {
return
}
// while a dialog is open, ALL clicks belong to it
if g.Input.RightClicked() {
g.Input.ConsumeRight()
}
if !g.Input.LeftClicked() {
return
}
g.Input.ConsumeLeft()
mp := g.Input.Point()
dlg := g.dialog
if dlg.LineIdx >= 0 && dlg.LineIdx < len(dlg.Node.Lines) {
dlg.LineIdx++
if dlg.LineIdx >= len(dlg.Node.Lines) {
dlg.LineIdx = -1
dlg.ChoiceHits = nil
}
return
}
// pick the clicked choice
for i, r := range dlg.ChoiceHits {
if r.Contains(mp) {
choice := d.resolveChoices(ctx)[i]
if choice.Once {
g.State.NoteTalked(dlg.Node.Name + ":" + choice.Text)
}
if len(choice.Actions) > 0 {
g.queueAction(Seq(choice.Actions...), "choice")
}
return
}
}
}
func (d *DialogBox) resolveChoices(ctx *UICtx) []DialogueChoice {
g := ctx.Game
dlg := g.dialog
out := make([]DialogueChoice, 0, len(dlg.Node.Choices))
for _, c := range dlg.Node.Choices {
gctx := g.makeCtx()
if c.Show != nil && !c.Show.Eval(gctx) {
continue
}
if c.Once && g.State.Talked(dlg.Node.Name+":"+c.Text) > 0 {
continue
}
out = append(out, c)
}
return out
}
// BlocksClickAt — when a dialog is open, every click is ours.
func (d *DialogBox) BlocksClickAt(p Point) bool {
// without ctx we can't know if a dialog is active; conservatively block
// when the cursor is in the dialog bounds (used only by RadialVerbs).
b := d.Bounds
if b.W == 0 {
return false
}
return b.Contains(p)
}
func (d *DialogBox) Draw(dst *ebiten.Image, ctx *UICtx) {
g := ctx.Game
if !g.dialogueActive() {
return
}
th := g.Theme()
b := d.Bounds
if b.W == 0 {
b = Rect(0, 140, float64(g.Width), 60)
}
pad := d.Padding
if pad <= 0 {
pad = 6
}
lh := d.LineHeight
if lh <= 0 {
lh = 14
}
vector.DrawFilledRect(dst, float32(b.X), float32(b.Y), float32(b.W), float32(b.H), th.DialogBG, false)
vector.StrokeRect(dst, float32(b.X), float32(b.Y), float32(b.W), float32(b.H), 1, th.DialogBorder, false)
dlg := g.dialog
if dlg.LineIdx >= 0 && dlg.LineIdx < len(dlg.Node.Lines) {
ln := dlg.Node.Lines[dlg.LineIdx]
g.DrawText(dst, ln.Speaker+":", int(b.X)+pad, int(b.Y)+2, th.DialogSpeaker)
lines := wrapText(ln.Text, int(b.W)-pad*2)
for i, l := range lines {
g.DrawText(dst, l, int(b.X)+pad, int(b.Y)+18+i*lh, th.DialogText)
}
return
}
choices := d.resolveChoices(ctx)
dlg.ChoiceHits = dlg.ChoiceHits[:0]
mp := g.Input.Point()
for i, c := range choices {
r := Rect(b.X+float64(pad), b.Y+4+float64(i*lh), b.W-float64(pad)*2, float64(lh-1))
dlg.ChoiceHits = append(dlg.ChoiceHits, r)
bg := th.DialogChoiceBG
if r.Contains(mp) {
bg = th.DialogChoiceHover
}
vector.DrawFilledRect(dst, float32(r.X), float32(r.Y), float32(r.W), float32(r.H), bg, false)
g.DrawText(dst, c.Text, int(r.X)+2, int(r.Y)-1, th.DialogText)
}
}