This repository has been archived on 2024-01-17. You can view files and clone it, but cannot push or open issues or pull requests.
messeji/textfield.go

1093 lines
24 KiB
Go

package messeji
import (
"bytes"
"image"
"image/color"
"math"
"runtime/debug"
"strings"
"sync"
"time"
"unicode"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
// 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}
initialScrollArea = color.RGBA{200, 200, 200, 255}
initialScrollHandle = color.RGBA{108, 108, 108, 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 text buffer split by newline characters.
buffer [][]byte
// incoming is text to be written to the buffer that has not yet been wrapped.
incoming []byte
// 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 after applying wrapping.
bufferWrapped []string
// wrapStart is the first line number in bufferWrapped which corresponds
// to the last line number in the actual text buffer.
wrapStart int
// needWrap is the first line number in the actual text buffer that needs to be wrapped.
needWrap int
// wrapScrollBar is whether the scroll bar was visible the last time the field was redrawn.
wrapScrollBar bool
// 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
// faceMutex is the lock which is held whenever utilizing the font face.
faceMutex *sync.Mutex
// 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
// lineOffset is the offset of the baseline current font.
lineOffset 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, relative to the top.
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
// scrollAreaColor is the color of the scroll area.
scrollAreaColor color.RGBA
// scrollHandleColor is the color of the scroll handle.
scrollHandleColor color.RGBA
// 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
// redraw is whether the field needs to be redrawn.
redraw bool
// keyBuffer is a buffer of key press events.
keyBuffer []ebiten.Key
// keyBuffer is a buffer of runes from key presses.
runeBuffer []rune
sync.Mutex
}
// NewTextField returns a new TextField. See type documentation for more info.
func NewTextField(face font.Face, faceMutex *sync.Mutex) *TextField {
if faceMutex == nil {
faceMutex = &sync.Mutex{}
}
f := &TextField{
face: face,
faceMutex: faceMutex,
textColor: initialForeground,
backgroundColor: initialBackground,
padding: initialPadding,
scrollWidth: initialScrollWidth,
scrollAreaColor: initialScrollArea,
scrollHandleColor: initialScrollHandle,
follow: true,
wordWrap: true,
scrollVisible: true,
scrollAutoHide: true,
visible: true,
redraw: true,
}
f.faceMutex.Lock()
defer f.faceMutex.Unlock()
f.fontUpdated()
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()
if f.r.Eq(r) {
return
}
if f.r.Dx() != r.Dx() || f.r.Dy() != r.Dy() {
f.bufferWrapped = f.bufferWrapped[:0]
f.lineWidths = f.lineWidths[:0]
f.needWrap = 0
f.wrapStart = 0
f.modified = true
}
f.r = r
}
// Text returns the text in the field.
func (f *TextField) Text() string {
f.Lock()
defer f.Unlock()
f.processIncoming()
return string(bytes.Join(f.buffer, []byte("\n")))
}
// SetText sets the text in the field.
func (f *TextField) SetText(text string) {
f.Lock()
defer f.Unlock()
f.buffer = f.buffer[:0]
f.bufferWrapped = f.bufferWrapped[:0]
f.lineWidths = f.lineWidths[:0]
f.needWrap = 0
f.wrapStart = 0
f.incoming = append(f.incoming[:0], []byte(text)...)
f.modified = true
f.redraw = 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.needWrap = 0
f.wrapStart = 0
f.modified = true
}
// 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.needWrap = 0
f.wrapStart = 0
f.modified = true
}
// 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.needWrap = 0
f.wrapStart = 0
f.modified = true
}
// 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.needWrap = 0
f.wrapStart = 0
f.modified = true
}
// 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.needWrap = 0
f.wrapStart = 0
f.modified = true
}
// 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
f.needWrap = 0
f.wrapStart = 0
f.modified = true
}
// 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
f.modified = true
}
// 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
f.modified = true
}
// SetFont sets the font face of the text within the field.
func (f *TextField) SetFont(face font.Face, mutex *sync.Mutex) {
if mutex == nil {
mutex = &sync.Mutex{}
}
f.Lock()
defer f.Unlock()
mutex.Lock()
defer mutex.Unlock()
f.face = face
f.faceMutex = mutex
f.fontUpdated()
f.needWrap = 0
f.wrapStart = 0
f.modified = true
}
// 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
f.needWrap = 0
f.wrapStart = 0
f.modified = true
}
// 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.redraw = true
}
}
// 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.needWrap = 0
f.wrapStart = 0
f.modified = true
}
// SetScrollBarColors sets the color of the scroll bar area and handle.
func (f *TextField) SetScrollBarColors(area color.RGBA, handle color.RGBA) {
f.Lock()
defer f.Unlock()
f.scrollAreaColor, f.scrollHandleColor = area, handle
f.redraw = true
}
// 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.needWrap = 0
f.wrapStart = 0
f.modified = true
}
// 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.needWrap = 0
f.wrapStart = 0
f.modified = true
}
// 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.needWrap = 0
f.wrapStart = 0
f.modified = true
}
// 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()
return f._write(p)
}
func (f *TextField) _write(p []byte) (n int, err error) {
f.incoming = append(f.incoming, p...)
f.modified = true
f.redraw = true
return len(p), nil
}
// HandleKeyboardEvent passes the provided key or rune to the TextField.
func (f *TextField) HandleKeyboardEvent(key ebiten.Key, r rune) (handled bool, err error) {
f.Lock()
defer f.Unlock()
if !f.visible || rectIsZero(f.r) || !f.handleKeyboard {
return false, nil
}
return f._handleKeyboardEvent(key, r)
}
func (f *TextField) _handleKeyboardEvent(key ebiten.Key, r rune) (handled bool, err error) {
if key != -1 {
// Handle keyboard PageUp/PageDown.
offsetAmount := 0
switch key {
case ebiten.KeyPageUp:
offsetAmount = 100
case ebiten.KeyPageDown:
offsetAmount = -100
}
if offsetAmount != 0 {
f.offset += offsetAmount
f.clampOffset()
f.redraw = true
return true, nil
}
return true, err
}
return true, nil
}
func (f *TextField) HandleMouseEvent(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) {
f.Lock()
defer f.Unlock()
if !f.visible || rectIsZero(f.r) {
return false, nil
}
return f._handleMouseEvent(cursor, pressed, clicked)
}
func (f *TextField) _handleMouseEvent(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) {
if !cursor.In(f.r) {
return false, nil
}
// Handle mouse wheel.
_, scrollY := ebiten.Wheel()
if scrollY != 0 {
const offsetAmount = 25
f.offset += int(scrollY * offsetAmount)
f.clampOffset()
f.redraw = true
}
// Handle scroll bar click (and drag).
if !f.showScrollBar() {
return true, nil
} else if pressed || f.scrollDrag {
p := image.Point{cursor.X - f.r.Min.X, cursor.Y - f.r.Min.Y}
if pressed && p.In(f.scrollRect) {
dragY := cursor.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 < 0 {
pct = 0
} else if pct > 1 {
pct = 1
}
h := f.r.Dy()
f.offset = -int(float64(f.bufferSize-h-f.lineOffset+f.padding*2) * pct)
f.clampOffset()
f.redraw = true
f.scrollDrag = true
} else if !pressed {
f.scrollDrag = false
}
}
return true, 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
}
f.keyBuffer = inpututil.AppendJustPressedKeys(f.keyBuffer[:0])
for _, key := range f.keyBuffer {
handled, err := f._handleKeyboardEvent(key, 0)
if err != nil {
return err
} else if handled {
f.redraw = true
}
}
f.runeBuffer = ebiten.AppendInputChars(f.runeBuffer[:0])
for _, r := range f.runeBuffer {
handled, err := f._handleKeyboardEvent(-1, r)
if err != nil {
return err
} else if handled {
f.redraw = true
}
}
cx, cy := ebiten.CursorPosition()
if cx != 0 || cy != 0 {
handled, err := f._handleMouseEvent(image.Point{X: cx, Y: cy}, ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft), inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft))
if err != nil {
return err
} else if handled {
f.redraw = true
}
}
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.faceMutex.Lock()
f.bufferModified()
f.modified = false
f.faceMutex.Unlock()
}
if !f.visible || rectIsZero(f.r) {
return
}
if f.redraw {
f.faceMutex.Lock()
f.drawImage()
f.redraw = false
f.faceMutex.Unlock()
}
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(f.r.Min.X), float64(f.r.Min.Y))
screen.DrawImage(f.img, op)
}
func (f *TextField) fontUpdated() {
m := f.face.Metrics()
f.lineHeight = m.Height.Ceil()
f.lineOffset = m.CapHeight.Ceil()
if f.lineOffset < 0 {
f.lineOffset *= -1
}
}
func (f *TextField) wrapContent(withScrollBar bool) {
if withScrollBar != f.wrapScrollBar {
f.needWrap = 0
f.wrapStart = 0
} else if f.needWrap == -1 {
return
}
f.wrapScrollBar = withScrollBar
if f.singleLine || len(f.buffer) == 0 {
buffer := f.prefix + string(bytes.Join(f.buffer, nil)) + f.suffix
bounds, _ := boundString(f.face, buffer)
f.bufferWrapped = []string{buffer}
f.wrapStart = 0
f.lineWidths = append(f.lineWidths[:0], (bounds.Max.X - bounds.Min.X).Floor())
f.needWrap = -1
return
}
w := f.r.Dx()
if withScrollBar {
w -= f.scrollWidth
}
bufferLen := len(f.buffer)
j := f.wrapStart
for i := f.needWrap; i < bufferLen; i++ {
var line string
if i == 0 {
line = f.prefix + string(f.buffer[i])
} else {
line = string(f.buffer[i])
}
if i == bufferLen-1 {
line += f.suffix
}
l := len(line)
availableWidth := w - (f.padding * 2)
f.wrapStart = j
// BoundString returns 0 for strings containing only whitespace.
if strings.TrimSpace(line) == "" {
if len(f.bufferWrapped) <= j {
f.bufferWrapped = append(f.bufferWrapped, "")
} else {
f.bufferWrapped[j] = ""
}
if len(f.lineWidths) <= j {
f.lineWidths = append(f.lineWidths, 0)
} else {
f.lineWidths[j] = 0
}
j++
continue
}
var start int
var end int
for start < l {
end = l
var initialEnd int
var bounds fixed.Rectangle26_6
var boundsWidth int
// Chop the line in half until it fits.
for end > start {
initialEnd = end
bounds, _ := boundString(f.face, line[start:end])
boundsWidth := (bounds.Max.X - bounds.Min.X).Floor()
if boundsWidth > availableWidth && end > start+1 {
delta := (end - start) / 2
if delta < 1 {
delta = 1
}
end -= delta
} else {
break
}
}
// Add characters until the line doesn't fit anymore.
lineEnd := end
var lastSpace = -1
for end < l {
initialEnd = end
bounds, _ := boundString(f.face, line[start:end])
boundsWidth := (bounds.Max.X - bounds.Min.X).Floor()
if boundsWidth > availableWidth && end > start+1 {
break
}
lineEnd = end
end++
if unicode.IsSpace(rune(line[lineEnd])) {
lastSpace = lineEnd
}
}
// Apply word wrapping.
if f.wordWrap && lineEnd < l {
if lastSpace == -1 {
// Search for a space going backwards.
end = lineEnd
for offset := 1; offset < end-start-2; offset++ {
if unicode.IsSpace(rune(line[end-offset])) {
lastSpace = end - offset
break
}
}
}
if lastSpace != -1 {
end = lastSpace + 1
} else {
end = lineEnd
}
} else {
end = lineEnd
}
if boundsWidth == 0 || end != initialEnd {
bounds, _ = boundString(f.face, line[start:end])
boundsWidth = (bounds.Max.X - bounds.Min.X).Floor()
}
if len(f.bufferWrapped) <= j {
f.bufferWrapped = append(f.bufferWrapped, line[start:end])
} else {
f.bufferWrapped[j] = line[start:end]
}
if len(f.lineWidths) <= j {
f.lineWidths = append(f.lineWidths, boundsWidth)
} else {
f.lineWidths[j] = boundsWidth
}
j++
start = end
}
}
if len(f.bufferWrapped) >= j {
f.bufferWrapped = f.bufferWrapped[:j]
}
f.needWrap = -1
}
// 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
}
var firstVisible, lastVisible int
firstVisible = 0
lastVisible = len(f.bufferWrapped) - 1
if !f.singleLine {
firstVisible = (f.offset * -1) / f.lineHeight
lastVisible = firstVisible + (f.r.Dy() / f.lineHeight) + 1
if lastVisible > len(f.bufferWrapped)-1 {
lastVisible = len(f.bufferWrapped) - 1
}
}
// Calculate buffer size (width for single-line fields or height for multi-line fields).
if f.singleLine {
bounds, _ := boundString(f.face, f.bufferWrapped[firstVisible])
f.bufferSize = (bounds.Max.X - bounds.Min.X).Floor()
} else {
f.bufferSize = (len(f.bufferWrapped)) * lineHeight
}
for i := firstVisible; i <= lastVisible; i++ {
line := f.bufferWrapped[i]
lineX := f.padding
lineY := 1 + f.padding + f.lineOffset + lineHeight*i
// Calculate whether the line overflows the visible area.
lineOverflows := lineY < 0 || lineY >= h-(f.padding*2)
if lineOverflows {
overflow = true
}
// Skip drawing off-screen lines.
if lineY < 0 {
continue
}
// Apply scrolling transformation.
if f.singleLine {
lineX += f.offset
} else {
lineY += f.offset
}
// Align horizontally.
if f.horizontal == AlignCenter {
lineX = (fieldWidth - f.lineWidths[i]) / 2
} else if f.horizontal == AlignEnd {
lineX = (fieldWidth - f.lineWidths[i]) - f.padding - 1
}
// Align vertically.
totalHeight := f.lineOffset + lineHeight*(lines-1)
if f.vertical == AlignCenter && totalHeight <= h {
lineY = fieldHeight/2 - totalHeight/2 + f.lineOffset + (lineHeight * (i))
} else if f.vertical == AlignEnd && totalHeight <= h {
lineY = (fieldHeight - lineHeight*i) - f.padding
}
// Draw line.
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()
}
minSize := -(f.bufferSize - fieldSize + f.padding*2)
if !f.singleLine {
minSize += f.lineOffset
}
if minSize > 0 {
minSize = 0
}
maxSize := 0
if f.offset < minSize {
f.offset = minSize
} else if f.offset > maxSize {
f.offset = maxSize
}
}
func (f *TextField) showScrollBar() bool {
return !f.singleLine && f.scrollVisible && (f.overflow || !f.scrollAutoHide)
}
func (f *TextField) wrap() {
showScrollBar := f.showScrollBar()
f.wrapContent(showScrollBar)
f.overflow = f.drawContent()
if f.showScrollBar() != showScrollBar {
f.wrapContent(!showScrollBar)
f.drawContent()
}
}
// 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.wrap()
// 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-f.lineOffset+f.padding*2)
scrollY += int(float64(h-scrollBarH) * pct)
scrollBarRect := image.Rect(scrollX, scrollY, scrollX+f.scrollWidth, scrollY+scrollBarH)
f.img.SubImage(f.scrollRect).(*ebiten.Image).Fill(f.scrollAreaColor)
f.img.SubImage(scrollBarRect).(*ebiten.Image).Fill(f.scrollHandleColor)
}
}
func (f *TextField) processIncoming() {
if len(f.incoming) == 0 {
return
}
line := len(f.buffer) - 1
if line < 0 {
line = 0
f.buffer = append(f.buffer, nil)
}
if f.needWrap == -1 {
f.needWrap = line
}
for _, b := range f.incoming {
if b == '\n' {
line++
f.buffer = append(f.buffer, nil)
continue
}
f.buffer[line] = append(f.buffer[line], b)
}
f.incoming = f.incoming[:0]
}
func (f *TextField) bufferModified() {
f.processIncoming()
f.drawImage()
lastOffset := f.offset
if f.follow {
f.offset = -math.MaxInt
f.clampOffset()
if f.offset != lastOffset {
f.drawImage()
}
}
f.redraw = false
}
func rectIsZero(r image.Rectangle) bool {
return r.Dx() == 0 || r.Dy() == 0
}
func boundString(f font.Face, s string) (bounds fixed.Rectangle26_6, advance fixed.Int26_6) {
if strings.TrimSpace(s) == "" {
return fixed.Rectangle26_6{}, 0
}
for i := 0; i < 100; i++ {
bounds, advance = func() (fixed.Rectangle26_6, fixed.Int26_6) {
defer func() {
err := recover()
if err != nil && i == 99 {
debug.PrintStack()
panic("failed to calculate bounds of string '" + s + "'")
}
}()
bounds, advance = font.BoundString(f, s)
return bounds, advance
}()
if !bounds.Empty() {
return bounds, advance
}
time.Sleep(10 * time.Millisecond)
}
return fixed.Rectangle26_6{}, 0
}