mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-06 18:05:35 +00:00
widget: make editor skip words with key modifier
Signed-off-by: Jack Mordaunt <jackmordaunt@gmail.com>
This commit is contained in:
committed by
Elias Naur
parent
2f67feafc0
commit
d27d1a989e
+51
-2
@@ -7,8 +7,10 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
@@ -266,6 +268,10 @@ func (e *Editor) moveLines(distance int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Editor) command(k key.Event) bool {
|
func (e *Editor) command(k key.Event) bool {
|
||||||
|
modSkip := key.ModCtrl
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
modSkip = key.ModAlt
|
||||||
|
}
|
||||||
switch k.Name {
|
switch k.Name {
|
||||||
case key.NameReturn, key.NameEnter:
|
case key.NameReturn, key.NameEnter:
|
||||||
e.append("\n")
|
e.append("\n")
|
||||||
@@ -278,9 +284,17 @@ func (e *Editor) command(k key.Event) bool {
|
|||||||
case key.NameDownArrow:
|
case key.NameDownArrow:
|
||||||
e.moveLines(+1)
|
e.moveLines(+1)
|
||||||
case key.NameLeftArrow:
|
case key.NameLeftArrow:
|
||||||
e.Move(-1)
|
if k.Modifiers == modSkip {
|
||||||
|
e.moveWord(-1)
|
||||||
|
} else {
|
||||||
|
e.Move(-1)
|
||||||
|
}
|
||||||
case key.NameRightArrow:
|
case key.NameRightArrow:
|
||||||
e.Move(1)
|
if k.Modifiers == modSkip {
|
||||||
|
e.moveWord(1)
|
||||||
|
} else {
|
||||||
|
e.Move(1)
|
||||||
|
}
|
||||||
case key.NamePageUp:
|
case key.NamePageUp:
|
||||||
e.movePages(-1)
|
e.movePages(-1)
|
||||||
case key.NamePageDown:
|
case key.NamePageDown:
|
||||||
@@ -775,6 +789,41 @@ func (e *Editor) moveEnd() {
|
|||||||
e.caret.xoff = l.Width + a - e.caret.x
|
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() {
|
func (e *Editor) scrollToCaret() {
|
||||||
e.makeValid()
|
e.makeValid()
|
||||||
l := e.lines[e.caret.line]
|
l := e.lines[e.caret.line]
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ const (
|
|||||||
moveStart
|
moveStart
|
||||||
moveEnd
|
moveEnd
|
||||||
moveCoord
|
moveCoord
|
||||||
|
moveWord
|
||||||
moveLast // Mark end; never generated.
|
moveLast // Mark end; never generated.
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,6 +141,8 @@ func TestEditorCaretConsistency(t *testing.T) {
|
|||||||
e.moveEnd()
|
e.moveEnd()
|
||||||
case moveCoord:
|
case moveCoord:
|
||||||
e.moveCoord(image.Pt(int(x), int(y)))
|
e.moveCoord(image.Pt(int(x), int(y)))
|
||||||
|
case moveWord:
|
||||||
|
e.moveWord(int(distance))
|
||||||
default:
|
default:
|
||||||
return false
|
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) {
|
func TestEditorNoLayout(t *testing.T) {
|
||||||
var e Editor
|
var e Editor
|
||||||
e.SetText("hi!\n")
|
e.SetText("hi!\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user