mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
widget: introduce caret indexing to Editor
An efficient index replaces all other ad-hoc caret positioning methods. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+103
-9
@@ -65,6 +65,10 @@ type Editor struct {
|
||||
dims layout.Dimensions
|
||||
requestFocus bool
|
||||
|
||||
// index tracks combined caret positions at regularly
|
||||
// spaced intervals to speed up caret seeking.
|
||||
index []combinedPos
|
||||
|
||||
caret struct {
|
||||
on bool
|
||||
scroll bool
|
||||
@@ -448,7 +452,8 @@ func (e *Editor) command(gtx layout.Context, k key.Event) bool {
|
||||
if k.Modifiers != key.ModShortcut {
|
||||
return false
|
||||
}
|
||||
e.caret.end, e.caret.start = e.offsetToScreenPos2(0, e.Len())
|
||||
e.caret.end = e.closestPosition(combinedPos{})
|
||||
e.caret.start = e.closestPosition(combinedPos{runes: math.MaxInt})
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -770,14 +775,6 @@ func (e *Editor) CaretCoords() f32.Point {
|
||||
return f32.Pt(float32(e.caret.start.x)/64, float32(e.caret.start.y))
|
||||
}
|
||||
|
||||
// offsetToScreenPos2 is a utility function to shortcut the common case of
|
||||
// wanting the positions of exactly two offsets.
|
||||
func (e *Editor) offsetToScreenPos2(o1, o2 int) (combinedPos, combinedPos) {
|
||||
cp1 := e.offsetToScreenPos(combinedPos{}, o1)
|
||||
cp2 := e.offsetToScreenPos(cp1, o2)
|
||||
return cp1, cp2
|
||||
}
|
||||
|
||||
// offsetToScreenPos takes an offset into the editor text (e.g.
|
||||
// e.caret.end.ofs) and returns a combinedPos that corresponds to its current
|
||||
// screen position. Hint is either the zero-value position or the result of
|
||||
@@ -821,7 +818,104 @@ LOOP:
|
||||
return hint
|
||||
}
|
||||
|
||||
// indexPosition returns the latest position from the index no later than pos.
|
||||
func (e *Editor) indexPosition(pos combinedPos) combinedPos {
|
||||
// Initialize index with first caret position.
|
||||
if len(e.index) == 0 {
|
||||
l := e.lines[0]
|
||||
e.index = append(e.index, combinedPos{
|
||||
x: align(e.Alignment, l.Width, e.viewSize.X),
|
||||
y: l.Ascent.Ceil(),
|
||||
})
|
||||
}
|
||||
i := sort.Search(len(e.index), func(i int) bool {
|
||||
return e.positionGreaterOrEqual(e.index[i], pos)
|
||||
})
|
||||
// Return position just before pos, which is guaranteed to be less than or equal to pos.
|
||||
if i > 0 {
|
||||
i--
|
||||
}
|
||||
return e.index[i]
|
||||
}
|
||||
|
||||
// positionGreaterOrEqual reports whether p1 >= p2 according to the non-zero fields
|
||||
// of p2. All fields of p1 must be a consistent and valid.
|
||||
func (e *Editor) positionGreaterOrEqual(p1, p2 combinedPos) bool {
|
||||
l := e.lines[p1.lineCol.Y]
|
||||
endCol := len(l.Layout.Advances) - 1
|
||||
if lastLine := p1.lineCol.Y == len(e.lines)-1; lastLine {
|
||||
endCol++
|
||||
}
|
||||
eol := p1.lineCol.X == endCol
|
||||
switch {
|
||||
case p2.runes != 0:
|
||||
return p1.runes >= p2.runes
|
||||
case p2.lineCol != (screenPos{}):
|
||||
if p1.lineCol.Y != p2.lineCol.Y {
|
||||
return p1.lineCol.Y > p2.lineCol.Y
|
||||
}
|
||||
return eol || p1.lineCol.X >= p2.lineCol.X
|
||||
case p2.x != 0 || p2.y != 0:
|
||||
ly := p1.y + l.Descent.Ceil()
|
||||
prevy := p1.y - l.Ascent.Ceil()
|
||||
switch {
|
||||
case ly < p2.y && p1.lineCol.Y < len(e.lines)-1:
|
||||
// p1 is on a line before p2.y.
|
||||
return false
|
||||
case prevy >= p2.y && p1.lineCol.Y > 0:
|
||||
// p1 is on a line after p2.y.
|
||||
return true
|
||||
}
|
||||
if eol {
|
||||
return true
|
||||
}
|
||||
adv := l.Layout.Advances[p1.lineCol.X]
|
||||
return p1.x+adv-p2.x >= p2.x-p1.x
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// closestPosition takes a position and returns its closest valid position.
|
||||
// Zero fields of pos and pos.ofs are ignored.
|
||||
func (e *Editor) closestPosition(pos combinedPos) combinedPos {
|
||||
closest := e.indexPosition(pos)
|
||||
l := e.lines[closest.lineCol.Y]
|
||||
count := 0
|
||||
const runesPerIndexEntry = 50
|
||||
// Advance next and prev until next is greater than or equal to pos.
|
||||
for {
|
||||
for ; closest.lineCol.X < len(l.Layout.Advances); closest.lineCol.X++ {
|
||||
if count == runesPerIndexEntry {
|
||||
e.index = append(e.index, closest)
|
||||
count = 0
|
||||
}
|
||||
count++
|
||||
if e.positionGreaterOrEqual(closest, pos) {
|
||||
return closest
|
||||
}
|
||||
|
||||
adv := l.Layout.Advances[closest.lineCol.X]
|
||||
closest.x += adv
|
||||
_, s := e.rr.runeAt(closest.ofs)
|
||||
closest.ofs += s
|
||||
closest.runes++
|
||||
}
|
||||
if closest.lineCol.Y == len(e.lines)-1 {
|
||||
// End of file.
|
||||
return closest
|
||||
}
|
||||
|
||||
prevDesc := l.Descent
|
||||
closest.lineCol.Y++
|
||||
closest.lineCol.X = 0
|
||||
l = e.lines[closest.lineCol.Y]
|
||||
closest.x = align(e.Alignment, l.Width, e.viewSize.X)
|
||||
closest.y += (prevDesc + l.Ascent).Ceil()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Editor) invalidate() {
|
||||
e.index = e.index[:0]
|
||||
e.valid = false
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,9 @@ func TestEditorCaretConsistency(t *testing.T) {
|
||||
t.Helper()
|
||||
gotLine, gotCol := e.CaretPos()
|
||||
gotCoords := e.CaretCoords()
|
||||
want := e.offsetToScreenPos(combinedPos{}, e.caret.start.ofs)
|
||||
// Blow away index to re-compute position from scratch.
|
||||
e.invalidate()
|
||||
want := e.closestPosition(combinedPos{runes: e.caret.start.runes})
|
||||
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",
|
||||
@@ -486,7 +488,8 @@ g123456789g
|
||||
_ = e.Events() // throw away any events from this layout
|
||||
|
||||
// Build the selection events
|
||||
startPos, endPos := e.offsetToScreenPos2(sortInts(start, end))
|
||||
startPos := e.closestPosition(combinedPos{runes: start})
|
||||
endPos := e.closestPosition(combinedPos{runes: end})
|
||||
tq := &testQueue{
|
||||
events: []event.Event{
|
||||
pointer.Event{
|
||||
|
||||
Reference in New Issue
Block a user