diff --git a/widget/editor.go b/widget/editor.go index 9cb31bc8..39237dbf 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -56,7 +56,10 @@ type Editor struct { // all characters are allowed. 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 blinkStart time.Time 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 !ke.Modifiers.Contain(key.ModShift) { + e.scratch = e.text.Text(e.scratch) e.events = append(e.events, SubmitEvent{ - Text: e.text.Text(), + Text: string(e.scratch), }) continue } @@ -358,8 +362,9 @@ func (e *Editor) processKey(gtx layout.Context) { if e.text.Changed() { e.events = append(e.events, ChangeEvent{}) } + e.scratch = e.text.Text(e.scratch) 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(). @@ -450,7 +455,8 @@ func (e *Editor) command(gtx layout.Context, k key.Event) { } // Copy or Cut selection -- ignored if nothing selected. 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) if k.Name == "X" && !e.ReadOnly { e.Delete(1) @@ -686,7 +692,8 @@ func (e *Editor) Len() int { // Text returns the contents of the editor. func (e *Editor) Text() string { e.initBuffer() - return e.text.Text() + e.scratch = e.text.Text(e.scratch) + return string(e.scratch) } 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. func (e *Editor) SelectedText() string { 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 diff --git a/widget/selectable.go b/widget/selectable.go index 9cdd1fc6..ee88935c 100644 --- a/widget/selectable.go +++ b/widget/selectable.go @@ -50,8 +50,11 @@ func (s stringSource) ReplaceRunes(byteOffset, runeCount int64, str string) { // Selectable holds text selection state. type Selectable struct { - initialized bool - source stringSource + initialized bool + source stringSource + // scratch is a buffer reused to efficiently read text out of the + // textView. + scratch []byte lastValue string text textView focused bool @@ -127,7 +130,8 @@ func (l *Selectable) SetCaret(start, end int) { // SelectedText returns the currently selected text (if any) from the editor. func (l *Selectable) SelectedText() string { 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 @@ -140,7 +144,8 @@ func (l *Selectable) ClearSelection() { // Text returns the contents of the label. func (l *Selectable) Text() string { 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 @@ -325,7 +330,8 @@ func (e *Selectable) command(gtx layout.Context, k key.Event) { e.text.MoveEnd(selAct) // Copy or Cut selection -- ignored if nothing selected. 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) } // Select all diff --git a/widget/text.go b/widget/text.go index a5694c05..94010b20 100644 --- a/widget/text.go +++ b/widget/text.go @@ -338,11 +338,19 @@ func (e *textView) Len() int { return e.closestToRune(math.MaxInt).runes } -// Text returns the contents of the editor. -func (e *textView) Text() string { +// Text returns the contents of the editor. If the provided buf is large enough, it will +// 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) - b, _ := io.ReadAll(e) - return string(b) + n, _ := io.ReadFull(e, buf) + buf = buf[:n] + return buf } func (e *textView) ScrollBounds() image.Rectangle { @@ -632,19 +640,26 @@ func (e *textView) SetCaret(start, end int) { e.caret.end = e.closestToRune(end).runes } -// SelectedText returns the currently selected text (if any) from the editor. -func (e *textView) SelectedText() string { +// SelectedText returns the currently selected text (if any) from the editor, +// 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) endOff := e.runeOffset(e.caret.end) start := min(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)) // There is no way to reasonably handle a read error here. We rely upon // implementations of textSource to provide other ways to signal errors // if the user cares about that, and here we use whatever data we were // able to read. - return string(buf[:n]) + return buf[:n] } func (e *textView) updateSelection(selAct selectionAction) {