Store incoming text written to the field as []byte
This commit is contained in:
parent
cee763ff9d
commit
6fd655a094
5 changed files with 208 additions and 19 deletions
|
@ -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
5
testdata/long.txt
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
7
testdata/short.txt
vendored
Normal file
7
testdata/short.txt
vendored
Normal 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.
|
57
textfield.go
57
textfield.go
|
@ -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
140
textfield_test.go
Normal 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()
|
||||
})
|
||||
}
|
||||
}
|
Reference in a new issue