mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-04 17:05:38 +00:00
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:
+6
-24
@@ -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
@@ -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++
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user