widget: add more editor shortcuts

Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
This commit is contained in:
Walter Werner SCHNEIDER
2024-05-04 01:35:53 +03:00
committed by Chris Waldon
parent 8242234274
commit 7a9ce51988
4 changed files with 114 additions and 31 deletions
+10 -6
View File
@@ -312,8 +312,8 @@ func (e *Editor) processPointerEvent(gtx layout.Context, ev event.Event) (Editor
e.text.MoveWord(1, selectionExtend) e.text.MoveWord(1, selectionExtend)
e.dragging = false e.dragging = false
case evt.NumClicks >= 3: case evt.NumClicks >= 3:
e.text.MoveStart(selectionClear) e.text.MoveLineStart(selectionClear)
e.text.MoveEnd(selectionExtend) e.text.MoveLineEnd(selectionExtend)
e.dragging = false e.dragging = false
} }
} }
@@ -374,8 +374,8 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
key.Filter{Focus: e, Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift}, key.Filter{Focus: e, Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift},
key.Filter{Focus: e, Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift}, key.Filter{Focus: e, Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift},
key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShift}, key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShortcut | key.ModShift},
key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShift}, key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShortcut | key.ModShift},
key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift}, key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift},
key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift}, key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift},
condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}), condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}),
@@ -521,6 +521,10 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
} }
} }
} }
case key.NameHome:
e.text.MoveTextStart(selAct)
case key.NameEnd:
e.text.MoveTextEnd(selAct)
} }
return nil, false return nil, false
} }
@@ -582,9 +586,9 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
case key.NamePageDown: case key.NamePageDown:
e.text.MovePages(+1, selAct) e.text.MovePages(+1, selAct)
case key.NameHome: case key.NameHome:
e.text.MoveStart(selAct) e.text.MoveLineStart(selAct)
case key.NameEnd: case key.NameEnd:
e.text.MoveEnd(selAct) e.text.MoveLineEnd(selAct)
} }
return nil, false return nil, false
} }
+77 -17
View File
@@ -256,7 +256,7 @@ func TestEditor(t *testing.T) {
// Regression test for bad in-cluster rune offset math. // Regression test for bad in-cluster rune offset math.
e.SetText("æbc") e.SetText("æbc")
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 3, len("æbc")) assertCaret(t, e, 0, 3, len("æbc"))
textSample := "æbc\naøå••" textSample := "æbc\naøå••"
@@ -268,7 +268,7 @@ func TestEditor(t *testing.T) {
} }
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
assertCaret(t, e, 0, 0, 0) assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 3, len("æbc")) assertCaret(t, e, 0, 3, len("æbc"))
e.MoveCaret(+1, +1) e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 0, len("æbc\n")) assertCaret(t, e, 1, 0, len("æbc\n"))
@@ -276,7 +276,7 @@ func TestEditor(t *testing.T) {
assertCaret(t, e, 0, 3, len("æbc")) assertCaret(t, e, 0, 3, len("æbc"))
e.text.MoveLines(+1, selectionClear) e.text.MoveLines(+1, selectionClear)
assertCaret(t, e, 1, 4, len("æbc\naøå•")) assertCaret(t, e, 1, 4, len("æbc\naøå•"))
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 1, 5, len("æbc\naøå••")) assertCaret(t, e, 1, 5, len("æbc\naøå••"))
e.MoveCaret(+1, +1) e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 5, len("æbc\naøå••")) assertCaret(t, e, 1, 5, len("æbc\naøå••"))
@@ -300,7 +300,7 @@ func TestEditor(t *testing.T) {
// Test that moveLine applies x offsets from previous moves. // Test that moveLine applies x offsets from previous moves.
e.SetText("long line\nshort") e.SetText("long line\nshort")
e.SetCaret(0, 0) e.SetCaret(0, 0)
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
e.text.MoveLines(+1, selectionClear) e.text.MoveLines(+1, selectionClear)
e.text.MoveLines(-1, selectionClear) e.text.MoveLines(-1, selectionClear)
assertCaret(t, e, 0, utf8.RuneCountInString("long line"), len("long line")) assertCaret(t, e, 0, utf8.RuneCountInString("long line"), len("long line"))
@@ -342,14 +342,14 @@ func TestEditorRTL(t *testing.T) {
e.MoveCaret(+1, +1) e.MoveCaret(+1, +1)
assertCaret(t, e, 0, 3, len("الح")) assertCaret(t, e, 0, 3, len("الح"))
// Move to the "end" of the line. This moves to the left edge of the line. // Move to the "end" of the line. This moves to the left edge of the line.
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 4, len("الحب")) assertCaret(t, e, 0, 4, len("الحب"))
sentence := "الحب سماء لا\nتمط غير الأحلام" sentence := "الحب سماء لا\nتمط غير الأحلام"
e.SetText(sentence) e.SetText(sentence)
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
assertCaret(t, e, 0, 0, 0) assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 12, len("الحب سماء لا")) assertCaret(t, e, 0, 12, len("الحب سماء لا"))
e.MoveCaret(+1, +1) e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 0, len("الحب سماء لا\n")) assertCaret(t, e, 1, 0, len("الحب سماء لا\n"))
@@ -361,7 +361,7 @@ func TestEditorRTL(t *testing.T) {
assertCaret(t, e, 0, 12, len("الحب سماء لا")) assertCaret(t, e, 0, 12, len("الحب سماء لا"))
e.text.MoveLines(+1, selectionClear) e.text.MoveLines(+1, selectionClear)
assertCaret(t, e, 1, 14, len("الحب سماء لا\nتمط غير الأحلا")) assertCaret(t, e, 1, 14, len("الحب سماء لا\nتمط غير الأحلا"))
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام")) assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
e.MoveCaret(+1, +1) e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام")) assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
@@ -417,7 +417,7 @@ func TestEditorLigature(t *testing.T) {
assertCaret(t, e, 0, 0, 0) assertCaret(t, e, 0, 0, 0)
e.SetText("fl") // just a ligature e.SetText("fl") // just a ligature
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 2, len("fl")) assertCaret(t, e, 0, 2, len("fl"))
e.MoveCaret(-1, -1) e.MoveCaret(-1, -1)
assertCaret(t, e, 0, 1, len("f")) assertCaret(t, e, 0, 1, len("f"))
@@ -428,7 +428,7 @@ func TestEditorLigature(t *testing.T) {
e.SetText("flaffl•ffi\n•fflfi") // 3 ligatures on line 0, 2 on line 1 e.SetText("flaffl•ffi\n•fflfi") // 3 ligatures on line 0, 2 on line 1
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
assertCaret(t, e, 0, 0, 0) assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 10, len("ffaffl•ffi")) assertCaret(t, e, 0, 10, len("ffaffl•ffi"))
e.MoveCaret(+1, +1) e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 0, len("ffaffl•ffi\n")) assertCaret(t, e, 1, 0, len("ffaffl•ffi\n"))
@@ -481,7 +481,7 @@ func TestEditorLigature(t *testing.T) {
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
// Ensure that all runes in the final cluster of a line are properly // Ensure that all runes in the final cluster of a line are properly
// decoded when moving to the end of the line. This is a regression test. // decoded when moving to the end of the line. This is a regression test.
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
// The first line was broken by line wrapping, not a newline character, and has a trailing // The first line was broken by line wrapping, not a newline character, and has a trailing
// whitespace. However, we should never be able to reach the "other side" of such a trailing // whitespace. However, we should never be able to reach the "other side" of such a trailing
// whitespace glyph. // whitespace glyph.
@@ -548,8 +548,10 @@ const (
moveRune moveRune
moveLine moveLine
movePage movePage
moveStart moveTextStart
moveEnd moveTextEnd
moveLineStart
moveLineEnd
moveCoord moveCoord
moveWord moveWord
deleteWord deleteWord
@@ -599,10 +601,14 @@ func TestEditorCaretConsistency(t *testing.T) {
e.text.MoveLines(int(distance), selectionClear) e.text.MoveLines(int(distance), selectionClear)
case movePage: case movePage:
e.text.MovePages(int(distance), selectionClear) e.text.MovePages(int(distance), selectionClear)
case moveStart: case moveLineStart:
e.text.MoveStart(selectionClear) e.text.MoveLineStart(selectionClear)
case moveEnd: case moveLineEnd:
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
case moveTextStart:
e.text.MoveTextStart(selectionClear)
case moveTextEnd:
e.text.MoveTextEnd(selectionClear)
case moveCoord: case moveCoord:
e.text.MoveCoord(image.Pt(int(x), int(y))) e.text.MoveCoord(image.Pt(int(x), int(y)))
case moveWord: case moveWord:
@@ -879,7 +885,7 @@ func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value {
// to make it much narrower (which makes the lines in the editor reflow), and // to make it much narrower (which makes the lines in the editor reflow), and
// then verifies that the updated (col, line) positions of the selected text // then verifies that the updated (col, line) positions of the selected text
// are where we expect. // are where we expect.
func TestEditorSelect(t *testing.T) { func TestEditorSelectReflow(t *testing.T) {
e := new(Editor) e := new(Editor)
e.SetText(`a 2 4 6 8 a e.SetText(`a 2 4 6 8 a
b 2 4 6 8 b b 2 4 6 8 b
@@ -982,6 +988,60 @@ g 2 4 6 8 g
} }
} }
func TestEditorSelectShortcuts(t *testing.T) {
tFont := font.Font{}
tFontSize := unit.Sp(10)
tShaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
var tEditor = &Editor{
SingleLine: false,
ReadOnly: true,
}
lines := "abc abc abc\ndef def def\nghi ghi ghi"
tEditor.SetText(lines)
type testCase struct {
// Initial text selection.
startPos, endPos int
// Keyboard shortcut to execute.
keyEvent key.Event
// Expected text selection.
selection string
}
pos1, pos2 := 14, 21
for n, tst := range []testCase{
{pos1, pos2, key.Event{Name: "A", Modifiers: key.ModShortcut}, lines},
{pos2, pos1, key.Event{Name: "A", Modifiers: key.ModShortcut}, lines},
{pos1, pos2, key.Event{Name: key.NameHome, Modifiers: key.ModShift}, "def def d"},
{pos1, pos2, key.Event{Name: key.NameEnd, Modifiers: key.ModShift}, "ef"},
{pos2, pos1, key.Event{Name: key.NameHome, Modifiers: key.ModShift}, "de"},
{pos2, pos1, key.Event{Name: key.NameEnd, Modifiers: key.ModShift}, "f def def"},
{pos1, pos2, key.Event{Name: key.NameHome, Modifiers: key.ModShortcut | key.ModShift}, "abc abc abc\ndef def d"},
{pos1, pos2, key.Event{Name: key.NameEnd, Modifiers: key.ModShortcut | key.ModShift}, "ef\nghi ghi ghi"},
{pos2, pos1, key.Event{Name: key.NameHome, Modifiers: key.ModShortcut | key.ModShift}, "abc abc abc\nde"},
{pos2, pos1, key.Event{Name: key.NameEnd, Modifiers: key.ModShortcut | key.ModShift}, "f def def\nghi ghi ghi"},
} {
tRouter := new(input.Router)
gtx := layout.Context{
Ops: new(op.Ops),
Locale: english,
Constraints: layout.Exact(image.Pt(100, 100)),
Source: tRouter.Source(),
}
gtx.Execute(key.FocusCmd{Tag: tEditor})
tEditor.Layout(gtx, tShaper, tFont, tFontSize, op.CallOp{}, op.CallOp{})
tEditor.SetCaret(tst.startPos, tst.endPos)
if cStart, cEnd := tEditor.Selection(); cStart != tst.startPos || cEnd != tst.endPos {
t.Errorf("TestEditorSelect %d: initial selection", n)
}
tRouter.Queue(tst.keyEvent)
tEditor.Update(gtx)
if got := tEditor.SelectedText(); got != tst.selection {
t.Errorf("TestEditorSelect %d: Expected %q, got %q", n, tst.selection, got)
}
}
}
// Verify that an existing selection is dismissed when you press arrow keys. // Verify that an existing selection is dismissed when you press arrow keys.
func TestSelectMove(t *testing.T) { func TestSelectMove(t *testing.T) {
e := new(Editor) e := new(Editor)
+4 -4
View File
@@ -247,8 +247,8 @@ func (e *Selectable) processPointer(gtx layout.Context) {
e.text.MoveWord(1, selectionExtend) e.text.MoveWord(1, selectionExtend)
e.dragging = false e.dragging = false
case evt.NumClicks >= 3: case evt.NumClicks >= 3:
e.text.MoveStart(selectionClear) e.text.MoveLineStart(selectionClear)
e.text.MoveEnd(selectionExtend) e.text.MoveLineEnd(selectionExtend)
e.dragging = false e.dragging = false
} }
} }
@@ -378,9 +378,9 @@ func (e *Selectable) command(gtx layout.Context, k key.Event) {
case key.NamePageDown: case key.NamePageDown:
e.text.MovePages(+1, selAct) e.text.MovePages(+1, selAct)
case key.NameHome: case key.NameHome:
e.text.MoveStart(selAct) e.text.MoveLineStart(selAct)
case key.NameEnd: case key.NameEnd:
e.text.MoveEnd(selAct) e.text.MoveLineEnd(selAct)
} }
} }
+23 -4
View File
@@ -639,9 +639,28 @@ func (e *textView) MoveCaret(startDelta, endDelta int) {
e.caret.end = e.moveByGraphemes(e.caret.end, endDelta) e.caret.end = e.moveByGraphemes(e.caret.end, endDelta)
} }
// MoveStart moves the caret to the start of the current line, ensuring that the resulting // MoveTextStart moves the caret to the start of the text.
func (e *textView) MoveTextStart(selAct selectionAction) {
caret := e.closestToRune(e.caret.end)
e.caret.start = 0
e.caret.end = caret.runes
e.caret.xoff = -caret.x
e.updateSelection(selAct)
e.clampCursorToGraphemes()
}
// MoveTextEnd moves the caret to the end of the text.
func (e *textView) MoveTextEnd(selAct selectionAction) {
caret := e.closestToRune(math.MaxInt)
e.caret.start = caret.runes
e.caret.xoff = fixed.I(e.params.MaxWidth) - caret.x
e.updateSelection(selAct)
e.clampCursorToGraphemes()
}
// MoveLineStart moves the caret to the start of the current line, ensuring that the resulting
// cursor position is on a grapheme cluster boundary. // cursor position is on a grapheme cluster boundary.
func (e *textView) MoveStart(selAct selectionAction) { func (e *textView) MoveLineStart(selAct selectionAction) {
caret := e.closestToRune(e.caret.start) caret := e.closestToRune(e.caret.start)
caret = e.closestToLineCol(caret.lineCol.line, 0) caret = e.closestToLineCol(caret.lineCol.line, 0)
e.caret.start = caret.runes e.caret.start = caret.runes
@@ -650,9 +669,9 @@ func (e *textView) MoveStart(selAct selectionAction) {
e.clampCursorToGraphemes() e.clampCursorToGraphemes()
} }
// MoveEnd moves the caret to the end of the current line, ensuring that the resulting // MoveLineEnd moves the caret to the end of the current line, ensuring that the resulting
// cursor position is on a grapheme cluster boundary. // cursor position is on a grapheme cluster boundary.
func (e *textView) MoveEnd(selAct selectionAction) { func (e *textView) MoveLineEnd(selAct selectionAction) {
caret := e.closestToRune(e.caret.start) caret := e.closestToRune(e.caret.start)
caret = e.closestToLineCol(caret.lineCol.line, math.MaxInt) caret = e.closestToLineCol(caret.lineCol.line, math.MaxInt)
e.caret.start = caret.runes e.caret.start = caret.runes