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.
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.processKey(gtx)
// 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{})
}
}
@@ -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 {
return e.rr.len()
e.makeValid()
end := e.closestPosition(combinedPos{runes: math.MaxInt})
return end.runes
}
// 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))
}
// 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.
func (e *Editor) indexPosition(pos combinedPos) combinedPos {
// 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.
func (e *Editor) MoveCaret(startDelta, endDelta int) {
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.xoff = 0
// 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)
}
// SelectionLen returns the length of the selection, in bytes; it is
// equivalent to len(e.SelectedText()).
// SelectionLen returns the length of the selection, in runes; it is
// equivalent to utf8.RuneCountInString(e.SelectedText()).
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
// editor text. start can be > end.
// Selection returns the start and end of the selection, as rune offsets.
// start can be > end.
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
// and end are in bytes, and represent offsets into the editor text. start and
// end must be at a rune boundary.
// and end are in runes, and represent offsets into the editor text.
func (e *Editor) SetCaret(start, end int) {
e.makeValid()
// Constrain start and end to [0, e.Len()].
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.caret.start.runes, e.caret.end.runes = start, end
e.makeValidCaret()
e.caret.scroll = true
e.scroller.Stop()
@@ -1340,24 +1294,17 @@ func (e *Editor) makeValidCaret(positions ...*combinedPos) {
// Jump through some hoops to order the offsets given to offsetToScreenPos,
// but still be able to update them correctly with the results thereof.
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 {
hint = e.offsetToScreenPos(hint, cp.ofs)
*cp = hint
*cp = e.closestPosition(combinedPos{runes: cp.runes})
}
}
// SelectedText returns the currently selected text (if any) from the editor.
func (e *Editor) SelectedText() string {
l := e.SelectionLen()
if l == 0 {
return ""
}
buf := make([]byte, l)
e.rr.Seek(int64(min(e.caret.start.ofs, e.caret.end.ofs)), io.SeekStart)
start := min(e.caret.start.ofs, e.caret.end.ofs)
end := max(e.caret.start.ofs, e.caret.end.ofs)
buf := make([]byte, end-start)
e.rr.Seek(int64(start), io.SeekStart)
_, err := e.rr.Read(buf)
if err != nil {
// 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 buf bytes.Buffer
for {
r, s, err := rr.ReadRune()
n += s
buf.WriteRune(r)
r, _, err := rr.ReadRune()
if err != nil {
rerr = err
break
}
n++
buf.WriteRune(r)
}
return []text.Line{
{
+6 -2
View File
@@ -13,6 +13,7 @@ import (
"testing"
"testing/quick"
"unicode"
"unicode/utf8"
"gioui.org/f32"
"gioui.org/font/gofont"
@@ -39,6 +40,9 @@ func TestEditor(t *testing.T) {
e.SetCaret(0, 0) // shouldn't panic
assertCaret(t, e, 0, 0, 0)
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)
assertCaret(t, e, 0, 0, 0)
e.moveEnd(selectionClear)
@@ -56,9 +60,9 @@ func TestEditor(t *testing.T) {
e.SetCaret(0, 0)
assertCaret(t, e, 0, 0, 0)
e.SetCaret(len("æ"), len("æ"))
e.SetCaret(utf8.RuneCountInString("æ"), utf8.RuneCountInString("æ"))
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øå•"))
// Ensure that password masking does not affect caret behavior