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
}
func (e *editBuffer) deleteRunes(caret, runes int) int {
func (e *editBuffer) deleteRunes(caret, count int) (bytes int, runes int) {
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])
e.gapstart -= s
caret -= s
bytes += s
runes++
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:])
e.gapend += s
e.changed = e.changed || s > 0
}
return caret
}
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
return
}
// 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.
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
// runes is the offset in runes.
runes int
// lineCol.Y = line (offset into Editor.lines), and X = col (offset into
// Editor.lines[Y])
@@ -666,7 +668,8 @@ func (e *Editor) SetText(s string) {
e.rr = editBuffer{}
e.caret.start = 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 {
@@ -787,6 +790,7 @@ func (e *Editor) offsetToScreenPos2(o1, o2 int) (combinedPos, combinedPos) {
func (e *Editor) offsetToScreenPos(offset int) (combinedPos, func(int) combinedPos) {
var col, line, idx int
var x fixed.Int26_6
runes := 0
l := e.lines[line]
y := l.Ascent.Ceil()
@@ -803,6 +807,7 @@ func (e *Editor) offsetToScreenPos(offset int) (combinedPos, func(int) combinedP
x += l.Layout.Advances[col]
_, s := e.rr.runeAt(idx)
idx += s
runes++
}
if lastLine := line == len(e.lines)-1; lastLine || idx > offset {
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),
y: y,
ofs: offset,
runes: runes,
}
}
return iter(offset), iter
@@ -838,15 +844,16 @@ func (e *Editor) Delete(runes int) {
return
}
if l := e.caret.end.ofs - e.caret.start.ofs; l != 0 {
e.caret.start.ofs = e.rr.deleteBytes(e.caret.start.ofs, l)
start := e.caret.start.runes
end := e.caret.end.runes
if start != end {
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.ClearSelection()
e.invalidate()
}
// 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.
// xxx|yyy + append zzz => xxxzzz|yyy
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.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
// a selection, prepend overwrites it.
// xxx|yyy + prepend zzz => xxx|zzzyyy
func (e *Editor) prepend(s string) {
// replace the text between start and end with s. Indices are in runes.
func (e *Editor) replace(start, end int, s string) {
if e.SingleLine {
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.
e.rr.prepend(e.caret.start.ofs, s)
e.caret.xoff = 0
if start > end {
start, end = end, start
}
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()
}
// 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) {
e.makeValid()
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]
_, s := e.rr.runeAt(pos.ofs)
pos.ofs += s
pos.runes++
pos.y += (prevDesc + l.Ascent).Ceil()
pos.lineCol.X = 0
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]
_, s := e.rr.runeBefore(pos.ofs)
pos.ofs -= s
pos.runes--
pos.y -= (prevDesc + l.Ascent).Ceil()
prevDesc = l.Descent
pos.lineCol.Y--
@@ -957,6 +1000,7 @@ func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6, line int) combi
pos.x += adv
_, s := e.rr.runeAt(pos.ofs)
pos.ofs += s
pos.runes++
pos.lineCol.X++
}
return pos
@@ -989,6 +1033,7 @@ func (e *Editor) movePos(pos combinedPos, distance int) combinedPos {
l := e.lines[pos.lineCol.Y].Layout
_, s := e.rr.runeBefore(pos.ofs)
pos.ofs -= s
pos.runes--
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]
_, s := e.rr.runeAt(pos.ofs)
pos.ofs += s
pos.runes++
pos.lineCol.X++
}
return pos
@@ -1024,6 +1070,7 @@ func (e *Editor) movePosToStart(pos combinedPos) combinedPos {
for i := pos.lineCol.X - 1; i >= 0; i-- {
_, s := e.rr.runeBefore(pos.ofs)
pos.ofs -= s
pos.runes--
pos.x -= layout.Advances[i]
}
pos.lineCol.X = 0
@@ -1048,6 +1095,7 @@ func (e *Editor) movePosToEnd(pos combinedPos) (combinedPos, fixed.Int26_6) {
adv := layout.Advances[i]
_, s := e.rr.runeAt(pos.ofs)
pos.ofs += s
pos.runes++
pos.x += adv
pos.lineCol.X++
}
+9 -4
View File
@@ -153,11 +153,16 @@ func TestEditorCaretConsistency(t *testing.T) {
gotCoords := e.CaretCoords()
want, _ := e.offsetToScreenPos(e.caret.start.ofs)
wantCoords := f32.Pt(float32(want.x)/64, float32(want.y))
if want.lineCol.Y == gotLine && want.lineCol.X == gotCol && gotCoords == wantCoords {
return nil
if want.lineCol.Y != gotLine || want.lineCol.X != gotCol || gotCoords != wantCoords {
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",
gotLine, gotCol, gotCoords, want.lineCol.Y, want.lineCol.X, wantCoords)
seekCaret := e.seek(combinedPos{}, e.caret.start.runes)
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 {
t.Errorf("initial editor inconsistency (alignment %s): %v", a, err)