Store incoming text written to the field as []byte

This commit is contained in:
Trevor Slocum 2023-11-17 18:43:56 -08:00
parent cee763ff9d
commit 6fd655a094
5 changed files with 208 additions and 19 deletions

View file

@ -120,8 +120,17 @@ func (f *InputField) handleKeys(keys []ebiten.Key) bool {
for _, key := range keys {
switch key {
case ebiten.KeyBackspace:
if len(f.buffer) > 0 {
f.buffer = f.buffer[:len(f.buffer)-1]
l := len(f.buffer)
if l > 0 {
if len(f.incoming) != 0 {
line := string(f.incoming)
f.incoming = append(f.incoming, []byte(line[:len(line)-1])...)
} else if len(f.buffer[l-1]) == 0 {
f.buffer = f.buffer[:l-1]
} else {
line := string(f.buffer[l-1])
f.buffer[l-1] = []byte(line[:len(line)-1])
}
redraw = true
f.modified = true
f.redraw = true
@ -134,14 +143,15 @@ func (f *InputField) handleKeys(keys []ebiten.Key) bool {
// Clear input buffer.
if accept {
f.buffer = ""
f.buffer = f.buffer[:0]
f.incoming = f.incoming[:0]
f.modified = true
f.redraw = true
redraw = true
}
} else if !f.singleLine {
// Append newline.
f.buffer += "\n"
f.incoming = append(f.incoming, '\n')
f.modified = true
f.redraw = true
redraw = true

5
testdata/long.txt vendored Normal file
View file

@ -0,0 +1,5 @@
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

7
testdata/short.txt vendored Normal file
View file

@ -0,0 +1,7 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

View file

@ -1,6 +1,7 @@
package messeji
import (
"bytes"
"image"
"image/color"
"runtime/debug"
@ -51,8 +52,11 @@ 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
// 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
@ -63,7 +67,7 @@ type TextField struct {
// 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 is the content of the field after applying wrapping.
bufferWrapped []string
// bufferSize is the size (in pixels) of the entire text buffer. When single
@ -219,7 +223,12 @@ func (f *TextField) Text() string {
f.Lock()
defer f.Unlock()
return f.buffer
if f.modified {
f.bufferModified()
f.modified = false
}
return string(bytes.Join(f.buffer, []byte("\n")))
}
// SetText sets the text in the field.
@ -227,7 +236,8 @@ func (f *TextField) SetText(text string) {
f.Lock()
defer f.Unlock()
f.buffer = text
f.buffer = f.buffer[:0]
f.incoming = append(f.incoming[:0], []byte(text)...)
f.modified = true
f.redraw = true
}
@ -476,7 +486,7 @@ func (f *TextField) Write(p []byte) (n int, err error) {
}
func (f *TextField) _write(p []byte) (n int, err error) {
f.buffer += string(p)
f.incoming = append(f.incoming, p...)
f.modified = true
f.redraw = true
return len(p), nil
@ -650,10 +660,9 @@ func (f *TextField) fontUpdated() {
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", "")
buffer := f.prefix + string(bytes.Join(f.buffer, nil)) + f.suffix
bounds, _ := boundString(f.face, buffer)
f.bufferWrapped = []string{buffer}
@ -666,14 +675,17 @@ func (f *TextField) wrapContent(withScrollBar bool) {
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
bufferLen := len(f.buffer)
for i, lineBytes := range f.buffer {
var line string
if i == 0 {
line = f.prefix + string(lineBytes)
} else {
line = string(lineBytes)
}
if i == bufferLen-1 {
line += f.suffix
}
l := len(line)
availableWidth := w - (f.padding * 2)
var start int
@ -852,6 +864,21 @@ func (f *TextField) drawImage() {
}
func (f *TextField) bufferModified() {
line := len(f.buffer) - 1
if line < 0 {
line = 0
f.buffer = append(f.buffer, nil)
}
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]
f.drawImage()
f.redraw = false

140
textfield_test.go Normal file
View file

@ -0,0 +1,140 @@
package messeji
import (
"embed"
"fmt"
"image"
"log"
"sync"
"testing"
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
)
//go:embed testdata
var testDataFS embed.FS
var testTextField *TextField
func TestWrapContent(t *testing.T) {
testCases := []struct {
long bool // Short or long text.
wordWrap bool // Enable wordwrap.
}{
{false, false},
{false, true},
{true, false},
{true, true},
}
tt, err := opentype.Parse(fonts.MPlus1pRegular_ttf)
if err != nil {
log.Fatal(err)
}
const size = 24
const dpi = 72
face, err := opentype.NewFace(tt, &opentype.FaceOptions{
Size: size,
DPI: dpi,
Hinting: font.HintingFull,
})
if err != nil {
log.Fatal(err)
}
testRect := image.Rect(0, 0, 200, 400)
for _, c := range testCases {
var name string
if !c.long {
name = "short"
} else {
name = "long"
}
content, err := testDataFS.ReadFile(fmt.Sprintf("testdata/%s.txt", name))
if err != nil {
t.Errorf("failed to open testdata: %s", err)
}
if !c.wordWrap {
name += "/wrapChar"
} else {
name += "/wrapWord"
}
t.Run(name, func(t *testing.T) {
textField := NewTextField(face, &sync.Mutex{})
testTextField = textField
textField.SetRect(testRect)
textField.SetWordWrap(c.wordWrap)
textField.Write(content)
textField.bufferModified()
})
}
}
func BenchmarkWrapContent(b *testing.B) {
testCases := []struct {
long bool // Short or long text.
wordWrap bool // Enable wordwrap.
}{
{false, false},
{false, true},
{true, false},
{true, true},
}
tt, err := opentype.Parse(fonts.MPlus1pRegular_ttf)
if err != nil {
log.Fatal(err)
}
const size = 24
const dpi = 72
face, err := opentype.NewFace(tt, &opentype.FaceOptions{
Size: size,
DPI: dpi,
Hinting: font.HintingFull,
})
if err != nil {
log.Fatal(err)
}
testRect := image.Rect(0, 0, 200, 400)
for _, c := range testCases {
var name string
if !c.long {
name = "short"
} else {
name = "long"
}
content, err := testDataFS.ReadFile(fmt.Sprintf("testdata/%s.txt", name))
if err != nil {
b.Errorf("failed to open testdata: %s", err)
}
if !c.wordWrap {
name += "/wrapChar"
} else {
name += "/wrapWord"
}
textField := NewTextField(face, &sync.Mutex{})
testTextField = textField
textField.SetRect(testRect)
textField.SetWordWrap(c.wordWrap)
b.Run(name, func(b *testing.B) {
textField.Write(content)
textField.bufferModified()
})
}
}