mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-03 00:16:15 +00:00
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:
+37
-32
@@ -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
@@ -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
@@ -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
@@ -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++
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user