package ui import ( "image/color" "iter" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/text/v2" "muhq.space/muhqs-game/go/font" "muhq.space/muhqs-game/go/game" ) var ( BUFFER_BACKGROUND = Gray BUFFER_FOREGROUND = color.White ) type Buffer struct { WidgetBase lines []string highlights map[int]color.Color pos int font *text.GoTextFace lineSpacing float64 xMargin float64 bg color.Color fg color.Color } func NewBuffer(x, y int, width, height int) *Buffer { b := &Buffer{ NewWidgetBase(x, y, width, height), []string{}, make(map[int]color.Color), 0, font.Font18, 1.5, 10., BUFFER_BACKGROUND, BUFFER_FOREGROUND, } b.renderImpl = func() *ebiten.Image { return b.render() } return b } func (b *Buffer) render() *ebiten.Image { img := ebiten.NewImage(b.Width, b.Height) img.Fill(b.bg) y := -b.font.Size for i, line := range b.lines[b.pos:] { _, h := text.Measure(line, b.font, b.font.Size*b.lineSpacing) y += h if y > float64(b.Height) { break } op := &text.DrawOptions{} if clr, highlighted := b.highlights[i]; highlighted { op.ColorScale.ScaleWithColor(clr) } else { op.ColorScale.ScaleWithColor(b.fg) } op.GeoM.Translate(b.xMargin, float64(y)) op.LineSpacing = b.font.Size * b.lineSpacing text.Draw(img, line, b.font, op) } return img } func (b *Buffer) AddLines(lines []string) { b.lines = append(b.lines, lines...) b.ForceRedraw() } func (b *Buffer) AddLine(line string) { b.AddLines([]string{line}) } func (b *Buffer) AddText(text string) { // TODO: auto break text lines := []string{text} b.AddLines(lines) } func (b *Buffer) ClearLines() { b.lines = []string{} b.ForceRedraw() } func (b *Buffer) RemoveLast() { b.lines = b.lines[:len(b.lines)-1] b.ForceRedraw() } func (b *Buffer) Remove(idx int) { b.lines = append(b.lines[:idx], b.lines[idx+1:]...) b.ForceRedraw() } func (b *Buffer) PrefixLine(idx int, prefix string) { b.lines[idx] = prefix + b.lines[idx] b.ForceRedraw() } func (b *Buffer) LinesSeq() iter.Seq[string] { return func(yield func(string) bool) { for _, l := range b.lines { if !yield(l) { return } } } } func (*Buffer) IsScrollable() bool { return true } func (b *Buffer) Scroll(x, y int) { if y == 0 { return } if y < 0 && b.pos > 0 { b.pos-- } else if y > 0 && b.pos < len(b.lines)-1 { b.pos++ } b.ForceRedraw() } func (b *Buffer) Bg(bg color.Color) *Buffer { b.bg = bg return b } func (b *Buffer) Fg(fg color.Color) *Buffer { b.fg = fg return b } type StackBuffer struct { Buffer actions []game.Action } func NewStackBuffer(x, y int, width, height int, actions []game.Action) *StackBuffer { sb := &StackBuffer{Buffer: *NewBuffer(x, y, width, height)} // This is needed because we do not use the pointer created in NewBuffer sb.renderImpl = func() *ebiten.Image { return sb.render() } for _, action := range actions { sb.AddAction(action) } return sb } func (b *StackBuffer) AddAction(a game.Action) { b.AddLine(a.String()) b.actions = append(b.actions, a) } func (b *StackBuffer) AddHighlight(a game.Action, clr color.Color) { for i, ba := range b.actions { if a == ba { b.highlights[i] = clr } } b.ForceRedraw() } func (b *StackBuffer) ClearHighlights() { b.highlights = make(map[int]color.Color) b.ForceRedraw() } func (b *StackBuffer) RemoveLast() { b.Buffer.RemoveLast() // Remove potential highlight i := len(b.actions) - 1 delete(b.highlights, i) b.actions = b.actions[:i] } func (b *StackBuffer) FindObjectAt(x, y int) any { if !b.Contains(x, y) { return nil } var _y float64 = 0 for i, line := range b.lines[b.pos:] { _, h := text.Measure(line, b.font, b.font.Size*b.lineSpacing) _y += h if int(_y)+b.Y >= y { return b.actions[i] } } return nil }