~eliasnaur/gio

d27d1a989e25878344caa7de2a6270260d0d825b — Jack Mordaunt 10 months ago 2f67fea
widget: make editor skip words with key modifier

Signed-off-by: Jack Mordaunt <jackmordaunt@gmail.com>
2 files changed, 99 insertions(+), 2 deletions(-)

M widget/editor.go
M widget/editor_test.go
M widget/editor.go => widget/editor.go +51 -2
@@ 7,8 7,10 @@ import (
	"image"
	"io"
	"math"
	"runtime"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"

	"gioui.org/f32"


@@ 266,6 268,10 @@ func (e *Editor) moveLines(distance int) {
}

func (e *Editor) command(k key.Event) bool {
	modSkip := key.ModCtrl
	if runtime.GOOS == "darwin" {
		modSkip = key.ModAlt
	}
	switch k.Name {
	case key.NameReturn, key.NameEnter:
		e.append("\n")


@@ 278,9 284,17 @@ func (e *Editor) command(k key.Event) bool {
	case key.NameDownArrow:
		e.moveLines(+1)
	case key.NameLeftArrow:
		e.Move(-1)
		if k.Modifiers == modSkip {
			e.moveWord(-1)
		} else {
			e.Move(-1)
		}
	case key.NameRightArrow:
		e.Move(1)
		if k.Modifiers == modSkip {
			e.moveWord(1)
		} else {
			e.Move(1)
		}
	case key.NamePageUp:
		e.movePages(-1)
	case key.NamePageDown:


@@ 775,6 789,41 @@ func (e *Editor) moveEnd() {
	e.caret.xoff = l.Width + a - e.caret.x
}

// moveWord moves the caret to the next word in the specified direction.
// Positive is forward, negative is backward.
// Absolute values greater than one will skip that many words.
func (e *Editor) moveWord(distance int) {
	e.makeValid()
	// split the distance information into constituent parts to be
	// used independently.
	words, direction := distance, 1
	if distance < 0 {
		words, direction = distance*-1, -1
	}
	// atEnd if caret is at either side of the buffer.
	atEnd := func() bool {
		return e.rr.caret == 0 || e.rr.caret == 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)
		} else {
			r, _ = e.rr.runeAt(e.rr.caret)
		}
		return r
	}
	for ii := 0; ii < words; ii++ {
		for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() {
			e.Move(direction)
		}
		e.Move(direction)
		for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() {
			e.Move(direction)
		}
	}
}

func (e *Editor) scrollToCaret() {
	e.makeValid()
	l := e.lines[e.caret.line]

M widget/editor_test.go => widget/editor_test.go +48 -0
@@ 91,6 91,7 @@ const (
	moveStart
	moveEnd
	moveCoord
	moveWord
	moveLast // Mark end; never generated.
)



@@ 140,6 141,8 @@ func TestEditorCaretConsistency(t *testing.T) {
				e.moveEnd()
			case moveCoord:
				e.moveCoord(image.Pt(int(x), int(y)))
			case moveWord:
				e.moveWord(int(distance))
			default:
				return false
			}


@@ 155,6 158,51 @@ func TestEditorCaretConsistency(t *testing.T) {
	}
}

func TestEditorMoveWord(t *testing.T) {
	type Test struct {
		Text  string
		Start int
		Skip  int
		Want  int
	}
	tests := []Test{
		{"", 0, 0, 0},
		{"", 0, -1, 0},
		{"", 0, 1, 0},
		{"hello", 0, -1, 0},
		{"hello", 0, 1, 5},
		{"hello world", 3, 1, 5},
		{"hello world", 3, -1, 0},
		{"hello world", 8, -1, 6},
		{"hello world", 8, 1, 11},
		{"hello    world", 3, 1, 5},
		{"hello    world", 3, 2, 14},
		{"hello    world", 8, 1, 14},
		{"hello    world", 8, -1, 0},
		{"hello brave new world", 0, 3, 15},
	}
	setup := func(t string) *Editor {
		e := new(Editor)
		gtx := layout.Context{
			Ops:         new(op.Ops),
			Constraints: layout.Exact(image.Pt(100, 100)),
		}
		cache := text.NewCache(gofont.Collection())
		fontSize := unit.Px(10)
		font := text.Font{}
		e.SetText(t)
		e.Layout(gtx, cache, font, fontSize)
		return e
	}
	for ii, tt := range tests {
		e := setup(tt.Text)
		e.Move(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)
		}
	}
}
func TestEditorNoLayout(t *testing.T) {
	var e Editor
	e.SetText("hi!\n")