widget: track rune positions for Editor carets

Needed for efficient implementation of the upcoming IME interface.

Also introduce Editor.replace, seek methods for easier caret navigation
and editing.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2022-02-04 15:14:12 +01:00
parent 212c3cc126
commit d0869ef457
3 changed files with 78 additions and 43 deletions
+6 -24
View File
@@ -30,39 +30,21 @@ func (e *editBuffer) Changed() bool {
return c return c
} }
func (e *editBuffer) deleteRunes(caret, runes int) int { func (e *editBuffer) deleteRunes(caret, count int) (bytes int, runes int) {
e.moveGap(caret, 0) e.moveGap(caret, 0)
for ; runes < 0 && e.gapstart > 0; runes++ { for ; count < 0 && e.gapstart > 0; count++ {
_, s := utf8.DecodeLastRune(e.text[:e.gapstart]) _, s := utf8.DecodeLastRune(e.text[:e.gapstart])
e.gapstart -= s e.gapstart -= s
caret -= s bytes += s
runes++
e.changed = e.changed || s > 0 e.changed = e.changed || s > 0
} }
for ; runes > 0 && e.gapend < len(e.text); runes-- { for ; count > 0 && e.gapend < len(e.text); count-- {
_, s := utf8.DecodeRune(e.text[e.gapend:]) _, s := utf8.DecodeRune(e.text[e.gapend:])
e.gapend += s e.gapend += s
e.changed = e.changed || s > 0 e.changed = e.changed || s > 0
} }
return caret return
}
func (e *editBuffer) deleteBytes(caret, bytes int) int {
e.moveGap(caret, 0)
if bytes < 0 {
e.gapstart += bytes
if e.gapstart < 0 {
e.gapstart = 0
}
caret = e.gapstart
}
if bytes > 0 {
e.gapend += bytes
if e.gapend > len(e.text) {
e.gapend = len(e.text)
}
}
e.changed = e.changed || bytes != 0
return caret
} }
// moveGap moves the gap to the caret position. After returning, // moveGap moves the gap to the caret position. After returning,
+63 -15
View File
@@ -104,8 +104,10 @@ type maskReader struct {
// combinedPos is a point in the editor. // combinedPos is a point in the editor.
type combinedPos struct { type combinedPos struct {
// editorBuffer offset. The other three fields are based off of this one. // ofs is the offset into the editorBuffer in bytes. The other three fields are based off of this one.
ofs int ofs int
// runes is the offset in runes.
runes int
// lineCol.Y = line (offset into Editor.lines), and X = col (offset into // lineCol.Y = line (offset into Editor.lines), and X = col (offset into
// Editor.lines[Y]) // Editor.lines[Y])
@@ -666,7 +668,8 @@ func (e *Editor) SetText(s string) {
e.rr = editBuffer{} e.rr = editBuffer{}
e.caret.start = combinedPos{} e.caret.start = combinedPos{}
e.caret.end = combinedPos{} e.caret.end = combinedPos{}
e.prepend(s) e.replace(e.caret.start.runes, e.caret.end.runes, s)
e.caret.xoff = 0
} }
func (e *Editor) scrollBounds() image.Rectangle { func (e *Editor) scrollBounds() image.Rectangle {
@@ -787,6 +790,7 @@ func (e *Editor) offsetToScreenPos2(o1, o2 int) (combinedPos, combinedPos) {
func (e *Editor) offsetToScreenPos(offset int) (combinedPos, func(int) combinedPos) { func (e *Editor) offsetToScreenPos(offset int) (combinedPos, func(int) combinedPos) {
var col, line, idx int var col, line, idx int
var x fixed.Int26_6 var x fixed.Int26_6
runes := 0
l := e.lines[line] l := e.lines[line]
y := l.Ascent.Ceil() y := l.Ascent.Ceil()
@@ -803,6 +807,7 @@ func (e *Editor) offsetToScreenPos(offset int) (combinedPos, func(int) combinedP
x += l.Layout.Advances[col] x += l.Layout.Advances[col]
_, s := e.rr.runeAt(idx) _, s := e.rr.runeAt(idx)
idx += s idx += s
runes++
} }
if lastLine := line == len(e.lines)-1; lastLine || idx > offset { if lastLine := line == len(e.lines)-1; lastLine || idx > offset {
break LOOP break LOOP
@@ -820,6 +825,7 @@ func (e *Editor) offsetToScreenPos(offset int) (combinedPos, func(int) combinedP
x: x + align(e.Alignment, e.lines[line].Width, e.viewSize.X), x: x + align(e.Alignment, e.lines[line].Width, e.viewSize.X),
y: y, y: y,
ofs: offset, ofs: offset,
runes: runes,
} }
} }
return iter(offset), iter return iter(offset), iter
@@ -838,15 +844,16 @@ func (e *Editor) Delete(runes int) {
return return
} }
if l := e.caret.end.ofs - e.caret.start.ofs; l != 0 { start := e.caret.start.runes
e.caret.start.ofs = e.rr.deleteBytes(e.caret.start.ofs, l) end := e.caret.end.runes
if start != end {
runes -= sign(runes) runes -= sign(runes)
} }
e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, runes) end += runes
e.replace(start, end, "")
e.caret.xoff = 0 e.caret.xoff = 0
e.ClearSelection() e.ClearSelection()
e.invalidate()
} }
// Insert inserts text at the caret, moving the caret forward. If there is a // Insert inserts text at the caret, moving the caret forward. If there is a
@@ -860,24 +867,58 @@ func (e *Editor) Insert(s string) {
// there is a selection, append overwrites it. // there is a selection, append overwrites it.
// xxx|yyy + append zzz => xxxzzz|yyy // xxx|yyy + append zzz => xxxzzz|yyy
func (e *Editor) append(s string) { func (e *Editor) append(s string) {
e.prepend(s) e.replace(e.caret.start.runes, e.caret.end.runes, s)
e.caret.xoff = 0
e.caret.start.ofs += len(s) e.caret.start.ofs += len(s)
e.caret.end.ofs = e.caret.start.ofs e.caret.start.runes += utf8.RuneCountInString(s)
e.caret.end = e.caret.start
} }
// prepend inserts s after the cursor; the caret does not change. If there is // replace the text between start and end with s. Indices are in runes.
// a selection, prepend overwrites it. func (e *Editor) replace(start, end int, s string) {
// xxx|yyy + prepend zzz => xxx|zzzyyy
func (e *Editor) prepend(s string) {
if e.SingleLine { if e.SingleLine {
s = strings.ReplaceAll(s, "\n", " ") s = strings.ReplaceAll(s, "\n", " ")
} }
e.caret.start.ofs = e.rr.deleteBytes(e.caret.start.ofs, e.caret.end.ofs-e.caret.start.ofs) // Delete any selection first. if start > end {
e.rr.prepend(e.caret.start.ofs, s) start, end = end, start
e.caret.xoff = 0 }
startPos := e.seek(e.caret.start, start)
endPos := e.seek(e.caret.end, end)
e.rr.deleteRunes(startPos.ofs, endPos.runes-startPos.runes)
e.rr.prepend(startPos.ofs, s)
newEnd := startPos.runes + utf8.RuneCountInString(s)
adjust := func(pos combinedPos) combinedPos {
switch {
case newEnd < pos.runes && pos.runes <= endPos.runes:
pos.runes = newEnd
case endPos.runes < pos.runes:
diff := newEnd - endPos.runes
pos.runes = pos.runes + diff
}
return e.seek(startPos, pos.runes)
}
e.caret.start = adjust(e.caret.start)
e.caret.end = adjust(e.caret.end)
e.invalidate() e.invalidate()
} }
// seek returns the byte offset for an absolute rune offset. The provided hint
// is a position potentially close.
func (e *Editor) seek(hint combinedPos, runes int) combinedPos {
pos := hint
for pos.runes > runes && pos.ofs > 0 {
_, s := e.rr.runeBefore(pos.ofs)
pos.ofs -= s
pos.runes--
}
for pos.runes < runes && pos.ofs < e.rr.len() {
_, s := e.rr.runeAt(pos.ofs)
pos.ofs += s
pos.runes++
}
return pos
}
func (e *Editor) movePages(pages int, selAct selectionAction) { func (e *Editor) movePages(pages int, selAct selectionAction) {
e.makeValid() e.makeValid()
y := e.caret.start.y + pages*e.viewSize.Y y := e.caret.start.y + pages*e.viewSize.Y
@@ -920,6 +961,7 @@ func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6, line int) combi
l := e.lines[pos.lineCol.Y] l := e.lines[pos.lineCol.Y]
_, s := e.rr.runeAt(pos.ofs) _, s := e.rr.runeAt(pos.ofs)
pos.ofs += s pos.ofs += s
pos.runes++
pos.y += (prevDesc + l.Ascent).Ceil() pos.y += (prevDesc + l.Ascent).Ceil()
pos.lineCol.X = 0 pos.lineCol.X = 0
prevDesc = l.Descent prevDesc = l.Descent
@@ -930,6 +972,7 @@ func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6, line int) combi
l := e.lines[pos.lineCol.Y] l := e.lines[pos.lineCol.Y]
_, s := e.rr.runeBefore(pos.ofs) _, s := e.rr.runeBefore(pos.ofs)
pos.ofs -= s pos.ofs -= s
pos.runes--
pos.y -= (prevDesc + l.Ascent).Ceil() pos.y -= (prevDesc + l.Ascent).Ceil()
prevDesc = l.Descent prevDesc = l.Descent
pos.lineCol.Y-- pos.lineCol.Y--
@@ -957,6 +1000,7 @@ func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6, line int) combi
pos.x += adv pos.x += adv
_, s := e.rr.runeAt(pos.ofs) _, s := e.rr.runeAt(pos.ofs)
pos.ofs += s pos.ofs += s
pos.runes++
pos.lineCol.X++ pos.lineCol.X++
} }
return pos return pos
@@ -989,6 +1033,7 @@ func (e *Editor) movePos(pos combinedPos, distance int) combinedPos {
l := e.lines[pos.lineCol.Y].Layout l := e.lines[pos.lineCol.Y].Layout
_, s := e.rr.runeBefore(pos.ofs) _, s := e.rr.runeBefore(pos.ofs)
pos.ofs -= s pos.ofs -= s
pos.runes--
pos.lineCol.X-- pos.lineCol.X--
pos.x -= l.Advances[pos.lineCol.X] pos.x -= l.Advances[pos.lineCol.X]
} }
@@ -1007,6 +1052,7 @@ func (e *Editor) movePos(pos combinedPos, distance int) combinedPos {
pos.x += l.Advances[pos.lineCol.X] pos.x += l.Advances[pos.lineCol.X]
_, s := e.rr.runeAt(pos.ofs) _, s := e.rr.runeAt(pos.ofs)
pos.ofs += s pos.ofs += s
pos.runes++
pos.lineCol.X++ pos.lineCol.X++
} }
return pos return pos
@@ -1024,6 +1070,7 @@ func (e *Editor) movePosToStart(pos combinedPos) combinedPos {
for i := pos.lineCol.X - 1; i >= 0; i-- { for i := pos.lineCol.X - 1; i >= 0; i-- {
_, s := e.rr.runeBefore(pos.ofs) _, s := e.rr.runeBefore(pos.ofs)
pos.ofs -= s pos.ofs -= s
pos.runes--
pos.x -= layout.Advances[i] pos.x -= layout.Advances[i]
} }
pos.lineCol.X = 0 pos.lineCol.X = 0
@@ -1048,6 +1095,7 @@ func (e *Editor) movePosToEnd(pos combinedPos) (combinedPos, fixed.Int26_6) {
adv := layout.Advances[i] adv := layout.Advances[i]
_, s := e.rr.runeAt(pos.ofs) _, s := e.rr.runeAt(pos.ofs)
pos.ofs += s pos.ofs += s
pos.runes++
pos.x += adv pos.x += adv
pos.lineCol.X++ pos.lineCol.X++
} }
+9 -4
View File
@@ -153,11 +153,16 @@ func TestEditorCaretConsistency(t *testing.T) {
gotCoords := e.CaretCoords() gotCoords := e.CaretCoords()
want, _ := e.offsetToScreenPos(e.caret.start.ofs) want, _ := e.offsetToScreenPos(e.caret.start.ofs)
wantCoords := f32.Pt(float32(want.x)/64, float32(want.y)) wantCoords := f32.Pt(float32(want.x)/64, float32(want.y))
if want.lineCol.Y == gotLine && want.lineCol.X == gotCol && gotCoords == wantCoords { if want.lineCol.Y != gotLine || want.lineCol.X != gotCol || gotCoords != wantCoords {
return nil return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s",
gotLine, gotCol, gotCoords, want.lineCol.Y, want.lineCol.X, wantCoords)
} }
return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s", seekCaret := e.seek(combinedPos{}, e.caret.start.runes)
gotLine, gotCol, gotCoords, want.lineCol.Y, want.lineCol.X, wantCoords) if seekCaret.runes != e.caret.start.runes || seekCaret.ofs != e.caret.start.ofs {
return fmt.Errorf("caret ofs %d, runes %d, expected ofs %d runes %d",
e.caret.start.ofs, e.caret.start.runes, seekCaret.ofs, seekCaret.runes)
}
return nil
} }
if err := consistent(); err != nil { if err := consistent(); err != nil {
t.Errorf("initial editor inconsistency (alignment %s): %v", a, err) t.Errorf("initial editor inconsistency (alignment %s): %v", a, err)