widget: [API] change Editor.SelectionLen, Selection, SetCaret, Len to operate in runes

This change uncovered and fixes a bug in nullLayout.

This is an API change; the methods operated in bytes before.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2022-02-05 20:59:27 +01:00
parent 2babf3b997
commit 44e0196173
2 changed files with 29 additions and 78 deletions
+23 -76
View File
@@ -214,11 +214,11 @@ func (e *Editor) processEvents(gtx layout.Context) {
// Can't process events without a shaper. // Can't process events without a shaper.
return return
} }
oldStart, oldLen := min(e.caret.start.ofs, e.caret.end.ofs), e.SelectionLen() oldStart, oldLen := min(e.caret.start.runes, e.caret.end.runes), e.SelectionLen()
e.processPointer(gtx) e.processPointer(gtx)
e.processKey(gtx) e.processKey(gtx)
// Queue a SelectEvent if the selection changed, including if it went away. // Queue a SelectEvent if the selection changed, including if it went away.
if newStart, newLen := min(e.caret.start.ofs, e.caret.end.ofs), e.SelectionLen(); oldStart != newStart || oldLen != newLen { if newStart, newLen := min(e.caret.start.runes, e.caret.end.runes), e.SelectionLen(); oldStart != newStart || oldLen != newLen {
e.events = append(e.events, SelectEvent{}) e.events = append(e.events, SelectEvent{})
} }
} }
@@ -658,9 +658,11 @@ func (e *Editor) PaintCaret(gtx layout.Context) {
} }
} }
// Len is the length of the editor contents. // Len is the length of the editor contents, in runes.
func (e *Editor) Len() int { func (e *Editor) Len() int {
return e.rr.len() e.makeValid()
end := e.closestPosition(combinedPos{runes: math.MaxInt})
return end.runes
} }
// Text returns the contents of the editor. // Text returns the contents of the editor.
@@ -763,49 +765,6 @@ func (e *Editor) CaretCoords() f32.Point {
return f32.Pt(float32(e.caret.start.x)/64, float32(e.caret.start.y)) return f32.Pt(float32(e.caret.start.x)/64, float32(e.caret.start.y))
} }
// offsetToScreenPos takes an offset into the editor text (e.g.
// e.caret.end.ofs) and returns a combinedPos that corresponds to its current
// screen position. Hint is either the zero-value position or the result of
// a previous call with a lower offset.
//
// This function is written this way to take advantage of previous work done
// for offsets after the first. Otherwise you have to start from the top each
// time.
func (e *Editor) offsetToScreenPos(hint combinedPos, offset int) combinedPos {
if hint == (combinedPos{}) {
l := e.lines[0]
hint = combinedPos{
x: align(e.Alignment, l.Width, e.viewSize.X),
y: l.Ascent.Ceil(),
}
}
LOOP:
for {
l := e.lines[hint.lineCol.Y]
for ; hint.lineCol.X < len(l.Layout.Advances); hint.lineCol.X++ {
if hint.ofs >= offset {
break LOOP
}
hint.x += l.Layout.Advances[hint.lineCol.X]
_, s := e.rr.runeAt(hint.ofs)
hint.ofs += s
hint.runes++
}
if lastLine := hint.lineCol.Y == len(e.lines)-1; lastLine || hint.ofs > offset {
break LOOP
}
prevDesc := l.Descent
hint.lineCol.Y++
hint.lineCol.X = 0
l = e.lines[hint.lineCol.Y]
hint.x = align(e.Alignment, l.Width, e.viewSize.X)
hint.y += (prevDesc + l.Ascent).Ceil()
}
return hint
}
// indexPosition returns the latest position from the index no later than pos. // indexPosition returns the latest position from the index no later than pos.
func (e *Editor) indexPosition(pos combinedPos) combinedPos { func (e *Editor) indexPosition(pos combinedPos) combinedPos {
// Initialize index with first caret position. // Initialize index with first caret position.
@@ -1083,7 +1042,7 @@ func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6, line int) combi
// negative distances moves backward. Distances are in runes. // negative distances moves backward. Distances are in runes.
func (e *Editor) MoveCaret(startDelta, endDelta int) { func (e *Editor) MoveCaret(startDelta, endDelta int) {
e.makeValid() e.makeValid()
keepSame := e.caret.start.ofs == e.caret.end.ofs && startDelta == endDelta keepSame := e.caret.start.runes == e.caret.end.runes && startDelta == endDelta
e.caret.start = e.movePos(e.caret.start, startDelta) e.caret.start = e.movePos(e.caret.start, startDelta)
e.caret.xoff = 0 e.caret.xoff = 0
// If they were in the same place, and we're moving them the same distance, // If they were in the same place, and we're moving them the same distance,
@@ -1309,28 +1268,23 @@ func (e *Editor) NumLines() int {
return len(e.lines) return len(e.lines)
} }
// SelectionLen returns the length of the selection, in bytes; it is // SelectionLen returns the length of the selection, in runes; it is
// equivalent to len(e.SelectedText()). // equivalent to utf8.RuneCountInString(e.SelectedText()).
func (e *Editor) SelectionLen() int { func (e *Editor) SelectionLen() int {
return abs(e.caret.start.ofs - e.caret.end.ofs) return abs(e.caret.start.runes - e.caret.end.runes)
} }
// Selection returns the start and end of the selection, as offsets into the // Selection returns the start and end of the selection, as rune offsets.
// editor text. start can be > end. // start can be > end.
func (e *Editor) Selection() (start, end int) { func (e *Editor) Selection() (start, end int) {
return e.caret.start.ofs, e.caret.end.ofs return e.caret.start.runes, e.caret.end.runes
} }
// SetCaret moves the caret to start, and sets the selection end to end. start // SetCaret moves the caret to start, and sets the selection end to end. start
// and end are in bytes, and represent offsets into the editor text. start and // and end are in runes, and represent offsets into the editor text.
// end must be at a rune boundary.
func (e *Editor) SetCaret(start, end int) { func (e *Editor) SetCaret(start, end int) {
e.makeValid() e.makeValid()
// Constrain start and end to [0, e.Len()]. e.caret.start.runes, e.caret.end.runes = start, end
l := e.Len()
start = max(min(start, l), 0)
end = max(min(end, l), 0)
e.caret.start.ofs, e.caret.end.ofs = start, end
e.makeValidCaret() e.makeValidCaret()
e.caret.scroll = true e.caret.scroll = true
e.scroller.Stop() e.scroller.Stop()
@@ -1340,24 +1294,17 @@ func (e *Editor) makeValidCaret(positions ...*combinedPos) {
// Jump through some hoops to order the offsets given to offsetToScreenPos, // Jump through some hoops to order the offsets given to offsetToScreenPos,
// but still be able to update them correctly with the results thereof. // but still be able to update them correctly with the results thereof.
positions = append(positions, &e.caret.start, &e.caret.end) positions = append(positions, &e.caret.start, &e.caret.end)
sort.Slice(positions, func(i, j int) bool {
return positions[i].ofs < positions[j].ofs
})
var hint combinedPos
for _, cp := range positions { for _, cp := range positions {
hint = e.offsetToScreenPos(hint, cp.ofs) *cp = e.closestPosition(combinedPos{runes: cp.runes})
*cp = hint
} }
} }
// 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 {
l := e.SelectionLen() start := min(e.caret.start.ofs, e.caret.end.ofs)
if l == 0 { end := max(e.caret.start.ofs, e.caret.end.ofs)
return "" buf := make([]byte, end-start)
} e.rr.Seek(int64(start), io.SeekStart)
buf := make([]byte, l)
e.rr.Seek(int64(min(e.caret.start.ofs, e.caret.end.ofs)), io.SeekStart)
_, err := e.rr.Read(buf) _, err := e.rr.Read(buf)
if err != nil { if err != nil {
// The only error that rr.Read can return is EOF, which just means no // The only error that rr.Read can return is EOF, which just means no
@@ -1440,13 +1387,13 @@ func nullLayout(r io.Reader) ([]text.Line, error) {
var n int var n int
var buf bytes.Buffer var buf bytes.Buffer
for { for {
r, s, err := rr.ReadRune() r, _, err := rr.ReadRune()
n += s
buf.WriteRune(r)
if err != nil { if err != nil {
rerr = err rerr = err
break break
} }
n++
buf.WriteRune(r)
} }
return []text.Line{ return []text.Line{
{ {
+6 -2
View File
@@ -13,6 +13,7 @@ import (
"testing" "testing"
"testing/quick" "testing/quick"
"unicode" "unicode"
"unicode/utf8"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/font/gofont" "gioui.org/font/gofont"
@@ -39,6 +40,9 @@ func TestEditor(t *testing.T) {
e.SetCaret(0, 0) // shouldn't panic e.SetCaret(0, 0) // shouldn't panic
assertCaret(t, e, 0, 0, 0) assertCaret(t, e, 0, 0, 0)
e.SetText("æbc\naøå•") e.SetText("æbc\naøå•")
if got, exp := e.Len(), utf8.RuneCountInString(e.Text()); got != exp {
t.Errorf("got length %d, expected %d", got, exp)
}
e.Layout(gtx, cache, font, fontSize, nil) e.Layout(gtx, cache, font, fontSize, nil)
assertCaret(t, e, 0, 0, 0) assertCaret(t, e, 0, 0, 0)
e.moveEnd(selectionClear) e.moveEnd(selectionClear)
@@ -56,9 +60,9 @@ func TestEditor(t *testing.T) {
e.SetCaret(0, 0) e.SetCaret(0, 0)
assertCaret(t, e, 0, 0, 0) assertCaret(t, e, 0, 0, 0)
e.SetCaret(len("æ"), len("æ")) e.SetCaret(utf8.RuneCountInString("æ"), utf8.RuneCountInString("æ"))
assertCaret(t, e, 0, 1, 2) assertCaret(t, e, 0, 1, 2)
e.SetCaret(len("æbc\naøå•"), len("æbc\naøå•")) e.SetCaret(utf8.RuneCountInString("æbc\naøå•"), utf8.RuneCountInString("æbc\naøå•"))
assertCaret(t, e, 1, 4, len("æbc\naøå•")) assertCaret(t, e, 1, 4, len("æbc\naøå•"))
// Ensure that password masking does not affect caret behavior // Ensure that password masking does not affect caret behavior