widget: refactoring to prep for editor selection

- Move caret from editBuffer.caret to Editor.caret.pos.ofs and related
  refactoring. Move other fields in Editor.caret into Editor.caret.pos.
- Refactor several functions to change a position passed into them,
  rather than changing e.rr.caret directly.
- Add editBuffer.Seek().
- Remove editBuffer.dump().
- Change Editor.Move to MoveCaret.
- Add Editor.SetCaret.
- Updated tests.

Signed-off-by: Larry Clapp <larry@theclapp.org>
This commit is contained in:
Larry Clapp
2021-01-20 20:36:00 -05:00
committed by Elias Naur
parent cc63a3aeb7
commit e78bd15564
4 changed files with 315 additions and 209 deletions
+37 -32
View File
@@ -3,18 +3,13 @@
package widget
import (
"fmt"
"io"
"strings"
"unicode/utf8"
)
const bufferDebug = false
// editBuffer implements a gap buffer for text editing.
type editBuffer struct {
// caret is the caret position in bytes.
caret int
// pos is the byte position for Read and ReadRune.
pos int
@@ -35,12 +30,12 @@ func (e *editBuffer) Changed() bool {
return c
}
func (e *editBuffer) deleteRunes(runes int) {
e.moveGap(0)
func (e *editBuffer) deleteRunes(caret, runes int) int {
e.moveGap(caret, 0)
for ; runes < 0 && e.gapstart > 0; runes++ {
_, s := utf8.DecodeLastRune(e.text[:e.gapstart])
e.gapstart -= s
e.caret -= s
caret -= s
e.changed = e.changed || s > 0
}
for ; runes > 0 && e.gapend < len(e.text); runes-- {
@@ -48,12 +43,12 @@ func (e *editBuffer) deleteRunes(runes int) {
e.gapend += s
e.changed = e.changed || s > 0
}
e.dump()
return caret
}
// moveGap moves the gap to the caret position. After returning,
// the gap is guaranteed to be at least space bytes long.
func (e *editBuffer) moveGap(space int) {
func (e *editBuffer) moveGap(caret, space int) {
if e.gapLen() < space {
if space < minSpace {
space = minSpace
@@ -62,29 +57,28 @@ func (e *editBuffer) moveGap(space int) {
// Expand to capacity.
txt = txt[:cap(txt)]
gaplen := len(txt) - e.len()
if e.caret > e.gapstart {
if caret > e.gapstart {
copy(txt, e.text[:e.gapstart])
copy(txt[e.caret+gaplen:], e.text[e.caret:])
copy(txt[e.gapstart:], e.text[e.gapend:e.caret+e.gapLen()])
copy(txt[caret+gaplen:], e.text[caret:])
copy(txt[e.gapstart:], e.text[e.gapend:caret+e.gapLen()])
} else {
copy(txt, e.text[:e.caret])
copy(txt, e.text[:caret])
copy(txt[e.gapstart+gaplen:], e.text[e.gapend:])
copy(txt[e.caret+gaplen:], e.text[e.caret:e.gapstart])
copy(txt[caret+gaplen:], e.text[caret:e.gapstart])
}
e.text = txt
e.gapstart = e.caret
e.gapstart = caret
e.gapend = e.gapstart + gaplen
} else {
if e.caret > e.gapstart {
copy(e.text[e.gapstart:], e.text[e.gapend:e.caret+e.gapLen()])
if caret > e.gapstart {
copy(e.text[e.gapstart:], e.text[e.gapend:caret+e.gapLen()])
} else {
copy(e.text[e.caret+e.gapLen():], e.text[e.caret:e.gapstart])
copy(e.text[caret+e.gapLen():], e.text[caret:e.gapstart])
}
l := e.gapLen()
e.gapstart = e.caret
e.gapstart = caret
e.gapend = e.gapstart + l
}
e.dump()
}
func (e *editBuffer) len() int {
@@ -96,7 +90,25 @@ func (e *editBuffer) gapLen() int {
}
func (e *editBuffer) Reset() {
e.pos = 0
e.Seek(0, io.SeekStart)
}
// Seek implements io.Seeker
func (e *editBuffer) Seek(offset int64, whence int) (ret int64, err error) {
switch whence {
case io.SeekStart:
e.pos = int(offset)
case io.SeekCurrent:
e.pos += int(offset)
case io.SeekEnd:
e.pos = e.len() - int(offset)
}
if e.pos < 0 {
e.pos = 0
} else if e.pos > e.len() {
e.pos = e.len()
}
return int64(e.pos), nil
}
func (e *editBuffer) Read(p []byte) (int, error) {
@@ -138,18 +150,11 @@ func (e *editBuffer) String() string {
return b.String()
}
func (e *editBuffer) prepend(s string) {
e.moveGap(len(s))
copy(e.text[e.caret:], s)
func (e *editBuffer) prepend(caret int, s string) {
e.moveGap(caret, len(s))
copy(e.text[caret:], s)
e.gapstart += len(s)
e.changed = e.changed || len(s) > 0
e.dump()
}
func (e *editBuffer) dump() {
if bufferDebug {
fmt.Printf("len(e.text) %d e.len() %d e.gapstart %d e.gapend %d e.caret %d txt:\n'%+x'<-%d->'%+x'\n", len(e.text), e.len(), e.gapstart, e.gapend, e.caret, e.text[:e.gapstart], e.gapLen(), e.text[e.gapend:])
}
}
func (e *editBuffer) runeBefore(idx int) (rune, int) {
+238 -143
View File
@@ -9,6 +9,7 @@ import (
"io"
"math"
"runtime"
"sort"
"strings"
"time"
"unicode"
@@ -64,18 +65,8 @@ type Editor struct {
caret struct {
on bool
scroll bool
// xoff is the offset to the current caret
// position when moving between lines.
xoff fixed.Int26_6
// line is the caret line position as an index into lines.
line int
// col is the caret column measured in runes.
col int
// (x, y) are the caret coordinates.
x fixed.Int26_6
y int
// pos is the current caret position.
pos combinedPos
}
scroller gesture.Scroll
@@ -99,6 +90,23 @@ type maskReader struct {
overflow []byte
}
// combinedPos is a point in the editor.
type combinedPos struct {
// editorBuffer offset. The other three fields are based off of this one.
ofs int
// lineCol.Y = line (offset into Editor.lines), and X = col (offset into
// Editor.lines[Y])
lineCol screenPos
// Pixel coordinates
x fixed.Int26_6
y int
// xoff is the offset to the current position when moving between lines.
xoff fixed.Int26_6
}
func (m *maskReader) Reset(r io.RuneReader, mr rune) {
m.rr = r
n := utf8.EncodeRune(m.maskBuf[:], mr)
@@ -177,16 +185,24 @@ func (e *Editor) processEvents(gtx layout.Context) {
e.processKey(gtx)
}
func (e *Editor) makeValid() {
func (e *Editor) makeValid(positions ...*combinedPos) {
if e.valid {
return
}
e.lines, e.dims = e.layoutText(e.shaper)
line, col, x, y := e.layoutCaret()
e.caret.line = line
e.caret.col = col
e.caret.x = x
e.caret.y = y
// Jump through some hoops to order the offsets given to offsetToScreenPos,
// but still be able to update them correctly with the results thereof.
positions = append(positions, &e.caret.pos)
sort.Slice(positions, func(i, j int) bool {
return positions[i].ofs < positions[j].ofs
})
var iter func(offset int) combinedPos
*positions[0], iter = e.offsetToScreenPos(positions[0].ofs)
for _, cp := range positions[1:] {
*cp = iter(cp.ofs)
}
e.valid = true
}
@@ -259,6 +275,7 @@ func (e *Editor) processKey(gtx layout.Context) {
e.caret.scroll = true
e.scroller.Stop()
e.append(ke.Text)
// Complete a paste event, initiated by Shortcut-V in Editor.command().
case clipboard.Event:
e.caret.scroll = true
e.scroller.Stop()
@@ -271,7 +288,7 @@ func (e *Editor) processKey(gtx layout.Context) {
}
func (e *Editor) moveLines(distance int) {
e.moveToLine(e.caret.x+e.caret.xoff, e.caret.line+distance)
e.caret.pos = e.movePosToLine(e.caret.pos, e.caret.pos.x+e.caret.pos.xoff, e.caret.pos.lineCol.Y+distance)
}
func (e *Editor) command(gtx layout.Context, k key.Event) bool {
@@ -279,17 +296,18 @@ func (e *Editor) command(gtx layout.Context, k key.Event) bool {
if runtime.GOOS == "darwin" {
modSkip = key.ModAlt
}
moveByWord := k.Modifiers.Contain(modSkip)
switch k.Name {
case key.NameReturn, key.NameEnter:
e.append("\n")
case key.NameDeleteBackward:
if k.Modifiers == modSkip {
if moveByWord {
e.deleteWord(-1)
} else {
e.Delete(-1)
}
case key.NameDeleteForward:
if k.Modifiers == modSkip {
if moveByWord {
e.deleteWord(1)
} else {
e.Delete(1)
@@ -299,16 +317,16 @@ func (e *Editor) command(gtx layout.Context, k key.Event) bool {
case key.NameDownArrow:
e.moveLines(+1)
case key.NameLeftArrow:
if k.Modifiers == modSkip {
if moveByWord {
e.moveWord(-1)
} else {
e.Move(-1)
e.MoveCaret(-1)
}
case key.NameRightArrow:
if k.Modifiers == modSkip {
if moveByWord {
e.moveWord(1)
} else {
e.Move(1)
e.MoveCaret(1)
}
case key.NamePageUp:
e.movePages(-1)
@@ -318,11 +336,14 @@ func (e *Editor) command(gtx layout.Context, k key.Event) bool {
e.moveStart()
case key.NameEnd:
e.moveEnd()
// Initiate a paste operation, by requesting the clipboard contents; other
// half is in Editor.processKey() under clipboard.Event.
case "V":
if k.Modifiers != key.ModShortcut {
return false
}
clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops)
// Copy all text.
case "C":
if k.Modifiers != key.ModShortcut {
return false
@@ -466,12 +487,12 @@ func (e *Editor) PaintCaret(gtx layout.Context) {
}
e.makeValid()
carWidth := fixed.I(gtx.Px(unit.Dp(1)))
carX := e.caret.x
carY := e.caret.y
carX := e.caret.pos.x
carY := e.caret.pos.y
defer op.Save(gtx.Ops).Load()
carX -= carWidth / 2
carAsc, carDesc := -e.lines[e.caret.line].Bounds.Min.Y, e.lines[e.caret.line].Bounds.Max.Y
carAsc, carDesc := -e.lines[e.caret.pos.lineCol.Y].Bounds.Min.Y, e.lines[e.caret.pos.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()},
@@ -512,7 +533,7 @@ func (e *Editor) Text() string {
// SetText replaces the contents of the editor.
func (e *Editor) SetText(s string) {
e.rr = editBuffer{}
e.caret.xoff = 0
e.caret.pos = combinedPos{}
e.prepend(s)
}
@@ -569,8 +590,8 @@ func (e *Editor) moveCoord(pos image.Point) {
carLine++
}
x := fixed.I(pos.X + e.scrollOff.X)
e.moveToLine(x, carLine)
e.caret.xoff = 0
e.caret.pos = e.movePosToLine(e.caret.pos, x, carLine)
e.caret.pos.xoff = 0
}
func (e *Editor) layoutText(s text.Shaper) ([]text.Line, layout.Dimensions) {
@@ -604,42 +625,65 @@ 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.line, e.caret.col
return e.caret.pos.lineCol.Y, e.caret.pos.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.x)/64, float32(e.caret.y))
return f32.Pt(float32(e.caret.pos.x)/64, float32(e.caret.pos.y))
}
func (e *Editor) layoutCaret() (line, col int, x fixed.Int26_6, y int) {
var idx int
var prevDesc fixed.Int26_6
loop:
for {
x = 0
col = 0
l := e.lines[line]
y += (prevDesc + l.Ascent).Ceil()
prevDesc = l.Descent
for _, adv := range l.Layout.Advances {
if idx == e.rr.caret {
break loop
// 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, as well as an iterator that lets you get the combinedPos
// of a later offset. The offsets given to offsetToScreenPos and to the
// returned iterator must be sorted, lowest first, and they must be valid (0
// <= offset <= e.Len()).
//
// This function is written this way to take advantage of previous work done
// for offsets after the first. Otherwise you have to start from the top each
// time.
func (e *Editor) offsetToScreenPos(offset int) (combinedPos, func(int) combinedPos) {
var col, line, idx int
var x fixed.Int26_6
l := e.lines[line]
y := l.Ascent.Ceil()
prevDesc := l.Descent
iter := func(offset int) combinedPos {
LOOP:
for {
for ; col < len(l.Layout.Advances); col++ {
if idx >= offset {
break LOOP
}
x += l.Layout.Advances[col]
_, s := e.rr.runeAt(idx)
idx += s
}
x += adv
_, s := e.rr.runeAt(idx)
idx += s
col++
if lastLine := line == len(e.lines)-1; lastLine || idx > offset {
break LOOP
}
line++
x = 0
col = 0
l = e.lines[line]
y += (prevDesc + l.Ascent).Ceil()
prevDesc = l.Descent
}
if line == len(e.lines)-1 || idx > e.rr.caret {
break
return combinedPos{
lineCol: screenPos{Y: line, X: col},
x: x + align(e.Alignment, e.lines[line].Width, e.viewSize.X),
y: y,
ofs: offset,
}
line++
}
x += align(e.Alignment, e.lines[line].Width, e.viewSize.X)
return
return iter(offset), iter
}
func (e *Editor) invalidate() {
@@ -649,8 +693,11 @@ func (e *Editor) invalidate() {
// Delete runes from the caret position. The sign of runes specifies the
// direction to delete: positive is forward, negative is backward.
func (e *Editor) Delete(runes int) {
e.rr.deleteRunes(runes)
e.caret.xoff = 0
if runes == 0 {
return
}
e.caret.pos.ofs = e.rr.deleteRunes(e.caret.pos.ofs, runes)
e.caret.pos.xoff = 0
e.invalidate()
}
@@ -658,26 +705,29 @@ func (e *Editor) Delete(runes int) {
func (e *Editor) Insert(s string) {
e.append(s)
e.caret.scroll = true
e.invalidate()
}
// append inserts s at the cursor, leaving the caret is at the end of s.
// xxx|yyy + append zzz => xxxzzz|yyy
func (e *Editor) append(s string) {
e.prepend(s)
e.rr.caret += len(s)
e.caret.pos.ofs += len(s)
}
// prepend inserts s after the cursor; the caret does not change.
// xxx|yyy + prepend zzz => xxx|zzzyyy
func (e *Editor) prepend(s string) {
if e.SingleLine {
s = strings.ReplaceAll(s, "\n", " ")
}
e.rr.prepend(s)
e.caret.xoff = 0
e.rr.prepend(e.caret.pos.ofs, s)
e.caret.pos.xoff = 0
e.invalidate()
}
func (e *Editor) movePages(pages int) {
e.makeValid()
y := e.caret.y + pages*e.viewSize.Y
y := e.caret.pos.y + pages*e.viewSize.Y
var (
prevDesc fixed.Int26_6
carLine2 int
@@ -696,11 +746,11 @@ func (e *Editor) movePages(pages int) {
y2 += h
carLine2++
}
e.moveToLine(e.caret.x+e.caret.xoff, carLine2)
e.caret.pos = e.movePosToLine(e.caret.pos, e.caret.pos.x+e.caret.pos.xoff, carLine2)
}
func (e *Editor) moveToLine(x fixed.Int26_6, line int) {
e.makeValid()
func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6, line int) combinedPos {
e.makeValid(&pos)
if line < 0 {
line = 0
}
@@ -709,31 +759,31 @@ func (e *Editor) moveToLine(x fixed.Int26_6, line int) {
}
prevDesc := e.lines[line].Descent
for e.caret.line < line {
e.moveEnd()
l := e.lines[e.caret.line]
_, s := e.rr.runeAt(e.rr.caret)
e.rr.caret += s
e.caret.y += (prevDesc + l.Ascent).Ceil()
e.caret.col = 0
for pos.lineCol.Y < line {
pos = e.movePosToEnd(pos)
l := e.lines[pos.lineCol.Y]
_, s := e.rr.runeAt(pos.ofs)
pos.ofs += s
pos.y += (prevDesc + l.Ascent).Ceil()
pos.lineCol.X = 0
prevDesc = l.Descent
e.caret.line++
pos.lineCol.Y++
}
for e.caret.line > line {
e.moveStart()
l := e.lines[e.caret.line]
_, s := e.rr.runeBefore(e.rr.caret)
e.rr.caret -= s
e.caret.y -= (prevDesc + l.Ascent).Ceil()
for pos.lineCol.Y > line {
pos = e.movePosToStart(pos)
l := e.lines[pos.lineCol.Y]
_, s := e.rr.runeBefore(pos.ofs)
pos.ofs -= s
pos.y -= (prevDesc + l.Ascent).Ceil()
prevDesc = l.Descent
e.caret.line--
l = e.lines[e.caret.line]
e.caret.col = len(l.Layout.Advances) - 1
pos.lineCol.Y--
l = e.lines[pos.lineCol.Y]
pos.lineCol.X = len(l.Layout.Advances) - 1
}
e.moveStart()
pos = e.movePosToStart(pos)
l := e.lines[line]
e.caret.x = align(e.Alignment, l.Width, e.viewSize.X)
pos.x = align(e.Alignment, l.Width, e.viewSize.X)
// Only move past the end of the last line
end := 0
if line < len(e.lines)-1 {
@@ -742,86 +792,103 @@ func (e *Editor) moveToLine(x fixed.Int26_6, line int) {
// Move to rune closest to x.
for i := 0; i < len(l.Layout.Advances)-end; i++ {
adv := l.Layout.Advances[i]
if e.caret.x >= x {
if pos.x >= x {
break
}
if e.caret.x+adv-x >= x-e.caret.x {
if pos.x+adv-x >= x-pos.x {
break
}
e.caret.x += adv
_, s := e.rr.runeAt(e.rr.caret)
e.rr.caret += s
e.caret.col++
pos.x += adv
_, s := e.rr.runeAt(pos.ofs)
pos.ofs += s
pos.lineCol.X++
}
e.caret.xoff = x - e.caret.x
pos.xoff = x - pos.x
return pos
}
// Move the caret: positive distance moves forward, negative distance moves
// backward.
func (e *Editor) Move(distance int) {
// MoveCaret moves the caret relative to its current position. Positive
// distance moves forward, negative distance moves backward. Distance is in
// runes.
func (e *Editor) MoveCaret(distance int) {
e.makeValid()
for ; distance < 0 && e.rr.caret > 0; distance++ {
if e.caret.col == 0 {
e.caret.pos = e.movePos(e.caret.pos, distance)
e.caret.pos.xoff = 0
}
func (e *Editor) movePos(pos combinedPos, distance int) combinedPos {
for ; distance < 0 && pos.ofs > 0; distance++ {
if pos.lineCol.X == 0 {
// Move to end of previous line.
e.moveToLine(fixed.I(e.maxWidth), e.caret.line-1)
pos = e.movePosToLine(pos, fixed.I(e.maxWidth), pos.lineCol.Y-1)
continue
}
l := e.lines[e.caret.line].Layout
_, s := e.rr.runeBefore(e.rr.caret)
e.rr.caret -= s
e.caret.col--
e.caret.x -= l.Advances[e.caret.col]
l := e.lines[pos.lineCol.Y].Layout
_, s := e.rr.runeBefore(pos.ofs)
pos.ofs -= s
pos.lineCol.X--
pos.x -= l.Advances[pos.lineCol.X]
}
for ; distance > 0 && e.rr.caret < e.rr.len(); distance-- {
l := e.lines[e.caret.line].Layout
for ; distance > 0 && pos.ofs < e.rr.len(); distance-- {
l := e.lines[pos.lineCol.Y].Layout
// Only move past the end of the last line
end := 0
if e.caret.line < len(e.lines)-1 {
if pos.lineCol.Y < len(e.lines)-1 {
end = 1
}
if e.caret.col >= len(l.Advances)-end {
if pos.lineCol.X >= len(l.Advances)-end {
// Move to start of next line.
e.moveToLine(0, e.caret.line+1)
pos = e.movePosToLine(pos, 0, pos.lineCol.Y+1)
continue
}
e.caret.x += l.Advances[e.caret.col]
_, s := e.rr.runeAt(e.rr.caret)
e.rr.caret += s
e.caret.col++
pos.x += l.Advances[pos.lineCol.X]
_, s := e.rr.runeAt(pos.ofs)
pos.ofs += s
pos.lineCol.X++
}
e.caret.xoff = 0
return pos
}
func (e *Editor) moveStart() {
e.makeValid()
layout := e.lines[e.caret.line].Layout
for i := e.caret.col - 1; i >= 0; i-- {
_, s := e.rr.runeBefore(e.rr.caret)
e.rr.caret -= s
e.caret.x -= layout.Advances[i]
e.caret.pos = e.movePosToStart(e.caret.pos)
}
func (e *Editor) movePosToStart(pos combinedPos) combinedPos {
e.makeValid(&pos)
layout := e.lines[pos.lineCol.Y].Layout
for i := pos.lineCol.X - 1; i >= 0; i-- {
_, s := e.rr.runeBefore(pos.ofs)
pos.ofs -= s
pos.x -= layout.Advances[i]
}
e.caret.col = 0
e.caret.xoff = -e.caret.x
pos.lineCol.X = 0
pos.xoff = -pos.x
return pos
}
func (e *Editor) moveEnd() {
e.makeValid()
l := e.lines[e.caret.line]
e.caret.pos = e.movePosToEnd(e.caret.pos)
}
func (e *Editor) movePosToEnd(pos combinedPos) combinedPos {
e.makeValid(&pos)
l := e.lines[pos.lineCol.Y]
// Only move past the end of the last line
end := 0
if e.caret.line < len(e.lines)-1 {
if pos.lineCol.Y < len(e.lines)-1 {
end = 1
}
layout := l.Layout
for i := e.caret.col; i < len(layout.Advances)-end; i++ {
for i := pos.lineCol.X; i < len(layout.Advances)-end; i++ {
adv := layout.Advances[i]
_, s := e.rr.runeAt(e.rr.caret)
e.rr.caret += s
e.caret.x += adv
e.caret.col++
_, s := e.rr.runeAt(pos.ofs)
pos.ofs += s
pos.x += adv
pos.lineCol.X++
}
a := align(e.Alignment, l.Width, e.viewSize.X)
e.caret.xoff = l.Width + a - e.caret.x
pos.xoff = l.Width + a - pos.x
return pos
}
// moveWord moves the caret to the next word in the specified direction.
@@ -837,33 +904,37 @@ func (e *Editor) moveWord(distance int) {
}
// atEnd if caret is at either side of the buffer.
atEnd := func() bool {
return e.rr.caret == 0 || e.rr.caret == e.rr.len()
return e.caret.pos.ofs == 0 || e.caret.pos.ofs == e.rr.len()
}
// next returns the appropriate rune given the direction.
next := func() (r rune) {
if direction < 0 {
r, _ = e.rr.runeBefore(e.rr.caret)
r, _ = e.rr.runeBefore(e.caret.pos.ofs)
} else {
r, _ = e.rr.runeAt(e.rr.caret)
r, _ = e.rr.runeAt(e.caret.pos.ofs)
}
return r
}
for ii := 0; ii < words; ii++ {
for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() {
e.Move(direction)
e.MoveCaret(direction)
}
e.Move(direction)
e.MoveCaret(direction)
for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() {
e.Move(direction)
e.MoveCaret(direction)
}
}
}
// deleteWord the next word(s) in the specified direction.
// deleteWord deletes the next word(s) in the specified direction.
// Unlike moveWord, deleteWord treats whitespace as a word itself.
// Positive is forward, negative is backward.
// Absolute values greater than one will delete that many words.
func (e *Editor) deleteWord(distance int) {
if distance == 0 {
return
}
e.makeValid()
// split the distance information into constituent parts to be
// used independently.
@@ -873,12 +944,12 @@ func (e *Editor) deleteWord(distance int) {
}
// atEnd if offset is at or beyond either side of the buffer.
atEnd := func(offset int) bool {
idx := e.rr.caret + offset*direction
idx := e.caret.pos.ofs + offset*direction
return idx <= 0 || idx >= e.rr.len()
}
// next returns the appropriate rune given the direction and offset.
next := func(offset int) (r rune) {
idx := e.rr.caret + offset*direction
idx := e.caret.pos.ofs + offset*direction
if idx < 0 {
idx = 0
} else if idx > e.rr.len() {
@@ -908,18 +979,18 @@ func (e *Editor) deleteWord(distance int) {
func (e *Editor) scrollToCaret() {
e.makeValid()
l := e.lines[e.caret.line]
l := e.lines[e.caret.pos.lineCol.Y]
if e.SingleLine {
var dist int
if d := e.caret.x.Floor() - e.scrollOff.X; d < 0 {
if d := e.caret.pos.x.Floor() - e.scrollOff.X; d < 0 {
dist = d
} else if d := e.caret.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 {
} else if d := e.caret.pos.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 {
dist = d
}
e.scrollRel(dist, 0)
} else {
miny := e.caret.y - l.Ascent.Ceil()
maxy := e.caret.y + l.Descent.Ceil()
miny := e.caret.pos.y - l.Ascent.Ceil()
maxy := e.caret.pos.y + l.Descent.Ceil()
var dist int
if d := miny - e.scrollOff.Y; d < 0 {
dist = d
@@ -936,6 +1007,30 @@ func (e *Editor) NumLines() int {
return len(e.lines)
}
// SetCaret moves the caret to ofs. ofs is in bytes, and represent an offset
// into the editor text. ofs must be at a rune boundary.
func (e *Editor) SetCaret(ofs int) {
e.makeValid()
// Constrain ofs to [0, e.Len()].
e.caret.pos, _ = e.offsetToScreenPos(max(min(ofs, e.Len()), 0))
e.caret.scroll = true
e.scroller.Stop()
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func nullLayout(r io.Reader) ([]text.Line, error) {
rr := bufio.NewReader(r)
var rerr error
+35 -27
View File
@@ -36,24 +36,31 @@ func TestEditor(t *testing.T) {
assertCaret(t, e, 0, 0, 0)
e.moveEnd()
assertCaret(t, e, 0, 3, len("æbc"))
e.Move(+1)
e.MoveCaret(+1)
assertCaret(t, e, 1, 0, len("æbc\n"))
e.Move(-1)
e.MoveCaret(-1)
assertCaret(t, e, 0, 3, len("æbc"))
e.moveLines(+1)
assertCaret(t, e, 1, 3, len("æbc\naøå"))
e.moveEnd()
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
e.Move(+1)
e.MoveCaret(+1)
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
e.SetCaret(0)
assertCaret(t, e, 0, 0, 0)
e.SetCaret(len("æ"))
assertCaret(t, e, 0, 1, 2)
e.SetCaret(len("æbc\naøå•"))
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
// Ensure that password masking does not affect caret behavior
e.Move(-3)
e.MoveCaret(-3)
assertCaret(t, e, 1, 1, len("æbc\na"))
e.Mask = '*'
e.Layout(gtx, cache, font, fontSize)
assertCaret(t, e, 1, 1, len("æbc\na"))
e.Move(-3)
e.MoveCaret(-3)
assertCaret(t, e, 0, 2, len("æb"))
e.Mask = '\U0001F92B'
e.Layout(gtx, cache, font, fontSize)
@@ -91,14 +98,6 @@ func TestEditorDimensions(t *testing.T) {
}
}
type testQueue struct {
events []event.Event
}
func (q *testQueue) Events(_ event.Tag) []event.Event {
return q.events
}
// assertCaret asserts that the editor caret is at a particular line
// and column, and that the byte position matches as well.
func assertCaret(t *testing.T, e *Editor, line, col, bytes int) {
@@ -107,8 +106,8 @@ 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.rr.caret {
t.Errorf("caret at buffer position %d, expected %d", e.rr.caret, bytes)
if bytes != e.caret.pos.ofs {
t.Errorf("caret at buffer position %d, expected %d", e.caret.pos.ofs, bytes)
}
}
@@ -145,12 +144,13 @@ func TestEditorCaretConsistency(t *testing.T) {
t.Helper()
gotLine, gotCol := e.CaretPos()
gotCoords := e.CaretCoords()
wantLine, wantCol, wantX, wantY := e.layoutCaret()
wantCoords := f32.Pt(float32(wantX)/64, float32(wantY))
if wantLine == gotLine && wantCol == gotCol && gotCoords == wantCoords {
want, _ := e.offsetToScreenPos(e.caret.pos.ofs)
wantCoords := f32.Pt(float32(want.x)/64, float32(want.y))
if want.lineCol.Y == gotLine && want.lineCol.X == gotCol && gotCoords == wantCoords {
return nil
}
return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s", gotLine, gotCol, gotCoords, wantLine, wantCol, wantCoords)
return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s",
gotLine, gotCol, gotCoords, want.lineCol.Y, want.lineCol.X, wantCoords)
}
if err := consistent(); err != nil {
t.Errorf("initial editor inconsistency (alignment %s): %v", a, err)
@@ -162,7 +162,7 @@ func TestEditorCaretConsistency(t *testing.T) {
e.SetText(str)
e.Layout(gtx, cache, font, fontSize)
case moveRune:
e.Move(int(distance))
e.MoveCaret(int(distance))
case moveLine:
e.moveLines(int(distance))
case movePage:
@@ -230,10 +230,10 @@ func TestEditorMoveWord(t *testing.T) {
}
for ii, tt := range tests {
e := setup(tt.Text)
e.Move(tt.Start)
e.MoveCaret(tt.Start)
e.moveWord(tt.Skip)
if e.rr.caret != tt.Want {
t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, e.rr.caret, tt.Want)
if e.caret.pos.ofs != tt.Want {
t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, e.caret.pos.ofs, tt.Want)
}
}
}
@@ -278,10 +278,10 @@ func TestEditorDeleteWord(t *testing.T) {
}
for ii, tt := range tests {
e := setup(tt.Text)
e.Move(tt.Start)
e.MoveCaret(tt.Start)
e.deleteWord(tt.Delete)
if e.rr.caret != tt.Want {
t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, e.rr.caret, tt.Want)
if e.caret.pos.ofs != tt.Want {
t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, e.caret.pos.ofs, tt.Want)
}
if e.Text() != tt.Result {
t.Fatalf("[%d] deleteWord: invalid result: got %q, want %q", ii, e.Text(), tt.Result)
@@ -292,7 +292,7 @@ func TestEditorDeleteWord(t *testing.T) {
func TestEditorNoLayout(t *testing.T) {
var e Editor
e.SetText("hi!\n")
e.Move(1)
e.MoveCaret(1)
}
// Generate generates a value of itself, for testing/quick.
@@ -300,3 +300,11 @@ func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value {
t := editMutation(rand.Intn(int(moveLast)))
return reflect.ValueOf(t)
}
type testQueue struct {
events []event.Event
}
func (q *testQueue) Events(_ event.Tag) []event.Event {
return q.events
}
+5 -7
View File
@@ -25,6 +25,10 @@ type Label struct {
MaxLines int
}
// screenPos describes a character position (in text line and column numbers,
// not pixels): Y = line number, X = rune column.
type screenPos image.Point
type lineIterator struct {
Lines []text.Line
Clip image.Rectangle
@@ -33,7 +37,6 @@ type lineIterator struct {
Offset image.Point
y, prevDesc fixed.Int26_6
txtOff int
}
const inf = 1e6
@@ -53,8 +56,6 @@ func (l *lineIterator) Next() (text.Layout, image.Point, bool) {
break
}
layout := line.Layout
start := l.txtOff
l.txtOff += len(line.Layout.Text)
if (off.Y + line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y {
continue
}
@@ -67,18 +68,15 @@ func (l *lineIterator) Next() (text.Layout, image.Point, bool) {
off.X += adv
layout.Text = layout.Text[n:]
layout.Advances = layout.Advances[1:]
start += n
}
end := start
endx := off.X
rune := 0
for n, r := range layout.Text {
for n := range layout.Text {
if (endx + line.Bounds.Min.X).Floor() > l.Clip.Max.X {
layout.Advances = layout.Advances[:rune]
layout.Text = layout.Text[:n]
break
}
end += utf8.RuneLen(r)
endx += layout.Advances[rune]
rune++
}