~eliasnaur/gio

e78bd15564e2a07548f56e30c91496cb20a2421d — Larry Clapp 5 months ago cc63a3a
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>
4 files changed, 315 insertions(+), 209 deletions(-)

M widget/buffer.go
M widget/editor.go
M widget/editor_test.go
M widget/label.go
M widget/buffer.go => widget/buffer.go +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) {

M widget/editor.go => widget/editor.go +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
			}
			if lastLine := line == len(e.lines)-1; lastLine || idx > offset {
				break LOOP
			}
			x += adv
			_, s := e.rr.runeAt(idx)
			idx += s
			col++

			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

M widget/editor_test.go => widget/editor_test.go +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
}

M widget/label.go => widget/label.go +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++
		}