mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-06 01:45:36 +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
|
dims layout.Dimensions
|
||||||
requestFocus bool
|
requestFocus bool
|
||||||
|
|
||||||
|
// index tracks combined caret positions at regularly
|
||||||
|
// spaced intervals to speed up caret seeking.
|
||||||
|
index []combinedPos
|
||||||
|
|
||||||
caret struct {
|
caret struct {
|
||||||
on bool
|
on bool
|
||||||
scroll bool
|
scroll bool
|
||||||
@@ -448,7 +452,8 @@ func (e *Editor) command(gtx layout.Context, k key.Event) bool {
|
|||||||
if k.Modifiers != key.ModShortcut {
|
if k.Modifiers != key.ModShortcut {
|
||||||
return false
|
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:
|
default:
|
||||||
return false
|
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))
|
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.
|
// offsetToScreenPos takes an offset into the editor text (e.g.
|
||||||
// e.caret.end.ofs) and returns a combinedPos that corresponds to its current
|
// 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
|
// screen position. Hint is either the zero-value position or the result of
|
||||||
@@ -821,7 +818,104 @@ LOOP:
|
|||||||
return hint
|
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() {
|
func (e *Editor) invalidate() {
|
||||||
|
e.index = e.index[:0]
|
||||||
e.valid = false
|
e.valid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,9 @@ func TestEditorCaretConsistency(t *testing.T) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
gotLine, gotCol := e.CaretPos()
|
gotLine, gotCol := e.CaretPos()
|
||||||
gotCoords := e.CaretCoords()
|
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))
|
wantCoords := f32.Pt(float32(want.x)/64, float32(want.y))
|
||||||
if want.lineCol.Y != gotLine || want.lineCol.X != gotCol || gotCoords != wantCoords {
|
if want.lineCol.Y != gotLine || want.lineCol.X != gotCol || gotCoords != wantCoords {
|
||||||
return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s",
|
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
|
_ = e.Events() // throw away any events from this layout
|
||||||
|
|
||||||
// Build the selection events
|
// 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{
|
tq := &testQueue{
|
||||||
events: []event.Event{
|
events: []event.Event{
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
|
|||||||
Reference in New Issue
Block a user