diff --git a/widget/buffer.go b/widget/buffer.go index 71767f3b..b6e89c85 100644 --- a/widget/buffer.go +++ b/widget/buffer.go @@ -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, diff --git a/widget/editor.go b/widget/editor.go index 96155ef9..94a7af86 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -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++ } diff --git a/widget/editor_test.go b/widget/editor_test.go index f7bda577..ec1abbc4 100644 --- a/widget/editor_test.go +++ b/widget/editor_test.go @@ -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)