733 lines
16 KiB
Go
733 lines
16 KiB
Go
package messeji
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
"strings"
|
|
"sync"
|
|
"unicode"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
|
"github.com/hajimehoshi/ebiten/v2/text"
|
|
"golang.org/x/image/font"
|
|
)
|
|
|
|
// Alignment specifies how text is aligned within the field.
|
|
type Alignment int
|
|
|
|
const (
|
|
// AlignStart aligns text at the start of the field.
|
|
AlignStart Alignment = 0
|
|
|
|
// AlignCenter aligns text at the center of the field.
|
|
AlignCenter Alignment = 1
|
|
|
|
// AlignEnd aligns text at the end of the field.
|
|
AlignEnd Alignment = 2
|
|
)
|
|
|
|
const (
|
|
initialPadding = 5
|
|
initialScrollWidth = 32
|
|
)
|
|
|
|
var (
|
|
initialForeground = color.RGBA{0, 0, 0, 255}
|
|
initialBackground = color.RGBA{255, 255, 255, 255}
|
|
)
|
|
|
|
// TextField is a text display field. Call Update and Draw when your Game's
|
|
// Update and Draw methods are called.
|
|
//
|
|
// Note: A position and size must be set via SetRect before the field will appear.
|
|
// Keyboard events are not handled by default, and may be enabled via SetHandleKeyboard.
|
|
type TextField struct {
|
|
// r specifies the position and size of the field.
|
|
r image.Rectangle
|
|
|
|
// buffer is the actual content of the field.
|
|
buffer string
|
|
|
|
// prefix is the text shown before the content of the field.
|
|
prefix string
|
|
|
|
// suffix is the text shown after the content of the field.
|
|
suffix string
|
|
|
|
// wordWrap determines whether content is wrapped at word boundaries.
|
|
wordWrap bool
|
|
|
|
// bufferWrapped is the content of the field as it appears on the screen.
|
|
bufferWrapped []string
|
|
|
|
// bufferSize is the size (in pixels) of the entire text buffer. When single
|
|
// line mode is enabled,
|
|
bufferSize int
|
|
|
|
// lineWidths is the size (in pixels) of each line as it appears on the screen.
|
|
lineWidths []int
|
|
|
|
// singleLine is whether the field displays all text on a single line.
|
|
singleLine bool
|
|
|
|
// horizontal is the horizontal alignment of the text within field.
|
|
horizontal Alignment
|
|
|
|
// vertical is the vertical alignment of the text within field.
|
|
vertical Alignment
|
|
|
|
// face is the font face of the text within the field.
|
|
face font.Face
|
|
|
|
// lineHeight is the height of a single line of text.
|
|
lineHeight int
|
|
|
|
// overrideLineHeight is the custom height for a line of text, or 0 to disable.
|
|
overrideLineHeight int
|
|
|
|
// textColor is the color of the text within the field.
|
|
textColor color.Color
|
|
|
|
// backgroundColor is the color of the background of the field.
|
|
backgroundColor color.Color
|
|
|
|
// padding is the amount of padding around the text within the field.
|
|
padding int
|
|
|
|
// follow determines whether the field should automatically scroll to the
|
|
// end when content is added to the buffer.
|
|
follow bool
|
|
|
|
// overflow is whether the content of the field is currently larger than the field.
|
|
overflow bool
|
|
|
|
// offset is the current view offset of the text within the field.
|
|
offset int
|
|
|
|
// handleKeyboard is a flag which, when enabled, causes keyboard input to be handled.
|
|
handleKeyboard bool
|
|
|
|
// modified is a flag which, when enabled, causes bufferModified to be called
|
|
// during the next Draw call.
|
|
modified bool
|
|
|
|
// scrollRect specifies the position and size of the scrolling area.
|
|
scrollRect image.Rectangle
|
|
|
|
// scrollWidth is the width of the scroll bar.
|
|
scrollWidth int
|
|
|
|
// scrollVisible is whether the scroll bar is visible on the screen.
|
|
scrollVisible bool
|
|
|
|
// scrollAutoHide is whether the scroll bar should be automatically hidden
|
|
// when the entire text buffer fits within the screen.
|
|
scrollAutoHide bool
|
|
|
|
// scrollDrag is whether the scroll bar is currently being dragged.
|
|
scrollDrag bool
|
|
|
|
// img is the image of the field.
|
|
img *ebiten.Image
|
|
|
|
// visible is whether the field is visible on the screen.
|
|
visible bool
|
|
|
|
sync.Mutex
|
|
}
|
|
|
|
// NewTextField returns a new TextField. See type documentation for more info.
|
|
func NewTextField(face font.Face) *TextField {
|
|
f := &TextField{
|
|
face: face,
|
|
textColor: initialForeground,
|
|
backgroundColor: initialBackground,
|
|
padding: initialPadding,
|
|
scrollWidth: initialScrollWidth,
|
|
follow: true,
|
|
wordWrap: true,
|
|
scrollVisible: true,
|
|
scrollAutoHide: true,
|
|
visible: true,
|
|
}
|
|
f.setDefaultLineHeight()
|
|
return f
|
|
}
|
|
|
|
// Rect returns the position and size of the field.
|
|
func (f *TextField) Rect() image.Rectangle {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
return f.r
|
|
}
|
|
|
|
// SetRect sets the position and size of the field.
|
|
func (f *TextField) SetRect(r image.Rectangle) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
f.r = r
|
|
f.drawImage()
|
|
}
|
|
|
|
// Text returns the text in the field.
|
|
func (f *TextField) Text() string {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
return f.buffer
|
|
}
|
|
|
|
// SetText sets the text in the field.
|
|
func (f *TextField) SetText(text string) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
f.buffer = text
|
|
f.modified = true
|
|
}
|
|
|
|
// SetPrefix sets the text shown before the content of the field.
|
|
func (f *TextField) SetPrefix(text string) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
f.prefix = text
|
|
f.drawImage()
|
|
}
|
|
|
|
// SetSuffix sets the text shown before the content of the field.
|
|
func (f *TextField) SetSuffix(text string) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
f.suffix = text
|
|
f.drawImage()
|
|
}
|
|
|
|
// SetFollow sets whether the field should automatically scroll to the end when
|
|
// content is added to the buffer.
|
|
func (f *TextField) SetFollow(follow bool) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
f.follow = follow
|
|
}
|
|
|
|
// SetSingleLine sets whether the field displays all text on a single line.
|
|
// When enabled, the field scrolls horizontally. Otherwise, it scrolls vertically.
|
|
func (f *TextField) SetSingleLine(single bool) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
if f.singleLine == single {
|
|
return
|
|
}
|
|
|
|
f.singleLine = single
|
|
f.bufferModified()
|
|
}
|
|
|
|
// SetHorizontal sets the horizontal alignment of the text within the field.
|
|
func (f *TextField) SetHorizontal(h Alignment) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
if f.horizontal == h {
|
|
return
|
|
}
|
|
|
|
f.horizontal = h
|
|
f.bufferModified()
|
|
}
|
|
|
|
// SetVertical sets the veritcal alignment of the text within the field.
|
|
func (f *TextField) SetVertical(v Alignment) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
if f.vertical == v {
|
|
return
|
|
}
|
|
|
|
f.vertical = v
|
|
f.bufferModified()
|
|
}
|
|
|
|
// LineHeight returns the line height for the field.
|
|
func (f *TextField) LineHeight() int {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
if f.overrideLineHeight != 0 {
|
|
return f.overrideLineHeight
|
|
}
|
|
return f.lineHeight
|
|
}
|
|
|
|
// SetLineHeight sets a custom line height for the field. Setting a line
|
|
// height of 0 restores the automatic line height detection based on the font.
|
|
func (f *TextField) SetLineHeight(height int) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
f.overrideLineHeight = height
|
|
}
|
|
|
|
// SetForegroundColor sets the color of the text within the field.
|
|
func (f *TextField) SetForegroundColor(c color.Color) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
f.textColor = c
|
|
}
|
|
|
|
// SetBackgroundColor sets the color of the background of the field.
|
|
func (f *TextField) SetBackgroundColor(c color.Color) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
f.backgroundColor = c
|
|
}
|
|
|
|
// SetFont sets the font face of the text within the field.
|
|
func (f *TextField) SetFont(face font.Face) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
f.face = face
|
|
f.setDefaultLineHeight()
|
|
}
|
|
|
|
// Padding returns the amount of padding around the text within the field.
|
|
func (f *TextField) Padding() int {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
return f.padding
|
|
}
|
|
|
|
// SetPadding sets the amount of padding around the text within the field.
|
|
func (f *TextField) SetPadding(padding int) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
f.padding = padding
|
|
}
|
|
|
|
// Visible returns whether the field is currently visible on the screen.
|
|
func (f *TextField) Visible() bool {
|
|
return f.visible
|
|
}
|
|
|
|
// SetVisible sets whether the field is visible on the screen.
|
|
func (f *TextField) SetVisible(visible bool) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
if f.visible == visible {
|
|
return
|
|
}
|
|
|
|
f.visible = visible
|
|
if visible {
|
|
f.drawImage()
|
|
}
|
|
}
|
|
|
|
// SetScrollBarWidth sets the width of the scroll bar.
|
|
func (f *TextField) SetScrollBarWidth(width int) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
if f.scrollWidth == width {
|
|
return
|
|
}
|
|
|
|
f.scrollWidth = width
|
|
f.drawImage()
|
|
}
|
|
|
|
// SetScrollBarVisible sets whether the scroll bar is visible on the screen.
|
|
func (f *TextField) SetScrollBarVisible(scrollVisible bool) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
if f.scrollVisible == scrollVisible {
|
|
return
|
|
}
|
|
|
|
f.scrollVisible = scrollVisible
|
|
f.drawImage()
|
|
}
|
|
|
|
// SetAutoHideScrollBar sets whether the scroll bar is automatically hidden
|
|
// when the entire text buffer is visible.
|
|
func (f *TextField) SetAutoHideScrollBar(autoHide bool) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
if f.scrollAutoHide == autoHide {
|
|
return
|
|
}
|
|
|
|
f.scrollAutoHide = autoHide
|
|
f.drawImage()
|
|
}
|
|
|
|
// WordWrap returns the current text wrap mode.
|
|
func (f *TextField) WordWrap() bool {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
return f.wordWrap
|
|
}
|
|
|
|
// SetWordWrap sets a flag which, when enabled, causes text to wrap without breaking words.
|
|
func (f *TextField) SetWordWrap(wrap bool) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
if f.wordWrap == wrap {
|
|
return
|
|
}
|
|
|
|
f.wordWrap = wrap
|
|
f.drawImage()
|
|
}
|
|
|
|
// SetHandleKeyboard sets a flag controlling whether keyboard input should be handled
|
|
// by the field. This can be used to facilitate focus changes between multiple inputs.
|
|
func (f *TextField) SetHandleKeyboard(handle bool) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
f.handleKeyboard = handle
|
|
}
|
|
|
|
// Write writes to the field's buffer.
|
|
func (f *TextField) Write(p []byte) (n int, err error) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
f.buffer += string(p)
|
|
f.modified = true
|
|
return len(p), nil
|
|
}
|
|
|
|
// Update updates the field. This function should be called when
|
|
// Game.Update is called.
|
|
func (f *TextField) Update() error {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
if !f.visible || rectIsZero(f.r) {
|
|
return nil
|
|
}
|
|
|
|
var redraw bool
|
|
|
|
// Handle keyboard PageUp/PageDown.
|
|
if f.handleKeyboard {
|
|
offsetAmount := 0
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyPageUp) {
|
|
offsetAmount = -100
|
|
} else if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) {
|
|
offsetAmount = 100
|
|
}
|
|
if offsetAmount != 0 {
|
|
f.offset += offsetAmount
|
|
f.clampOffset()
|
|
redraw = true
|
|
}
|
|
}
|
|
|
|
// Handle mouse wheel.
|
|
_, scrollY := ebiten.Wheel()
|
|
if scrollY != 0 {
|
|
x, y := ebiten.CursorPosition()
|
|
p := image.Point{x, y}
|
|
if p.In(f.r) {
|
|
const offsetAmount = 25
|
|
f.offset -= int(scrollY * offsetAmount)
|
|
f.clampOffset()
|
|
redraw = true
|
|
}
|
|
}
|
|
|
|
// Handle scroll bar click (and drag).
|
|
if f.showScrollBar() {
|
|
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) || f.scrollDrag {
|
|
x, y := ebiten.CursorPosition()
|
|
p := image.Point{x - f.r.Min.X, y - f.r.Min.Y}
|
|
if f.scrollDrag || p.In(f.scrollRect) {
|
|
dragY := y - f.r.Min.Y - f.scrollWidth/4
|
|
if dragY < 0 {
|
|
dragY = 0
|
|
} else if dragY > f.scrollRect.Dy() {
|
|
dragY = f.scrollRect.Dy()
|
|
}
|
|
pct := float64(dragY) / float64(f.scrollRect.Dy()-f.scrollWidth/2)
|
|
if pct > 1 {
|
|
pct = 1
|
|
}
|
|
h := f.r.Dy()
|
|
f.offset = int(float64(f.bufferSize-h) * pct)
|
|
redraw = true
|
|
f.scrollDrag = true
|
|
}
|
|
if !ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
|
|
f.scrollDrag = false
|
|
}
|
|
}
|
|
}
|
|
|
|
if redraw {
|
|
f.drawImage()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Draw draws the field on the screen. This function should be called
|
|
// when Game.Draw is called.
|
|
func (f *TextField) Draw(screen *ebiten.Image) {
|
|
f.Lock()
|
|
defer f.Unlock()
|
|
|
|
if f.modified {
|
|
f.bufferModified()
|
|
f.modified = false
|
|
}
|
|
|
|
if !f.visible || rectIsZero(f.r) || f.img == nil {
|
|
return
|
|
}
|
|
|
|
op := &ebiten.DrawImageOptions{}
|
|
op.GeoM.Translate(float64(f.r.Min.X), float64(f.r.Min.Y))
|
|
screen.DrawImage(f.img, op)
|
|
}
|
|
|
|
func (f *TextField) setDefaultLineHeight() {
|
|
lineBounds := text.BoundString(f.face, "ATZgpq.")
|
|
f.lineHeight = lineBounds.Dy() + 5
|
|
}
|
|
|
|
func (f *TextField) wrapContent(withScrollBar bool) {
|
|
f.lineWidths = f.lineWidths[:0]
|
|
buffer := f.prefix + f.buffer + f.suffix
|
|
|
|
if f.singleLine {
|
|
buffer = strings.ReplaceAll(buffer, "\n", "")
|
|
bounds := text.BoundString(f.face, buffer)
|
|
|
|
f.bufferWrapped = []string{buffer}
|
|
f.lineWidths = append(f.lineWidths, bounds.Dx())
|
|
return
|
|
}
|
|
|
|
w := f.r.Dx()
|
|
if withScrollBar {
|
|
w -= f.scrollWidth
|
|
}
|
|
f.bufferWrapped = f.bufferWrapped[:0]
|
|
for _, line := range strings.Split(buffer, "\n") {
|
|
// BoundString returns 0 for strings containing only whitespace.
|
|
if strings.TrimSpace(line) == "" {
|
|
f.bufferWrapped = append(f.bufferWrapped, "")
|
|
f.lineWidths = append(f.lineWidths, 0)
|
|
continue
|
|
}
|
|
|
|
l := len(line)
|
|
var start int
|
|
var end int
|
|
var initialEnd int
|
|
for start < l {
|
|
for end = l; end > start; end-- {
|
|
initialEnd = end
|
|
|
|
bounds := text.BoundString(f.face, line[start:end])
|
|
if bounds.Dx() < w-(f.padding*2)-2 {
|
|
if f.wordWrap {
|
|
if end < l && !unicode.IsSpace(rune(line[end])) {
|
|
for endOffset := 0; endOffset < end-start; endOffset++ {
|
|
if unicode.IsSpace(rune(line[end-endOffset])) {
|
|
end = end - endOffset
|
|
if end < l-1 {
|
|
end++
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if end != initialEnd && f.horizontal != AlignStart {
|
|
bounds = text.BoundString(f.face, line[start:end])
|
|
}
|
|
|
|
f.bufferWrapped = append(f.bufferWrapped, line[start:end])
|
|
f.lineWidths = append(f.lineWidths, bounds.Dx())
|
|
break
|
|
}
|
|
}
|
|
start = end
|
|
}
|
|
}
|
|
}
|
|
|
|
// drawContent draws the text buffer to img.
|
|
func (f *TextField) drawContent() (overflow bool) {
|
|
f.img.Fill(f.backgroundColor)
|
|
|
|
fieldWidth := f.r.Dx()
|
|
fieldHeight := f.r.Dy()
|
|
if f.showScrollBar() {
|
|
fieldWidth -= f.scrollWidth
|
|
}
|
|
lines := len(f.bufferWrapped)
|
|
|
|
h := f.r.Dy()
|
|
lineHeight := f.overrideLineHeight
|
|
if lineHeight == 0 {
|
|
lineHeight = f.lineHeight
|
|
}
|
|
lineOffset := lineHeight / 3
|
|
f.bufferSize = 0
|
|
for i, line := range f.bufferWrapped {
|
|
lineX := f.padding
|
|
lineY := f.padding
|
|
|
|
lineY += lineHeight*(i+1) - lineOffset
|
|
|
|
if f.singleLine {
|
|
bounds := text.BoundString(f.face, line)
|
|
f.bufferSize = bounds.Dx() + f.padding*4
|
|
} else {
|
|
f.bufferSize = lineY + lineOffset + f.padding
|
|
}
|
|
|
|
if lineY < 0 || lineY >= h-(f.padding*2) {
|
|
overflow = true
|
|
}
|
|
|
|
// Skip drawing off-screen lines.
|
|
if lineY < 0 || lineY-lineHeight > f.offset+h {
|
|
continue
|
|
}
|
|
|
|
if f.singleLine {
|
|
lineX -= f.offset
|
|
} else {
|
|
lineY -= f.offset
|
|
}
|
|
|
|
if f.horizontal == AlignCenter {
|
|
lineX = (fieldWidth - f.lineWidths[i]) / 2
|
|
} else if f.horizontal == AlignEnd {
|
|
lineX = (fieldWidth - f.lineWidths[i]) - f.padding*2
|
|
}
|
|
|
|
if f.vertical == AlignCenter && lineHeight*lines <= h {
|
|
lineY = (fieldHeight-(lineHeight*lines))/2 + lineHeight*(i+1) - lineOffset
|
|
} else if f.vertical == AlignEnd && lineHeight*lines <= h {
|
|
lineY = (fieldHeight - lineHeight*i) - f.padding*2
|
|
}
|
|
|
|
text.Draw(f.img, line, f.face, lineX, lineY, f.textColor)
|
|
}
|
|
|
|
return overflow
|
|
}
|
|
|
|
func (f *TextField) clampOffset() {
|
|
fieldSize := f.r.Dy()
|
|
if f.singleLine {
|
|
fieldSize = f.r.Dx()
|
|
}
|
|
|
|
if f.offset > f.bufferSize-fieldSize {
|
|
f.offset = f.bufferSize - fieldSize
|
|
}
|
|
if f.offset < 0 {
|
|
f.offset = 0
|
|
}
|
|
}
|
|
|
|
func (f *TextField) showScrollBar() bool {
|
|
return !f.singleLine && f.scrollVisible && (f.overflow || !f.scrollAutoHide)
|
|
}
|
|
|
|
// drawImage draws the field to img (caching it for future draws).
|
|
func (f *TextField) drawImage() {
|
|
if rectIsZero(f.r) {
|
|
f.img = nil
|
|
return
|
|
}
|
|
|
|
w, h := f.r.Dx(), f.r.Dy()
|
|
var newImage bool
|
|
if f.img == nil {
|
|
newImage = true
|
|
} else {
|
|
imgRect := f.img.Bounds()
|
|
imgW, imgH := imgRect.Dx(), imgRect.Dy()
|
|
newImage = imgW != w || imgH != h
|
|
}
|
|
if newImage {
|
|
f.img = ebiten.NewImage(w, h)
|
|
}
|
|
|
|
f.wrapContent(false)
|
|
f.overflow = f.drawContent()
|
|
if f.showScrollBar() {
|
|
f.wrapContent(true)
|
|
f.drawContent()
|
|
}
|
|
|
|
// Draw scrollbar.
|
|
if f.showScrollBar() {
|
|
scrollAreaX, scrollAreaY := w-f.scrollWidth, 0
|
|
f.scrollRect = image.Rect(scrollAreaX, scrollAreaY, scrollAreaX+f.scrollWidth, h)
|
|
|
|
scrollBarH := f.scrollWidth / 2
|
|
if scrollBarH < 4 {
|
|
scrollBarH = 4
|
|
}
|
|
|
|
scrollX, scrollY := w-f.scrollWidth, 0
|
|
pct := float64(f.offset) / float64(f.bufferSize-h)
|
|
scrollY += int(float64(h-scrollBarH) * pct)
|
|
scrollBarRect := image.Rect(scrollX, scrollY, scrollX+f.scrollWidth, scrollY+scrollBarH)
|
|
|
|
f.img.SubImage(f.scrollRect).(*ebiten.Image).Fill(color.RGBA{200, 200, 200, 255})
|
|
f.img.SubImage(scrollBarRect).(*ebiten.Image).Fill(color.RGBA{108, 108, 108, 255})
|
|
}
|
|
}
|
|
|
|
func (f *TextField) bufferModified() {
|
|
f.drawImage()
|
|
|
|
if !f.follow {
|
|
return
|
|
}
|
|
fieldSize := f.r.Dy()
|
|
if f.singleLine {
|
|
fieldSize = f.r.Dx()
|
|
}
|
|
offset := f.bufferSize - fieldSize
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
if offset != f.offset {
|
|
f.offset = offset
|
|
f.drawImage()
|
|
}
|
|
}
|
|
|
|
func rectIsZero(r image.Rectangle) bool {
|
|
return r == image.Rectangle{}
|
|
}
|