widget: use caller-provided buffers for reading out text

This commit alters the textView API to give callers the option to provide
their own buffers for reading text. This enables some widget usecases to
be zero-allocation if a widget simply needs to examine the contents of the
text without returning it as a string.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
Chris Waldon
2023-01-10 12:17:39 -05:00
committed by Elias Naur
parent 940f0f6021
commit 1eb5c7dbcd
3 changed files with 48 additions and 19 deletions
+14 -6
View File
@@ -56,7 +56,10 @@ type Editor struct {
// all characters are allowed. // all characters are allowed.
Filter string Filter string
buffer *editBuffer buffer *editBuffer
// scratch is a byte buffer that is reused to efficiently read portions of text
// from the textView.
scratch []byte
eventKey int eventKey int
blinkStart time.Time blinkStart time.Time
focused bool focused bool
@@ -320,8 +323,9 @@ func (e *Editor) processKey(gtx layout.Context) {
} }
if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) { if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) {
if !ke.Modifiers.Contain(key.ModShift) { if !ke.Modifiers.Contain(key.ModShift) {
e.scratch = e.text.Text(e.scratch)
e.events = append(e.events, SubmitEvent{ e.events = append(e.events, SubmitEvent{
Text: e.text.Text(), Text: string(e.scratch),
}) })
continue continue
} }
@@ -358,8 +362,9 @@ func (e *Editor) processKey(gtx layout.Context) {
if e.text.Changed() { if e.text.Changed() {
e.events = append(e.events, ChangeEvent{}) e.events = append(e.events, ChangeEvent{})
} }
e.scratch = e.text.Text(e.scratch)
e.events = append(e.events, SubmitEvent{ e.events = append(e.events, SubmitEvent{
Text: e.text.Text(), Text: string(e.scratch),
}) })
} }
// Complete a paste event, initiated by Shortcut-V in Editor.command(). // Complete a paste event, initiated by Shortcut-V in Editor.command().
@@ -450,7 +455,8 @@ func (e *Editor) command(gtx layout.Context, k key.Event) {
} }
// Copy or Cut selection -- ignored if nothing selected. // Copy or Cut selection -- ignored if nothing selected.
case "C", "X": case "C", "X":
if text := e.text.SelectedText(); text != "" { e.scratch = e.text.SelectedText(e.scratch)
if text := string(e.scratch); text != "" {
clipboard.WriteOp{Text: text}.Add(gtx.Ops) clipboard.WriteOp{Text: text}.Add(gtx.Ops)
if k.Name == "X" && !e.ReadOnly { if k.Name == "X" && !e.ReadOnly {
e.Delete(1) e.Delete(1)
@@ -686,7 +692,8 @@ func (e *Editor) Len() int {
// Text returns the contents of the editor. // Text returns the contents of the editor.
func (e *Editor) Text() string { func (e *Editor) Text() string {
e.initBuffer() e.initBuffer()
return e.text.Text() e.scratch = e.text.Text(e.scratch)
return string(e.scratch)
} }
func (e *Editor) SetText(s string) { func (e *Editor) SetText(s string) {
@@ -954,7 +961,8 @@ func (e *Editor) SetCaret(start, end int) {
// SelectedText returns the currently selected text (if any) from the editor. // SelectedText returns the currently selected text (if any) from the editor.
func (e *Editor) SelectedText() string { func (e *Editor) SelectedText() string {
e.initBuffer() e.initBuffer()
return e.text.SelectedText() e.scratch = e.text.SelectedText(e.scratch)
return string(e.scratch)
} }
// ClearSelection clears the selection, by setting the selection end equal to // ClearSelection clears the selection, by setting the selection end equal to
+11 -5
View File
@@ -50,8 +50,11 @@ func (s stringSource) ReplaceRunes(byteOffset, runeCount int64, str string) {
// Selectable holds text selection state. // Selectable holds text selection state.
type Selectable struct { type Selectable struct {
initialized bool initialized bool
source stringSource source stringSource
// scratch is a buffer reused to efficiently read text out of the
// textView.
scratch []byte
lastValue string lastValue string
text textView text textView
focused bool focused bool
@@ -127,7 +130,8 @@ func (l *Selectable) SetCaret(start, end int) {
// SelectedText returns the currently selected text (if any) from the editor. // SelectedText returns the currently selected text (if any) from the editor.
func (l *Selectable) SelectedText() string { func (l *Selectable) SelectedText() string {
l.initialize() l.initialize()
return l.text.SelectedText() l.scratch = l.text.SelectedText(l.scratch)
return string(l.scratch)
} }
// ClearSelection clears the selection, by setting the selection end equal to // ClearSelection clears the selection, by setting the selection end equal to
@@ -140,7 +144,8 @@ func (l *Selectable) ClearSelection() {
// Text returns the contents of the label. // Text returns the contents of the label.
func (l *Selectable) Text() string { func (l *Selectable) Text() string {
l.initialize() l.initialize()
return l.text.Text() l.scratch = l.text.Text(l.scratch)
return string(l.scratch)
} }
// SetText updates the text to s if it does not already contain s. Updating the // SetText updates the text to s if it does not already contain s. Updating the
@@ -325,7 +330,8 @@ func (e *Selectable) command(gtx layout.Context, k key.Event) {
e.text.MoveEnd(selAct) e.text.MoveEnd(selAct)
// Copy or Cut selection -- ignored if nothing selected. // Copy or Cut selection -- ignored if nothing selected.
case "C", "X": case "C", "X":
if text := e.text.SelectedText(); text != "" { e.scratch = e.text.SelectedText(e.scratch)
if text := string(e.scratch); text != "" {
clipboard.WriteOp{Text: text}.Add(gtx.Ops) clipboard.WriteOp{Text: text}.Add(gtx.Ops)
} }
// Select all // Select all
+23 -8
View File
@@ -338,11 +338,19 @@ func (e *textView) Len() int {
return e.closestToRune(math.MaxInt).runes return e.closestToRune(math.MaxInt).runes
} }
// Text returns the contents of the editor. // Text returns the contents of the editor. If the provided buf is large enough, it will
func (e *textView) Text() string { // be filled and returned. Otherwise a new buffer will be allocated.
// Callers can guarantee that buf is large enough by giving it capacity e.Len()*utf8.UTFMax.
func (e *textView) Text(buf []byte) []byte {
size := e.rr.Size()
if cap(buf) < int(size) {
buf = make([]byte, size)
}
buf = buf[:size]
e.Seek(0, io.SeekStart) e.Seek(0, io.SeekStart)
b, _ := io.ReadAll(e) n, _ := io.ReadFull(e, buf)
return string(b) buf = buf[:n]
return buf
} }
func (e *textView) ScrollBounds() image.Rectangle { func (e *textView) ScrollBounds() image.Rectangle {
@@ -632,19 +640,26 @@ func (e *textView) SetCaret(start, end int) {
e.caret.end = e.closestToRune(end).runes e.caret.end = e.closestToRune(end).runes
} }
// SelectedText returns the currently selected text (if any) from the editor. // SelectedText returns the currently selected text (if any) from the editor,
func (e *textView) SelectedText() string { // filling the provided byte slice if it is large enough or allocating and
// returning a new byte slice if the provided one is insufficient.
// Callers can guarantee that the buf is large enough by providing a buffer
// with capacity e.SelectionLen()*utf8.UTFMax.
func (e *textView) SelectedText(buf []byte) []byte {
startOff := e.runeOffset(e.caret.start) startOff := e.runeOffset(e.caret.start)
endOff := e.runeOffset(e.caret.end) endOff := e.runeOffset(e.caret.end)
start := min(startOff, endOff) start := min(startOff, endOff)
end := max(startOff, endOff) end := max(startOff, endOff)
buf := make([]byte, end-start) if cap(buf) < end-start {
buf = make([]byte, end-start)
}
buf = buf[:end-start]
n, _ := e.rr.ReadAt(buf, int64(start)) n, _ := e.rr.ReadAt(buf, int64(start))
// There is no way to reasonably handle a read error here. We rely upon // There is no way to reasonably handle a read error here. We rely upon
// implementations of textSource to provide other ways to signal errors // implementations of textSource to provide other ways to signal errors
// if the user cares about that, and here we use whatever data we were // if the user cares about that, and here we use whatever data we were
// able to read. // able to read.
return string(buf[:n]) return buf[:n]
} }
func (e *textView) updateSelection(selAct selectionAction) { func (e *textView) updateSelection(selAct selectionAction) {