widget: track only Editor caret start in runes

The other information can be queries at use.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2022-02-05 22:55:50 +01:00
parent e323afa822
commit c22138f5f8
2 changed files with 88 additions and 67 deletions
+75 -58
View File
@@ -74,12 +74,12 @@ type Editor struct {
scroll bool
// xoff is the offset to the current position when moving between lines.
xoff fixed.Int26_6
// start is the current caret position, and also the start position of
// selected text. end is the end position of selected text. If start.ofs
// == end.ofs, then there's no selection. Note that it's possible (and
// start is the current caret position in runes, and also the start position of
// selected text. end is the end position of selected text. If start
// == end, then there's no selection. Note that it's possible (and
// common) that the caret (start) is after the end, e.g. after
// Shift-DownArrow.
start combinedPos
start int
end int
}
@@ -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.runes, e.caret.end), e.SelectionLen()
oldStart, oldLen := min(e.caret.start, e.caret.end), 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.runes, e.caret.end), e.SelectionLen(); oldStart != newStart || oldLen != newLen {
if newStart, newLen := min(e.caret.start, e.caret.end), e.SelectionLen(); oldStart != newStart || oldLen != newLen {
e.events = append(e.events, SelectEvent{})
}
}
@@ -228,7 +228,7 @@ func (e *Editor) makeValid() {
return
}
e.lines, e.dims = e.layoutText(e.shaper)
e.caret.start = e.closestPosition(combinedPos{runes: e.caret.start.runes})
e.caret.start = e.closestPosition(combinedPos{runes: e.caret.start}).runes
e.caret.end = e.closestPosition(combinedPos{runes: e.caret.end}).runes
e.valid = true
}
@@ -273,8 +273,8 @@ func (e *Editor) processPointer(gtx layout.Context) {
if evt.Modifiers == key.ModShift {
// If they clicked closer to the end, then change the end to
// where the caret used to be (effectively swapping start & end).
if abs(e.caret.end-e.caret.start.runes) < abs(e.caret.start.runes-prevCaretPos.runes) {
e.caret.end = prevCaretPos.runes
if abs(e.caret.end-e.caret.start) < abs(e.caret.start-prevCaretPos) {
e.caret.end = prevCaretPos
}
} else {
e.ClearSelection()
@@ -369,12 +369,13 @@ func (e *Editor) processKey(gtx layout.Context) {
}
func (e *Editor) moveLines(distance int, selAct selectionAction) {
x := e.caret.start.x + e.caret.xoff
caretStart := e.closestPosition(combinedPos{runes: e.caret.start})
x := caretStart.x + e.caret.xoff
// Seek to line.
pos := e.closestPosition(combinedPos{lineCol: screenPos{Y: e.caret.start.lineCol.Y + distance}})
pos := e.closestPosition(combinedPos{lineCol: screenPos{Y: caretStart.lineCol.Y + distance}})
pos = e.closestPosition(combinedPos{x: x, y: pos.y})
e.caret.start = pos
e.caret.xoff = x - e.caret.start.x
e.caret.start = pos.runes
e.caret.xoff = x - caretStart.x
e.updateSelection(selAct)
}
@@ -457,7 +458,7 @@ func (e *Editor) command(gtx layout.Context, k key.Event) bool {
return false
}
e.caret.end = 0
e.caret.start = e.closestPosition(combinedPos{runes: math.MaxInt})
e.caret.start = e.Len()
default:
return false
}
@@ -527,8 +528,9 @@ func (e *Editor) layout(gtx layout.Context, content layout.Widget) layout.Dimens
}
cl := textPadding(e.lines)
cl.Max = cl.Max.Add(e.viewSize)
caretStart := e.closestPosition(combinedPos{runes: e.caret.start})
caretEnd := e.closestPosition(combinedPos{runes: e.caret.end})
startSel, endSel := sortPoints(e.caret.start.lineCol, caretEnd.lineCol)
startSel, endSel := sortPoints(caretStart.lineCol, caretEnd.lineCol)
it := segmentIterator{
startSel: startSel,
endSel: endSel,
@@ -633,11 +635,12 @@ func (e *Editor) PaintCaret(gtx layout.Context) {
}
e.makeValid()
carWidth := fixed.I(gtx.Px(unit.Dp(1)))
carX := e.caret.start.x
carY := e.caret.start.y
caretStart := e.closestPosition(combinedPos{runes: e.caret.start})
carX := caretStart.x
carY := caretStart.y
carX -= carWidth / 2
carAsc, carDesc := -e.lines[e.caret.start.lineCol.Y].Bounds.Min.Y, e.lines[e.caret.start.lineCol.Y].Bounds.Max.Y
carAsc, carDesc := -e.lines[caretStart.lineCol.Y].Bounds.Min.Y, e.lines[caretStart.lineCol.Y].Bounds.Max.Y
carRect := image.Rectangle{
Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()},
Max: image.Point{X: carX.Ceil() + carWidth.Ceil(), Y: carY + carDesc.Ceil()},
@@ -678,9 +681,9 @@ func (e *Editor) Text() string {
// SetText replaces the contents of the editor, clearing any selection first.
func (e *Editor) SetText(s string) {
e.rr = editBuffer{}
e.caret.start = combinedPos{}
e.caret.start = 0
e.caret.end = 0
e.replace(e.caret.start.runes, e.caret.end, s)
e.replace(e.caret.start, e.caret.end, s)
e.caret.xoff = 0
}
@@ -725,7 +728,7 @@ func (e *Editor) scrollAbs(x, y int) {
func (e *Editor) moveCoord(pos image.Point) {
x := fixed.I(pos.X + e.scrollOff.X)
y := pos.Y + e.scrollOff.Y
e.caret.start = e.closestPosition(combinedPos{x: x, y: y})
e.caret.start = e.closestPosition(combinedPos{x: x, y: y}).runes
e.caret.xoff = 0
}
@@ -760,14 +763,16 @@ func (e *Editor) layoutText(s text.Shaper) ([]text.Line, layout.Dimensions) {
// CaretPos returns the line & column numbers of the caret.
func (e *Editor) CaretPos() (line, col int) {
e.makeValid()
return e.caret.start.lineCol.Y, e.caret.start.lineCol.X
caret := e.closestPosition(combinedPos{runes: e.caret.start})
return caret.lineCol.Y, caret.lineCol.X
}
// CaretCoords returns the coordinates of the caret, relative to the
// editor itself.
func (e *Editor) CaretCoords() f32.Point {
e.makeValid()
return f32.Pt(float32(e.caret.start.x)/64, float32(e.caret.start.y))
caret := e.closestPosition(combinedPos{runes: e.caret.start})
return f32.Pt(float32(caret.x)/64, float32(caret.y))
}
// indexPosition returns the latest position from the index no later than pos.
@@ -880,7 +885,7 @@ func (e *Editor) Delete(runes int) {
return
}
start := e.caret.start.runes
start := e.caret.start
end := e.caret.end
if start != end {
runes -= sign(runes)
@@ -903,11 +908,10 @@ 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.replace(e.caret.start.runes, e.caret.end, s)
e.replace(e.caret.start, e.caret.end, s)
e.caret.xoff = 0
e.caret.start.ofs += len(s)
e.caret.start.runes += utf8.RuneCountInString(s)
e.caret.end = e.caret.start.runes
e.caret.start += utf8.RuneCountInString(s)
e.caret.end = e.caret.start
}
// replace the text between start and end with s. Indices are in runes.
@@ -934,17 +938,19 @@ func (e *Editor) replace(start, end int, s string) {
}
return pos
}
e.caret.start = e.closestPosition(combinedPos{runes: adjust(e.caret.start.runes)})
e.caret.start = adjust(e.caret.start)
e.caret.end = adjust(e.caret.end)
e.invalidate()
}
func (e *Editor) movePages(pages int, selAct selectionAction) {
e.makeValid()
x := e.caret.start.x + e.caret.xoff
y := e.caret.start.y + pages*e.viewSize.Y
e.caret.start = e.closestPosition(combinedPos{x: x, y: y})
e.caret.xoff = x - e.caret.start.x
caret := e.closestPosition(combinedPos{runes: e.caret.start})
x := caret.x + e.caret.xoff
y := caret.y + pages*e.viewSize.Y
pos := e.closestPosition(combinedPos{x: x, y: y})
e.caret.start = pos.runes
e.caret.xoff = x - pos.x
e.updateSelection(selAct)
}
@@ -954,21 +960,25 @@ func (e *Editor) movePages(pages int, selAct selectionAction) {
func (e *Editor) MoveCaret(startDelta, endDelta int) {
e.makeValid()
e.caret.xoff = 0
e.caret.start = e.closestPosition(combinedPos{runes: e.caret.start.runes + startDelta})
e.caret.start = e.closestPosition(combinedPos{runes: e.caret.start + startDelta}).runes
e.caret.end = e.closestPosition(combinedPos{runes: e.caret.end + endDelta}).runes
}
func (e *Editor) moveStart(selAct selectionAction) {
e.caret.start = e.closestPosition(combinedPos{lineCol: screenPos{Y: e.caret.start.lineCol.Y}})
e.caret.xoff = -e.caret.start.x
caret := e.closestPosition(combinedPos{runes: e.caret.start})
caret = e.closestPosition(combinedPos{lineCol: screenPos{Y: caret.lineCol.Y}})
e.caret.start = caret.runes
e.caret.xoff = -caret.x
e.updateSelection(selAct)
}
func (e *Editor) moveEnd(selAct selectionAction) {
e.caret.start = e.closestPosition(combinedPos{lineCol: screenPos{X: math.MaxInt, Y: e.caret.start.lineCol.Y}})
l := e.lines[e.caret.start.lineCol.Y]
caret := e.closestPosition(combinedPos{runes: e.caret.start})
caret = e.closestPosition(combinedPos{lineCol: screenPos{X: math.MaxInt, Y: caret.lineCol.Y}})
e.caret.start = caret.runes
l := e.lines[caret.lineCol.Y]
a := align(e.Alignment, l.Width, e.viewSize.X)
e.caret.xoff = l.Width + a - e.caret.start.x
e.caret.xoff = l.Width + a - caret.x
e.updateSelection(selAct)
}
@@ -984,25 +994,29 @@ func (e *Editor) moveWord(distance int, selAct selectionAction) {
words, direction = distance*-1, -1
}
// atEnd if caret is at either side of the buffer.
caret := e.closestPosition(combinedPos{runes: e.caret.start})
atEnd := func() bool {
return e.caret.start.ofs == 0 || e.caret.start.ofs == e.rr.len()
return caret.ofs == 0 || caret.ofs == e.rr.len()
}
// next returns the appropriate rune given the direction.
next := func() (r rune) {
if direction < 0 {
r, _ = e.rr.runeBefore(e.caret.start.ofs)
r, _ = e.rr.runeBefore(caret.ofs)
} else {
r, _ = e.rr.runeAt(e.caret.start.ofs)
r, _ = e.rr.runeAt(caret.ofs)
}
return r
}
for ii := 0; ii < words; ii++ {
for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() {
e.MoveCaret(direction, 0)
caret = e.closestPosition(combinedPos{runes: e.caret.start})
}
e.MoveCaret(direction, 0)
caret = e.closestPosition(combinedPos{runes: e.caret.start})
for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() {
e.MoveCaret(direction, 0)
caret = e.closestPosition(combinedPos{runes: e.caret.start})
}
}
e.updateSelection(selAct)
@@ -1020,7 +1034,7 @@ func (e *Editor) deleteWord(distance int) {
e.makeValid()
if e.caret.start.runes != e.caret.end {
if e.caret.start != e.caret.end {
e.Delete(1)
distance -= sign(distance)
}
@@ -1035,13 +1049,14 @@ func (e *Editor) deleteWord(distance int) {
words, direction = distance*-1, -1
}
// atEnd if offset is at or beyond either side of the buffer.
caret := e.closestPosition(combinedPos{runes: e.caret.start})
atEnd := func(offset int) bool {
idx := e.caret.start.ofs + offset*direction
idx := caret.ofs + offset*direction
return idx <= 0 || idx >= e.rr.len()
}
// next returns the appropriate rune and length given the direction and offset (in bytes).
next := func(offset int) (r rune, l int) {
idx := e.caret.start.ofs + offset*direction
idx := caret.ofs + offset*direction
if idx < 0 {
idx = 0
} else if idx > e.rr.len() {
@@ -1055,9 +1070,9 @@ func (e *Editor) deleteWord(distance int) {
return
}
var runes = 1
_, bytes := e.rr.runeAt(e.caret.start.ofs)
_, bytes := e.rr.runeAt(caret.ofs)
if direction < 0 {
_, bytes = e.rr.runeBefore(e.caret.start.ofs)
_, bytes = e.rr.runeBefore(caret.ofs)
}
for ii := 0; ii < words; ii++ {
if r, _ := next(bytes); unicode.IsSpace(r) {
@@ -1077,18 +1092,19 @@ func (e *Editor) deleteWord(distance int) {
func (e *Editor) scrollToCaret() {
e.makeValid()
l := e.lines[e.caret.start.lineCol.Y]
caret := e.closestPosition(combinedPos{runes: e.caret.start})
l := e.lines[caret.lineCol.Y]
if e.SingleLine {
var dist int
if d := e.caret.start.x.Floor() - e.scrollOff.X; d < 0 {
if d := caret.x.Floor() - e.scrollOff.X; d < 0 {
dist = d
} else if d := e.caret.start.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 {
} else if d := caret.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 {
dist = d
}
e.scrollRel(dist, 0)
} else {
miny := e.caret.start.y - l.Ascent.Ceil()
maxy := e.caret.start.y + l.Descent.Ceil()
miny := caret.y - l.Ascent.Ceil()
maxy := caret.y + l.Descent.Ceil()
var dist int
if d := miny - e.scrollOff.Y; d < 0 {
dist = d
@@ -1108,20 +1124,20 @@ func (e *Editor) NumLines() int {
// 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.runes - e.caret.end)
return abs(e.caret.start - e.caret.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.runes, e.caret.end
return e.caret.start, e.caret.end
}
// SetCaret moves the caret to start, and sets the selection end to end. start
// and end are in runes, and represent offsets into the editor text.
func (e *Editor) SetCaret(start, end int) {
e.makeValid()
e.caret.start = e.closestPosition(combinedPos{runes: start})
e.caret.start = e.closestPosition(combinedPos{runes: start}).runes
e.caret.end = e.closestPosition(combinedPos{runes: end}).runes
e.caret.scroll = true
e.scroller.Stop()
@@ -1129,9 +1145,10 @@ func (e *Editor) SetCaret(start, end int) {
// SelectedText returns the currently selected text (if any) from the editor.
func (e *Editor) SelectedText() string {
caretStart := e.closestPosition(combinedPos{runes: e.caret.start})
caretEnd := e.closestPosition(combinedPos{runes: e.caret.end})
start := min(e.caret.start.ofs, caretEnd.ofs)
end := max(e.caret.start.ofs, caretEnd.ofs)
start := min(caretStart.ofs, caretEnd.ofs)
end := max(caretStart.ofs, caretEnd.ofs)
buf := make([]byte, end-start)
e.rr.Seek(int64(start), io.SeekStart)
_, err := e.rr.Read(buf)
@@ -1152,7 +1169,7 @@ func (e *Editor) updateSelection(selAct selectionAction) {
// ClearSelection clears the selection, by setting the selection end equal to
// the selection start.
func (e *Editor) ClearSelection() {
e.caret.end = e.caret.start.runes
e.caret.end = e.caret.start
}
// WriteTo implements io.WriterTo.
+13 -9
View File
@@ -117,8 +117,9 @@ func assertCaret(t *testing.T, e *Editor, line, col, bytes int) {
if gotLine != line || gotCol != col {
t.Errorf("caret at (%d, %d), expected (%d, %d)", gotLine, gotCol, line, col)
}
if bytes != e.caret.start.ofs {
t.Errorf("caret at buffer position %d, expected %d", e.caret.start.ofs, bytes)
caretBytes := e.closestPosition(combinedPos{runes: e.caret.start}).ofs
if bytes != caretBytes {
t.Errorf("caret at buffer position %d, expected %d", caretBytes, bytes)
}
}
@@ -157,7 +158,7 @@ func TestEditorCaretConsistency(t *testing.T) {
gotCoords := e.CaretCoords()
// Blow away index to re-compute position from scratch.
e.invalidate()
want := e.closestPosition(combinedPos{runes: e.caret.start.runes})
want := e.closestPosition(combinedPos{runes: e.caret.start})
wantCoords := f32.Pt(float32(want.x)/64, float32(want.y))
if want.lineCol.Y != gotLine || want.lineCol.X != gotCol || gotCoords != wantCoords {
return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s",
@@ -245,8 +246,9 @@ func TestEditorMoveWord(t *testing.T) {
e := setup(tt.Text)
e.MoveCaret(tt.Start, tt.Start)
e.moveWord(tt.Skip, selectionClear)
if e.caret.start.ofs != tt.Want {
t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, e.caret.start.ofs, tt.Want)
caretBytes := e.closestPosition(combinedPos{runes: e.caret.start}).ofs
if caretBytes != tt.Want {
t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, caretBytes, tt.Want)
}
}
}
@@ -438,8 +440,9 @@ func TestEditorDeleteWord(t *testing.T) {
e.MoveCaret(tt.Start, tt.Start)
e.MoveCaret(0, tt.Selection)
e.deleteWord(tt.Delete)
if e.caret.start.ofs != tt.Want {
t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, e.caret.start.ofs, tt.Want)
caretBytes := e.closestPosition(combinedPos{runes: e.caret.start}).ofs
if caretBytes != tt.Want {
t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, caretBytes, tt.Want)
}
if e.Text() != tt.Result {
t.Fatalf("[%d] deleteWord: invalid result: got %q, want %q", ii, e.Text(), tt.Result)
@@ -547,11 +550,12 @@ g123456789g
gtx.Queue = nil
e.Layout(gtx, cache, font, fontSize, nil)
caretStart := e.closestPosition(combinedPos{runes: e.caret.start})
caretEnd := e.closestPosition(combinedPos{runes: e.caret.end})
if caretEnd.lineCol != tst.startPos || e.caret.start.lineCol != tst.endPos {
if caretEnd.lineCol != tst.startPos || caretStart.lineCol != tst.endPos {
t.Errorf("Test %d pt2: Expected %#v, %#v; got %#v, %#v",
n,
caretEnd.lineCol, e.caret.start.lineCol,
caretEnd.lineCol, caretStart.lineCol,
tst.startPos, tst.endPos)
continue
}