gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/widget/editor_test.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package widget
     4  
     5  import (
     6  	"bytes"
     7  	"fmt"
     8  	"image"
     9  	"io"
    10  	"math/rand"
    11  	"reflect"
    12  	"testing"
    13  	"testing/quick"
    14  	"time"
    15  	"unicode/utf8"
    16  
    17  	nsareg "eliasnaur.com/font/noto/sans/arabic/regular"
    18  	"eliasnaur.com/font/roboto/robotoregular"
    19  	"gioui.org/f32"
    20  	"gioui.org/font"
    21  	"gioui.org/font/gofont"
    22  	"gioui.org/font/opentype"
    23  	"gioui.org/io/input"
    24  	"gioui.org/io/key"
    25  	"gioui.org/io/pointer"
    26  	"gioui.org/io/system"
    27  	"gioui.org/layout"
    28  	"gioui.org/op"
    29  	"gioui.org/text"
    30  	"gioui.org/unit"
    31  )
    32  
    33  var english = system.Locale{
    34  	Language:  "EN",
    35  	Direction: system.LTR,
    36  }
    37  
    38  // TestEditorHistory ensures that undo and redo behave correctly.
    39  func TestEditorHistory(t *testing.T) {
    40  	e := new(Editor)
    41  	// Insert some multi-byte unicode text.
    42  	e.SetText("안П你 hello 안П你")
    43  	assertContents(t, e, "안П你 hello 안П你", 0, 0)
    44  	// Overwrite all of the text with the empty string.
    45  	e.SetCaret(0, len([]rune("안П你 hello 안П你")))
    46  	e.Insert("")
    47  	assertContents(t, e, "", 0, 0)
    48  	// Ensure that undoing the overwrite succeeds.
    49  	e.undo()
    50  	assertContents(t, e, "안П你 hello 안П你", 13, 0)
    51  	// Ensure that redoing the overwrite succeeds.
    52  	e.redo()
    53  	assertContents(t, e, "", 0, 0)
    54  	// Insert some smaller text.
    55  	e.Insert("안П你 hello")
    56  	assertContents(t, e, "안П你 hello", 9, 9)
    57  	// Replace a region in the middle of the text.
    58  	e.SetCaret(1, 5)
    59  	e.Insert("П")
    60  	assertContents(t, e, "안Пello", 2, 2)
    61  	// Replace a second region in the middle.
    62  	e.SetCaret(3, 4)
    63  	e.Insert("П")
    64  	assertContents(t, e, "안ПeПlo", 4, 4)
    65  	// Ensure both operations undo successfully.
    66  	e.undo()
    67  	assertContents(t, e, "안Пello", 4, 3)
    68  	e.undo()
    69  	assertContents(t, e, "안П你 hello", 5, 1)
    70  	// Make a new modification.
    71  	e.Insert("Something New")
    72  	// Ensure that redo history is discarded now that
    73  	// we've diverged from the linear editing history.
    74  	// This redo() call should do nothing.
    75  	text := e.Text()
    76  	start, end := e.Selection()
    77  	e.redo()
    78  	assertContents(t, e, text, start, end)
    79  }
    80  
    81  func assertContents(t *testing.T, e *Editor, contents string, selectionStart, selectionEnd int) {
    82  	t.Helper()
    83  	actualContents := e.Text()
    84  	if actualContents != contents {
    85  		t.Errorf("expected editor to contain %s, got %s", contents, actualContents)
    86  	}
    87  	actualStart, actualEnd := e.Selection()
    88  	if actualStart != selectionStart {
    89  		t.Errorf("expected selection start to be %d, got %d", selectionStart, actualStart)
    90  	}
    91  	if actualEnd != selectionEnd {
    92  		t.Errorf("expected selection end to be %d, got %d", selectionEnd, actualEnd)
    93  	}
    94  }
    95  
    96  // TestEditorReadOnly ensures that mouse and keyboard interactions with readonly
    97  // editors do nothing but manipulate the text selection.
    98  func TestEditorReadOnly(t *testing.T) {
    99  	r := new(input.Router)
   100  	gtx := layout.Context{
   101  		Ops: new(op.Ops),
   102  		Constraints: layout.Constraints{
   103  			Max: image.Pt(100, 100),
   104  		},
   105  		Locale: english,
   106  		Source: r.Source(),
   107  	}
   108  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
   109  	fontSize := unit.Sp(10)
   110  	font := font.Font{}
   111  	e := new(Editor)
   112  	e.ReadOnly = true
   113  	e.SetText("The quick brown fox jumps over the lazy dog. We just need a few lines of text in the editor so that it can adequately test a few different modes of selection. The quick brown fox jumps over the lazy dog. We just need a few lines of text in the editor so that it can adequately test a few different modes of selection.")
   114  	cStart, cEnd := e.Selection()
   115  	if cStart != cEnd {
   116  		t.Errorf("unexpected initial caret positions")
   117  	}
   118  	gtx.Execute(key.FocusCmd{Tag: e})
   119  	layoutEditor := func() layout.Dimensions {
   120  		return e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   121  	}
   122  	layoutEditor()
   123  	r.Frame(gtx.Ops)
   124  	gtx.Ops.Reset()
   125  	layoutEditor()
   126  	r.Frame(gtx.Ops)
   127  	gtx.Ops.Reset()
   128  	layoutEditor()
   129  	r.Frame(gtx.Ops)
   130  
   131  	// Select everything.
   132  	gtx.Ops.Reset()
   133  	r.Queue(key.Event{Name: "A", Modifiers: key.ModShortcut})
   134  	layoutEditor()
   135  	textContent := e.Text()
   136  	cStart2, cEnd2 := e.Selection()
   137  	if cStart2 > cEnd2 {
   138  		cStart2, cEnd2 = cEnd2, cStart2
   139  	}
   140  	if cEnd2 != e.Len() {
   141  		t.Errorf("expected selection to contain %d runes, got %d", e.Len(), cEnd2)
   142  	}
   143  	if cStart2 != 0 {
   144  		t.Errorf("expected selection to start at rune 0, got %d", cStart2)
   145  	}
   146  
   147  	// Type some new characters.
   148  	gtx.Ops.Reset()
   149  	r.Queue(key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"})
   150  	e.Update(gtx)
   151  	textContent2 := e.Text()
   152  	if textContent2 != textContent {
   153  		t.Errorf("readonly editor modified by key.EditEvent")
   154  	}
   155  
   156  	// Try to delete selection.
   157  	gtx.Ops.Reset()
   158  	r.Queue(key.Event{Name: key.NameDeleteBackward})
   159  	dims := layoutEditor()
   160  	textContent2 = e.Text()
   161  	if textContent2 != textContent {
   162  		t.Errorf("readonly editor modified by delete key.Event")
   163  	}
   164  
   165  	// Click and drag from the middle of the first line
   166  	// to the center.
   167  	gtx.Ops.Reset()
   168  	r.Queue(
   169  		pointer.Event{
   170  			Kind:     pointer.Press,
   171  			Buttons:  pointer.ButtonPrimary,
   172  			Position: f32.Pt(float32(dims.Size.X)*.5, 5),
   173  		},
   174  		pointer.Event{
   175  			Kind:     pointer.Move,
   176  			Buttons:  pointer.ButtonPrimary,
   177  			Position: layout.FPt(dims.Size).Mul(.5),
   178  		},
   179  		pointer.Event{
   180  			Kind:     pointer.Release,
   181  			Buttons:  pointer.ButtonPrimary,
   182  			Position: layout.FPt(dims.Size).Mul(.5),
   183  		},
   184  	)
   185  	e.Update(gtx)
   186  	cStart3, cEnd3 := e.Selection()
   187  	if cStart3 == cStart2 || cEnd3 == cEnd2 {
   188  		t.Errorf("expected mouse interaction to change selection.")
   189  	}
   190  }
   191  
   192  func TestEditorConfigurations(t *testing.T) {
   193  	gtx := layout.Context{
   194  		Ops:         new(op.Ops),
   195  		Constraints: layout.Exact(image.Pt(300, 300)),
   196  		Locale:      english,
   197  	}
   198  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
   199  	fontSize := unit.Sp(10)
   200  	font := font.Font{}
   201  	sentence := "\n\n\n\n\n\n\n\n\n\n\n\nthe quick brown fox jumps over the lazy dog"
   202  	runes := len([]rune(sentence))
   203  
   204  	// Ensure that both ends of the text are reachable in all permutations
   205  	// of settings that influence layout.
   206  	for _, singleLine := range []bool{true, false} {
   207  		for _, alignment := range []text.Alignment{text.Start, text.Middle, text.End} {
   208  			for _, zeroMin := range []bool{true, false} {
   209  				t.Run(fmt.Sprintf("SingleLine: %v Alignment: %v ZeroMinConstraint: %v", singleLine, alignment, zeroMin), func(t *testing.T) {
   210  					defer func() {
   211  						if err := recover(); err != nil {
   212  							t.Error(err)
   213  						}
   214  					}()
   215  					if zeroMin {
   216  						gtx.Constraints.Min = image.Point{}
   217  					} else {
   218  						gtx.Constraints.Min = gtx.Constraints.Max
   219  					}
   220  					e := new(Editor)
   221  					e.SingleLine = singleLine
   222  					e.Alignment = alignment
   223  					e.SetText(sentence)
   224  					e.SetCaret(0, 0)
   225  					dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   226  					if dims.Size.X < gtx.Constraints.Min.X || dims.Size.Y < gtx.Constraints.Min.Y {
   227  						t.Errorf("expected min size %#+v, got %#+v", gtx.Constraints.Min, dims.Size)
   228  					}
   229  					coords := e.CaretCoords()
   230  					if halfway := float32(gtx.Constraints.Min.X) * .5; !singleLine && alignment == text.Middle && !zeroMin && coords.X != halfway {
   231  						t.Errorf("expected caret X to be %f, got %f", halfway, coords.X)
   232  					}
   233  					e.SetCaret(runes, runes)
   234  					e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   235  					coords = e.CaretCoords()
   236  					if int(coords.X) > gtx.Constraints.Max.X || int(coords.Y) > gtx.Constraints.Max.Y {
   237  						t.Errorf("caret coordinates %v exceed constraints %v", coords, gtx.Constraints.Max)
   238  					}
   239  				})
   240  			}
   241  		}
   242  	}
   243  }
   244  
   245  func TestEditor(t *testing.T) {
   246  	e := new(Editor)
   247  	gtx := layout.Context{
   248  		Ops:         new(op.Ops),
   249  		Constraints: layout.Exact(image.Pt(100, 100)),
   250  		Locale:      english,
   251  	}
   252  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
   253  	fontSize := unit.Sp(10)
   254  	font := font.Font{}
   255  
   256  	// Regression test for bad in-cluster rune offset math.
   257  	e.SetText("æbc")
   258  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   259  	e.text.MoveLineEnd(selectionClear)
   260  	assertCaret(t, e, 0, 3, len("æbc"))
   261  
   262  	textSample := "æbc\naøå••"
   263  	e.SetCaret(0, 0) // shouldn't panic
   264  	assertCaret(t, e, 0, 0, 0)
   265  	e.SetText(textSample)
   266  	if got, exp := e.Len(), utf8.RuneCountInString(e.Text()); got != exp {
   267  		t.Errorf("got length %d, expected %d", got, exp)
   268  	}
   269  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   270  	assertCaret(t, e, 0, 0, 0)
   271  	e.text.MoveLineEnd(selectionClear)
   272  	assertCaret(t, e, 0, 3, len("æbc"))
   273  	e.MoveCaret(+1, +1)
   274  	assertCaret(t, e, 1, 0, len("æbc\n"))
   275  	e.MoveCaret(-1, -1)
   276  	assertCaret(t, e, 0, 3, len("æbc"))
   277  	e.text.MoveLines(+1, selectionClear)
   278  	assertCaret(t, e, 1, 4, len("æbc\naøå•"))
   279  	e.text.MoveLineEnd(selectionClear)
   280  	assertCaret(t, e, 1, 5, len("æbc\naøå••"))
   281  	e.MoveCaret(+1, +1)
   282  	assertCaret(t, e, 1, 5, len("æbc\naøå••"))
   283  	e.text.MoveLines(3, selectionClear)
   284  
   285  	e.SetCaret(0, 0)
   286  	assertCaret(t, e, 0, 0, 0)
   287  	e.SetCaret(utf8.RuneCountInString("æ"), utf8.RuneCountInString("æ"))
   288  	assertCaret(t, e, 0, 1, 2)
   289  	e.SetCaret(utf8.RuneCountInString("æbc\naøå•"), utf8.RuneCountInString("æbc\naøå•"))
   290  	assertCaret(t, e, 1, 4, len("æbc\naøå•"))
   291  
   292  	// Ensure that password masking does not affect caret behavior
   293  	e.MoveCaret(-3, -3)
   294  	assertCaret(t, e, 1, 1, len("æbc\na"))
   295  	e.text.Mask = '*'
   296  	e.Update(gtx)
   297  	assertCaret(t, e, 1, 1, len("æbc\na"))
   298  	e.MoveCaret(-3, -3)
   299  	assertCaret(t, e, 0, 2, len("æb"))
   300  	// Test that moveLine applies x offsets from previous moves.
   301  	e.SetText("long line\nshort")
   302  	e.SetCaret(0, 0)
   303  	e.text.MoveLineEnd(selectionClear)
   304  	e.text.MoveLines(+1, selectionClear)
   305  	e.text.MoveLines(-1, selectionClear)
   306  	assertCaret(t, e, 0, utf8.RuneCountInString("long line"), len("long line"))
   307  }
   308  
   309  var arabic = system.Locale{
   310  	Language:  "AR",
   311  	Direction: system.RTL,
   312  }
   313  
   314  var arabicCollection = func() []font.FontFace {
   315  	parsed, _ := opentype.Parse(nsareg.TTF)
   316  	return []font.FontFace{{Font: font.Font{}, Face: parsed}}
   317  }()
   318  
   319  func TestEditorRTL(t *testing.T) {
   320  	e := new(Editor)
   321  	gtx := layout.Context{
   322  		Ops:         new(op.Ops),
   323  		Constraints: layout.Exact(image.Pt(100, 100)),
   324  		Locale:      arabic,
   325  	}
   326  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(arabicCollection))
   327  	fontSize := unit.Sp(10)
   328  	font := font.Font{}
   329  
   330  	e.SetCaret(0, 0) // shouldn't panic
   331  	assertCaret(t, e, 0, 0, 0)
   332  
   333  	// Set the text to a single RTL word. The caret should start at 0 column
   334  	// zero, but this is the first column on the right.
   335  	e.SetText("الحب")
   336  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   337  	assertCaret(t, e, 0, 0, 0)
   338  	e.MoveCaret(+1, +1)
   339  	assertCaret(t, e, 0, 1, len("ا"))
   340  	e.MoveCaret(+1, +1)
   341  	assertCaret(t, e, 0, 2, len("ال"))
   342  	e.MoveCaret(+1, +1)
   343  	assertCaret(t, e, 0, 3, len("الح"))
   344  	// Move to the "end" of the line. This moves to the left edge of the line.
   345  	e.text.MoveLineEnd(selectionClear)
   346  	assertCaret(t, e, 0, 4, len("الحب"))
   347  
   348  	sentence := "الحب سماء لا\nتمط غير الأحلام"
   349  	e.SetText(sentence)
   350  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   351  	assertCaret(t, e, 0, 0, 0)
   352  	e.text.MoveLineEnd(selectionClear)
   353  	assertCaret(t, e, 0, 12, len("الحب سماء لا"))
   354  	e.MoveCaret(+1, +1)
   355  	assertCaret(t, e, 1, 0, len("الحب سماء لا\n"))
   356  	e.MoveCaret(+1, +1)
   357  	assertCaret(t, e, 1, 1, len("الحب سماء لا\nت"))
   358  	e.MoveCaret(-1, -1)
   359  	assertCaret(t, e, 1, 0, len("الحب سماء لا\n"))
   360  	e.MoveCaret(-1, -1)
   361  	assertCaret(t, e, 0, 12, len("الحب سماء لا"))
   362  	e.text.MoveLines(+1, selectionClear)
   363  	assertCaret(t, e, 1, 14, len("الحب سماء لا\nتمط غير الأحلا"))
   364  	e.text.MoveLineEnd(selectionClear)
   365  	assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
   366  	e.MoveCaret(+1, +1)
   367  	assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
   368  	e.text.MoveLines(3, selectionClear)
   369  	assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
   370  	e.SetCaret(utf8.RuneCountInString(sentence), 0)
   371  	assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
   372  	if selection := e.SelectedText(); selection != sentence {
   373  		t.Errorf("expected selection %s, got %s", sentence, selection)
   374  	}
   375  
   376  	e.SetCaret(0, 0)
   377  	assertCaret(t, e, 0, 0, 0)
   378  	e.SetCaret(utf8.RuneCountInString("ا"), utf8.RuneCountInString("ا"))
   379  	assertCaret(t, e, 0, 1, len("ا"))
   380  	e.SetCaret(utf8.RuneCountInString("الحب سماء لا\nتمط غ"), utf8.RuneCountInString("الحب سماء لا\nتمط غ"))
   381  	assertCaret(t, e, 1, 5, len("الحب سماء لا\nتمط غ"))
   382  }
   383  
   384  func TestEditorLigature(t *testing.T) {
   385  	e := new(Editor)
   386  	e.WrapPolicy = text.WrapWords
   387  	gtx := layout.Context{
   388  		Ops:         new(op.Ops),
   389  		Constraints: layout.Exact(image.Pt(100, 100)),
   390  		Locale:      english,
   391  	}
   392  	face, err := opentype.Parse(robotoregular.TTF)
   393  	if err != nil {
   394  		t.Skipf("failed parsing test font: %v", err)
   395  	}
   396  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{
   397  		{
   398  			Font: font.Font{
   399  				Typeface: "Roboto",
   400  			},
   401  			Face: face,
   402  		},
   403  	}))
   404  	fontSize := unit.Sp(10)
   405  	font := font.Font{}
   406  
   407  	/*
   408  		In this font, the following rune sequences form ligatures:
   409  
   410  		- ffi
   411  		- ffl
   412  		- fi
   413  		- fl
   414  	*/
   415  
   416  	e.SetCaret(0, 0) // shouldn't panic
   417  	assertCaret(t, e, 0, 0, 0)
   418  	e.SetText("fl") // just a ligature
   419  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   420  	e.text.MoveLineEnd(selectionClear)
   421  	assertCaret(t, e, 0, 2, len("fl"))
   422  	e.MoveCaret(-1, -1)
   423  	assertCaret(t, e, 0, 1, len("f"))
   424  	e.MoveCaret(-1, -1)
   425  	assertCaret(t, e, 0, 0, 0)
   426  	e.MoveCaret(+2, +2)
   427  	assertCaret(t, e, 0, 2, len("fl"))
   428  	e.SetText("flaffl•ffi\n•fflfi") // 3 ligatures on line 0, 2 on line 1
   429  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   430  	assertCaret(t, e, 0, 0, 0)
   431  	e.text.MoveLineEnd(selectionClear)
   432  	assertCaret(t, e, 0, 10, len("ffaffl•ffi"))
   433  	e.MoveCaret(+1, +1)
   434  	assertCaret(t, e, 1, 0, len("ffaffl•ffi\n"))
   435  	e.MoveCaret(+1, +1)
   436  	assertCaret(t, e, 1, 1, len("ffaffl•ffi\n•"))
   437  	e.MoveCaret(+1, +1)
   438  	assertCaret(t, e, 1, 2, len("ffaffl•ffi\n•f"))
   439  	e.MoveCaret(+1, +1)
   440  	assertCaret(t, e, 1, 3, len("ffaffl•ffi\n•ff"))
   441  	e.MoveCaret(+1, +1)
   442  	assertCaret(t, e, 1, 4, len("ffaffl•ffi\n•ffl"))
   443  	e.MoveCaret(+1, +1)
   444  	assertCaret(t, e, 1, 5, len("ffaffl•ffi\n•fflf"))
   445  	e.MoveCaret(+1, +1)
   446  	assertCaret(t, e, 1, 6, len("ffaffl•ffi\n•fflfi"))
   447  	e.MoveCaret(-1, -1)
   448  	assertCaret(t, e, 1, 5, len("ffaffl•ffi\n•fflf"))
   449  	e.MoveCaret(-1, -1)
   450  	assertCaret(t, e, 1, 4, len("ffaffl•ffi\n•ffl"))
   451  	e.MoveCaret(-1, -1)
   452  	assertCaret(t, e, 1, 3, len("ffaffl•ffi\n•ff"))
   453  	e.MoveCaret(-1, -1)
   454  	assertCaret(t, e, 1, 2, len("ffaffl•ffi\n•f"))
   455  	e.MoveCaret(-1, -1)
   456  	assertCaret(t, e, 1, 1, len("ffaffl•ffi\n•"))
   457  	e.MoveCaret(-1, -1)
   458  	assertCaret(t, e, 1, 0, len("ffaffl•ffi\n"))
   459  	e.MoveCaret(-1, -1)
   460  	assertCaret(t, e, 0, 10, len("ffaffl•ffi"))
   461  	e.MoveCaret(-2, -2)
   462  	assertCaret(t, e, 0, 8, len("ffaffl•f"))
   463  	e.MoveCaret(-1, -1)
   464  	assertCaret(t, e, 0, 7, len("ffaffl•"))
   465  	e.MoveCaret(-1, -1)
   466  	assertCaret(t, e, 0, 6, len("ffaffl"))
   467  	e.MoveCaret(-1, -1)
   468  	assertCaret(t, e, 0, 5, len("ffaff"))
   469  	e.MoveCaret(-1, -1)
   470  	assertCaret(t, e, 0, 4, len("ffaf"))
   471  	e.MoveCaret(-1, -1)
   472  	assertCaret(t, e, 0, 3, len("ffa"))
   473  	e.MoveCaret(-1, -1)
   474  	assertCaret(t, e, 0, 2, len("ff"))
   475  	e.MoveCaret(-1, -1)
   476  	assertCaret(t, e, 0, 1, len("f"))
   477  	e.MoveCaret(-1, -1)
   478  	assertCaret(t, e, 0, 0, 0)
   479  	gtx.Constraints = layout.Exact(image.Pt(50, 50))
   480  	e.SetText("fflffl fflffl fflffl fflffl") // Many ligatures broken across lines.
   481  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   482  	// Ensure that all runes in the final cluster of a line are properly
   483  	// decoded when moving to the end of the line. This is a regression test.
   484  	e.text.MoveLineEnd(selectionClear)
   485  	// The first line was broken by line wrapping, not a newline character, and has a trailing
   486  	// whitespace. However, we should never be able to reach the "other side" of such a trailing
   487  	// whitespace glyph.
   488  	assertCaret(t, e, 0, 13, len("fflffl fflffl"))
   489  	e.text.MoveLines(1, selectionClear)
   490  	assertCaret(t, e, 1, 13, len("fflffl fflffl fflffl fflffl"))
   491  	e.text.MoveLines(-1, selectionClear)
   492  	assertCaret(t, e, 0, 13, len("fflffl fflffl"))
   493  
   494  	// Absurdly narrow constraints to force each ligature onto its own line.
   495  	gtx.Constraints = layout.Exact(image.Pt(10, 10))
   496  	e.SetText("ffl ffl") // Two ligatures on separate lines.
   497  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   498  	assertCaret(t, e, 0, 0, 0)
   499  	e.MoveCaret(1, 1) // Move the caret into the first ligature.
   500  	assertCaret(t, e, 0, 1, len("f"))
   501  	e.MoveCaret(4, 4) // Move the caret several positions.
   502  	assertCaret(t, e, 1, 1, len("ffl f"))
   503  }
   504  
   505  func TestEditorDimensions(t *testing.T) {
   506  	e := new(Editor)
   507  	r := new(input.Router)
   508  	gtx := layout.Context{
   509  		Ops:         new(op.Ops),
   510  		Constraints: layout.Constraints{Max: image.Pt(100, 100)},
   511  		Source:      r.Source(),
   512  		Locale:      english,
   513  	}
   514  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
   515  	fontSize := unit.Sp(10)
   516  	font := font.Font{}
   517  	gtx.Execute(key.FocusCmd{Tag: e})
   518  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   519  	r.Frame(gtx.Ops)
   520  	r.Queue(key.EditEvent{Text: "A"})
   521  	dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   522  	if dims.Size.X < 5 {
   523  		t.Errorf("EditEvent was not reflected in Editor width")
   524  	}
   525  }
   526  
   527  // assertCaret asserts that the editor caret is at a particular line
   528  // and column, and that the byte position matches as well.
   529  func assertCaret(t *testing.T, e *Editor, line, col, bytes int) {
   530  	t.Helper()
   531  	gotLine, gotCol := e.CaretPos()
   532  	if gotLine != line || gotCol != col {
   533  		t.Errorf("caret at (%d, %d), expected (%d, %d)", gotLine, gotCol, line, col)
   534  	}
   535  	caretBytes := e.text.runeOffset(e.text.caret.start)
   536  	if bytes != caretBytes {
   537  		t.Errorf("caret at buffer position %d, expected %d", caretBytes, bytes)
   538  	}
   539  	// Ensure that SelectedText() does not panic no matter what the
   540  	// editor's state is.
   541  	_ = e.SelectedText()
   542  }
   543  
   544  type editMutation int
   545  
   546  const (
   547  	setText editMutation = iota
   548  	moveRune
   549  	moveLine
   550  	movePage
   551  	moveTextStart
   552  	moveTextEnd
   553  	moveLineStart
   554  	moveLineEnd
   555  	moveCoord
   556  	moveWord
   557  	deleteWord
   558  	moveLast // Mark end; never generated.
   559  )
   560  
   561  func TestEditorCaretConsistency(t *testing.T) {
   562  	gtx := layout.Context{
   563  		Ops:         new(op.Ops),
   564  		Constraints: layout.Exact(image.Pt(100, 100)),
   565  		Locale:      english,
   566  	}
   567  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
   568  	fontSize := unit.Sp(10)
   569  	font := font.Font{}
   570  	for _, a := range []text.Alignment{text.Start, text.Middle, text.End} {
   571  		e := &Editor{}
   572  		e.Alignment = a
   573  		e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   574  
   575  		consistent := func() error {
   576  			t.Helper()
   577  			gotLine, gotCol := e.CaretPos()
   578  			gotCoords := e.CaretCoords()
   579  			// Blow away index to re-compute position from scratch.
   580  			e.text.invalidate()
   581  			want := e.text.closestToRune(e.text.caret.start)
   582  			wantCoords := f32.Pt(float32(want.x)/64, float32(want.y))
   583  			if want.lineCol.line != gotLine || int(want.lineCol.col) != gotCol || gotCoords != wantCoords {
   584  				return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s",
   585  					gotLine, gotCol, gotCoords, want.lineCol.line, want.lineCol.col, wantCoords)
   586  			}
   587  			return nil
   588  		}
   589  		if err := consistent(); err != nil {
   590  			t.Errorf("initial editor inconsistency (alignment %s): %v", a, err)
   591  		}
   592  
   593  		move := func(mutation editMutation, str string, distance int8, x, y uint16) bool {
   594  			switch mutation {
   595  			case setText:
   596  				e.SetText(str)
   597  				e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   598  			case moveRune:
   599  				e.MoveCaret(int(distance), int(distance))
   600  			case moveLine:
   601  				e.text.MoveLines(int(distance), selectionClear)
   602  			case movePage:
   603  				e.text.MovePages(int(distance), selectionClear)
   604  			case moveLineStart:
   605  				e.text.MoveLineStart(selectionClear)
   606  			case moveLineEnd:
   607  				e.text.MoveLineEnd(selectionClear)
   608  			case moveTextStart:
   609  				e.text.MoveTextStart(selectionClear)
   610  			case moveTextEnd:
   611  				e.text.MoveTextEnd(selectionClear)
   612  			case moveCoord:
   613  				e.text.MoveCoord(image.Pt(int(x), int(y)))
   614  			case moveWord:
   615  				e.text.MoveWord(int(distance), selectionClear)
   616  			case deleteWord:
   617  				e.deleteWord(int(distance))
   618  			default:
   619  				return false
   620  			}
   621  			if err := consistent(); err != nil {
   622  				t.Error(err)
   623  				return false
   624  			}
   625  			return true
   626  		}
   627  		if err := quick.Check(move, nil); err != nil {
   628  			t.Errorf("editor inconsistency (alignment %s): %v", a, err)
   629  		}
   630  	}
   631  }
   632  
   633  func TestEditorMoveWord(t *testing.T) {
   634  	type Test struct {
   635  		Text  string
   636  		Start int
   637  		Skip  int
   638  		Want  int
   639  	}
   640  	tests := []Test{
   641  		{"", 0, 0, 0},
   642  		{"", 0, -1, 0},
   643  		{"", 0, 1, 0},
   644  		{"hello", 0, -1, 0},
   645  		{"hello", 0, 1, 5},
   646  		{"hello world", 3, 1, 5},
   647  		{"hello world", 3, -1, 0},
   648  		{"hello world", 8, -1, 6},
   649  		{"hello world", 8, 1, 11},
   650  		{"hello    world", 3, 1, 5},
   651  		{"hello    world", 3, 2, 14},
   652  		{"hello    world", 8, 1, 14},
   653  		{"hello    world", 8, -1, 0},
   654  		{"hello brave new world", 0, 3, 15},
   655  	}
   656  	setup := func(t string) *Editor {
   657  		e := new(Editor)
   658  		gtx := layout.Context{
   659  			Ops:         new(op.Ops),
   660  			Constraints: layout.Exact(image.Pt(100, 100)),
   661  			Locale:      english,
   662  		}
   663  		e.SetText(t)
   664  		e.Update(gtx)
   665  		return e
   666  	}
   667  	for ii, tt := range tests {
   668  		e := setup(tt.Text)
   669  		e.MoveCaret(tt.Start, tt.Start)
   670  		e.text.MoveWord(tt.Skip, selectionClear)
   671  		caretBytes := e.text.runeOffset(e.text.caret.start)
   672  		if caretBytes != tt.Want {
   673  			t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, caretBytes, tt.Want)
   674  		}
   675  	}
   676  }
   677  
   678  func TestEditorInsert(t *testing.T) {
   679  	type Test struct {
   680  		Text      string
   681  		Start     int
   682  		Selection int
   683  		Insertion string
   684  
   685  		Result string
   686  	}
   687  	tests := []Test{
   688  		// Nothing inserted
   689  		{"", 0, 0, "", ""},
   690  		{"", 0, -1, "", ""},
   691  		{"", 0, 1, "", ""},
   692  		{"", 0, -2, "", ""},
   693  		{"", 0, 2, "", ""},
   694  		{"world", 0, 0, "", "world"},
   695  		{"world", 0, -1, "", "world"},
   696  		{"world", 0, 1, "", "orld"},
   697  		{"world", 2, 0, "", "world"},
   698  		{"world", 2, -1, "", "wrld"},
   699  		{"world", 2, 1, "", "wold"},
   700  		{"world", 5, 0, "", "world"},
   701  		{"world", 5, -1, "", "worl"},
   702  		{"world", 5, 1, "", "world"},
   703  		// One rune inserted
   704  		{"", 0, 0, "_", "_"},
   705  		{"", 0, -1, "_", "_"},
   706  		{"", 0, 1, "_", "_"},
   707  		{"", 0, -2, "_", "_"},
   708  		{"", 0, 2, "_", "_"},
   709  		{"world", 0, 0, "_", "_world"},
   710  		{"world", 0, -1, "_", "_world"},
   711  		{"world", 0, 1, "_", "_orld"},
   712  		{"world", 2, 0, "_", "wo_rld"},
   713  		{"world", 2, -1, "_", "w_rld"},
   714  		{"world", 2, 1, "_", "wo_ld"},
   715  		{"world", 5, 0, "_", "world_"},
   716  		{"world", 5, -1, "_", "worl_"},
   717  		{"world", 5, 1, "_", "world_"},
   718  		// More runes inserted
   719  		{"", 0, 0, "-3-", "-3-"},
   720  		{"", 0, -1, "-3-", "-3-"},
   721  		{"", 0, 1, "-3-", "-3-"},
   722  		{"", 0, -2, "-3-", "-3-"},
   723  		{"", 0, 2, "-3-", "-3-"},
   724  		{"world", 0, 0, "-3-", "-3-world"},
   725  		{"world", 0, -1, "-3-", "-3-world"},
   726  		{"world", 0, 1, "-3-", "-3-orld"},
   727  		{"world", 2, 0, "-3-", "wo-3-rld"},
   728  		{"world", 2, -1, "-3-", "w-3-rld"},
   729  		{"world", 2, 1, "-3-", "wo-3-ld"},
   730  		{"world", 5, 0, "-3-", "world-3-"},
   731  		{"world", 5, -1, "-3-", "worl-3-"},
   732  		{"world", 5, 1, "-3-", "world-3-"},
   733  		// Runes with length > 1 inserted
   734  		{"", 0, 0, "éêè", "éêè"},
   735  		{"", 0, -1, "éêè", "éêè"},
   736  		{"", 0, 1, "éêè", "éêè"},
   737  		{"", 0, -2, "éêè", "éêè"},
   738  		{"", 0, 2, "éêè", "éêè"},
   739  		{"world", 0, 0, "éêè", "éêèworld"},
   740  		{"world", 0, -1, "éêè", "éêèworld"},
   741  		{"world", 0, 1, "éêè", "éêèorld"},
   742  		{"world", 2, 0, "éêè", "woéêèrld"},
   743  		{"world", 2, -1, "éêè", "wéêèrld"},
   744  		{"world", 2, 1, "éêè", "woéêèld"},
   745  		{"world", 5, 0, "éêè", "worldéêè"},
   746  		{"world", 5, -1, "éêè", "worléêè"},
   747  		{"world", 5, 1, "éêè", "worldéêè"},
   748  		// Runes with length > 1 deleted from selection
   749  		{"élançé", 0, 1, "", "lançé"},
   750  		{"élançé", 0, 1, "-3-", "-3-lançé"},
   751  		{"élançé", 3, 2, "-3-", "éla-3-é"},
   752  		{"élançé", 3, 3, "-3-", "éla-3-"},
   753  		{"élançé", 3, 10, "-3-", "éla-3-"},
   754  		{"élançé", 5, -1, "-3-", "élan-3-é"},
   755  		{"élançé", 6, -1, "-3-", "élanç-3-"},
   756  		{"élançé", 6, -3, "-3-", "éla-3-"},
   757  	}
   758  	setup := func(t string) *Editor {
   759  		e := new(Editor)
   760  		gtx := layout.Context{
   761  			Ops:         new(op.Ops),
   762  			Constraints: layout.Exact(image.Pt(100, 100)),
   763  			Locale:      english,
   764  		}
   765  		e.SetText(t)
   766  		e.Update(gtx)
   767  		return e
   768  	}
   769  	for ii, tt := range tests {
   770  		e := setup(tt.Text)
   771  		e.MoveCaret(tt.Start, tt.Start)
   772  		e.MoveCaret(0, tt.Selection)
   773  		e.Insert(tt.Insertion)
   774  		if e.Text() != tt.Result {
   775  			t.Fatalf("[%d] Insert: invalid result: got %q, want %q", ii, e.Text(), tt.Result)
   776  		}
   777  	}
   778  }
   779  
   780  func TestEditorDeleteWord(t *testing.T) {
   781  	type Test struct {
   782  		Text      string
   783  		Start     int
   784  		Selection int
   785  		Delete    int
   786  
   787  		Want   int
   788  		Result string
   789  	}
   790  	tests := []Test{
   791  		// No text selected
   792  		{"", 0, 0, 0, 0, ""},
   793  		{"", 0, 0, -1, 0, ""},
   794  		{"", 0, 0, 1, 0, ""},
   795  		{"", 0, 0, -2, 0, ""},
   796  		{"", 0, 0, 2, 0, ""},
   797  		{"hello", 0, 0, -1, 0, "hello"},
   798  		{"hello", 0, 0, 1, 0, ""},
   799  
   800  		// Document (imho) incorrect behavior w.r.t. deleting spaces following
   801  		// words.
   802  		{"hello world", 0, 0, 1, 0, " world"},   // Should be "world", if you ask me.
   803  		{"hello world", 0, 0, 2, 0, "world"},    // Should be "".
   804  		{"hello ", 0, 0, 1, 0, " "},             // Should be "".
   805  		{"hello world", 11, 0, -1, 6, "hello "}, // Should be "hello".
   806  		{"hello world", 11, 0, -2, 5, "hello"},  // Should be "".
   807  		{"hello ", 6, 0, -1, 0, ""},             // Correct result.
   808  
   809  		{"hello world", 3, 0, 1, 3, "hel world"},
   810  		{"hello world", 3, 0, -1, 0, "lo world"},
   811  		{"hello world", 8, 0, -1, 6, "hello rld"},
   812  		{"hello world", 8, 0, 1, 8, "hello wo"},
   813  		{"hello    world", 3, 0, 1, 3, "hel    world"},
   814  		{"hello    world", 3, 0, 2, 3, "helworld"},
   815  		{"hello    world", 8, 0, 1, 8, "hello   "},
   816  		{"hello    world", 8, 0, -1, 5, "hello world"},
   817  		{"hello brave new world", 0, 0, 3, 0, " new world"},
   818  		{"helléèçàô world", 3, 0, 1, 3, "hel world"}, // unicode char with length > 1 in deleted part
   819  		// Add selected text.
   820  		//
   821  		// Several permutations must be tested:
   822  		// - select from the left or right
   823  		// - Delete + or -
   824  		// - abs(Delete) == 1 or > 1
   825  		//
   826  		// "brave |" selected; caret at |
   827  		{"hello there brave new world", 12, 6, 1, 12, "hello there new world"}, // #16
   828  		{"hello there brave new world", 12, 6, 2, 12, "hello there  world"},    // The two spaces after "there" are actually suboptimal, if you ask me. See also above cases.
   829  		{"hello there brave new world", 12, 6, -1, 12, "hello there new world"},
   830  		{"hello there brave new world", 12, 6, -2, 6, "hello new world"},
   831  		{"hello there b®âve new world", 12, 6, 1, 12, "hello there new world"},  // unicode chars with length > 1 in selection
   832  		{"hello there b®âve new world", 12, 6, 2, 12, "hello there  world"},     // ditto
   833  		{"hello there b®âve new world", 12, 6, -1, 12, "hello there new world"}, // ditto
   834  		{"hello there b®âve new world", 12, 6, -2, 6, "hello new world"},        // ditto
   835  		// "|brave " selected
   836  		{"hello there brave new world", 18, -6, 1, 12, "hello there new world"}, // #20
   837  		{"hello there brave new world", 18, -6, 2, 12, "hello there  world"},    // ditto
   838  		{"hello there brave new world", 18, -6, -1, 12, "hello there new world"},
   839  		{"hello there brave new world", 18, -6, -2, 6, "hello new world"},
   840  		{"hello there b®âve new world", 18, -6, 1, 12, "hello there new world"}, // unicode chars with length > 1 in selection
   841  		// Random edge cases
   842  		{"hello there brave new world", 12, 6, 99, 12, "hello there "},
   843  		{"hello there brave new world", 18, -6, -99, 0, "new world"},
   844  	}
   845  	setup := func(t string) *Editor {
   846  		e := new(Editor)
   847  		gtx := layout.Context{
   848  			Ops:         new(op.Ops),
   849  			Constraints: layout.Exact(image.Pt(100, 100)),
   850  			Locale:      english,
   851  		}
   852  		e.SetText(t)
   853  		e.Update(gtx)
   854  		return e
   855  	}
   856  	for ii, tt := range tests {
   857  		e := setup(tt.Text)
   858  		e.MoveCaret(tt.Start, tt.Start)
   859  		e.MoveCaret(0, tt.Selection)
   860  		e.deleteWord(tt.Delete)
   861  		caretBytes := e.text.runeOffset(e.text.caret.start)
   862  		if caretBytes != tt.Want {
   863  			t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, caretBytes, tt.Want)
   864  		}
   865  		if e.Text() != tt.Result {
   866  			t.Fatalf("[%d] deleteWord: invalid result: got %q, want %q", ii, e.Text(), tt.Result)
   867  		}
   868  	}
   869  }
   870  
   871  func TestEditorNoLayout(t *testing.T) {
   872  	var e Editor
   873  	e.SetText("hi!\n")
   874  	e.MoveCaret(1, 1)
   875  }
   876  
   877  // Generate generates a value of itself, for testing/quick.
   878  func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value {
   879  	t := editMutation(rand.Intn(int(moveLast)))
   880  	return reflect.ValueOf(t)
   881  }
   882  
   883  // TestEditorSelect tests the selection code. It lays out an editor with several
   884  // lines in it, selects some text, verifies the selection, resizes the editor
   885  // to make it much narrower (which makes the lines in the editor reflow), and
   886  // then verifies that the updated (col, line) positions of the selected text
   887  // are where we expect.
   888  func TestEditorSelectReflow(t *testing.T) {
   889  	e := new(Editor)
   890  	e.SetText(`a 2 4 6 8 a
   891  b 2 4 6 8 b
   892  c 2 4 6 8 c
   893  d 2 4 6 8 d
   894  e 2 4 6 8 e
   895  f 2 4 6 8 f
   896  g 2 4 6 8 g
   897  `)
   898  
   899  	r := new(input.Router)
   900  	gtx := layout.Context{
   901  		Ops:    new(op.Ops),
   902  		Locale: english,
   903  		Source: r.Source(),
   904  	}
   905  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
   906  	font := font.Font{}
   907  	fontSize := unit.Sp(10)
   908  
   909  	var tim time.Duration
   910  	selected := func(start, end int) string {
   911  		gtx.Execute(key.FocusCmd{Tag: e})
   912  		// Layout once with no events; populate e.lines.
   913  		e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   914  
   915  		r.Frame(gtx.Ops)
   916  		gtx.Source = r.Source()
   917  		// Build the selection events
   918  		startPos := e.text.closestToRune(start)
   919  		endPos := e.text.closestToRune(end)
   920  		r.Queue(
   921  			pointer.Event{
   922  				Buttons:  pointer.ButtonPrimary,
   923  				Kind:     pointer.Press,
   924  				Source:   pointer.Mouse,
   925  				Time:     tim,
   926  				Position: f32.Pt(textWidth(e, startPos.lineCol.line, 0, startPos.lineCol.col), textBaseline(e, startPos.lineCol.line)),
   927  			},
   928  			pointer.Event{
   929  				Kind:     pointer.Release,
   930  				Source:   pointer.Mouse,
   931  				Time:     tim,
   932  				Position: f32.Pt(textWidth(e, endPos.lineCol.line, 0, endPos.lineCol.col), textBaseline(e, endPos.lineCol.line)),
   933  			},
   934  		)
   935  		tim += time.Second // Avoid multi-clicks.
   936  
   937  		for {
   938  			_, ok := e.Update(gtx) // throw away any events from this layout
   939  			if !ok {
   940  				break
   941  			}
   942  		}
   943  		return e.SelectedText()
   944  	}
   945  	type screenPos image.Point
   946  	logicalPosMatch := func(t *testing.T, n int, label string, expected screenPos, actual combinedPos) {
   947  		t.Helper()
   948  		if actual.lineCol.line != expected.Y || actual.lineCol.col != expected.X {
   949  			t.Errorf("Test %d: Expected %s %#v; got %#v",
   950  				n, label,
   951  				expected, actual)
   952  		}
   953  	}
   954  
   955  	type testCase struct {
   956  		// input text offsets
   957  		start, end int
   958  
   959  		// expected selected text
   960  		selection string
   961  		// expected line/col positions of selection after resize
   962  		startPos, endPos screenPos
   963  	}
   964  
   965  	for n, tst := range []testCase{
   966  		{0, 1, "a", screenPos{}, screenPos{Y: 0, X: 1}},
   967  		{0, 4, "a 2 ", screenPos{}, screenPos{Y: 0, X: 4}},
   968  		{0, 11, "a 2 4 6 8 a", screenPos{}, screenPos{Y: 1, X: 3}},
   969  		{6, 10, "6 8 ", screenPos{Y: 0, X: 6}, screenPos{Y: 1, X: 2}},
   970  		{41, 66, " 6 8 d\ne 2 4 6 8 e\nf 2 4 ", screenPos{Y: 6, X: 5}, screenPos{Y: 10, X: 6}},
   971  	} {
   972  		gtx.Constraints = layout.Exact(image.Pt(100, 100))
   973  		if got := selected(tst.start, tst.end); got != tst.selection {
   974  			t.Errorf("Test %d pt1: Expected %q, got %q", n, tst.selection, got)
   975  			continue
   976  		}
   977  
   978  		// Constrain the editor to roughly 6 columns wide and redraw
   979  		gtx.Constraints = layout.Exact(image.Pt(36, 36))
   980  		// Keep existing selection
   981  		gtx = gtx.Disabled()
   982  		e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   983  
   984  		caretStart := e.text.closestToRune(e.text.caret.start)
   985  		caretEnd := e.text.closestToRune(e.text.caret.end)
   986  		logicalPosMatch(t, n, "start", tst.startPos, caretEnd)
   987  		logicalPosMatch(t, n, "end", tst.endPos, caretStart)
   988  	}
   989  }
   990  
   991  func TestEditorSelectShortcuts(t *testing.T) {
   992  	tFont := font.Font{}
   993  	tFontSize := unit.Sp(10)
   994  	tShaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
   995  	var tEditor = &Editor{
   996  		SingleLine: false,
   997  		ReadOnly:   true,
   998  	}
   999  	lines := "abc abc abc\ndef def def\nghi ghi ghi"
  1000  	tEditor.SetText(lines)
  1001  	type testCase struct {
  1002  		// Initial text selection.
  1003  		startPos, endPos int
  1004  		// Keyboard shortcut to execute.
  1005  		keyEvent key.Event
  1006  		// Expected text selection.
  1007  		selection string
  1008  	}
  1009  
  1010  	pos1, pos2 := 14, 21
  1011  	for n, tst := range []testCase{
  1012  		{pos1, pos2, key.Event{Name: "A", Modifiers: key.ModShortcut}, lines},
  1013  		{pos2, pos1, key.Event{Name: "A", Modifiers: key.ModShortcut}, lines},
  1014  		{pos1, pos2, key.Event{Name: key.NameHome, Modifiers: key.ModShift}, "def def d"},
  1015  		{pos1, pos2, key.Event{Name: key.NameEnd, Modifiers: key.ModShift}, "ef"},
  1016  		{pos2, pos1, key.Event{Name: key.NameHome, Modifiers: key.ModShift}, "de"},
  1017  		{pos2, pos1, key.Event{Name: key.NameEnd, Modifiers: key.ModShift}, "f def def"},
  1018  		{pos1, pos2, key.Event{Name: key.NameHome, Modifiers: key.ModShortcut | key.ModShift}, "abc abc abc\ndef def d"},
  1019  		{pos1, pos2, key.Event{Name: key.NameEnd, Modifiers: key.ModShortcut | key.ModShift}, "ef\nghi ghi ghi"},
  1020  		{pos2, pos1, key.Event{Name: key.NameHome, Modifiers: key.ModShortcut | key.ModShift}, "abc abc abc\nde"},
  1021  		{pos2, pos1, key.Event{Name: key.NameEnd, Modifiers: key.ModShortcut | key.ModShift}, "f def def\nghi ghi ghi"},
  1022  	} {
  1023  		tRouter := new(input.Router)
  1024  		gtx := layout.Context{
  1025  			Ops:         new(op.Ops),
  1026  			Locale:      english,
  1027  			Constraints: layout.Exact(image.Pt(100, 100)),
  1028  			Source:      tRouter.Source(),
  1029  		}
  1030  		gtx.Execute(key.FocusCmd{Tag: tEditor})
  1031  		tEditor.Layout(gtx, tShaper, tFont, tFontSize, op.CallOp{}, op.CallOp{})
  1032  
  1033  		tEditor.SetCaret(tst.startPos, tst.endPos)
  1034  		if cStart, cEnd := tEditor.Selection(); cStart != tst.startPos || cEnd != tst.endPos {
  1035  			t.Errorf("TestEditorSelect %d: initial selection", n)
  1036  		}
  1037  		tRouter.Queue(tst.keyEvent)
  1038  		tEditor.Update(gtx)
  1039  		if got := tEditor.SelectedText(); got != tst.selection {
  1040  			t.Errorf("TestEditorSelect %d: Expected %q, got %q", n, tst.selection, got)
  1041  		}
  1042  	}
  1043  }
  1044  
  1045  // Verify that an existing selection is dismissed when you press arrow keys.
  1046  func TestSelectMove(t *testing.T) {
  1047  	e := new(Editor)
  1048  	e.SetText(`0123456789`)
  1049  
  1050  	r := new(input.Router)
  1051  	gtx := layout.Context{
  1052  		Ops:    new(op.Ops),
  1053  		Locale: english,
  1054  		Source: r.Source(),
  1055  	}
  1056  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
  1057  	font := font.Font{}
  1058  	fontSize := unit.Sp(10)
  1059  
  1060  	// Layout once to populate e.lines and get focus.
  1061  	gtx.Execute(key.FocusCmd{Tag: e})
  1062  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1063  	r.Frame(gtx.Ops)
  1064  	// Set up selecton so the Editor key handler filters for all 4 directional keys.
  1065  	e.SetCaret(3, 6)
  1066  	gtx.Ops.Reset()
  1067  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1068  	r.Frame(gtx.Ops)
  1069  	gtx.Ops.Reset()
  1070  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1071  	r.Frame(gtx.Ops)
  1072  
  1073  	for _, keyName := range []key.Name{key.NameLeftArrow, key.NameRightArrow, key.NameUpArrow, key.NameDownArrow} {
  1074  		// Select 345
  1075  		e.SetCaret(3, 6)
  1076  		if expected, got := "345", e.SelectedText(); expected != got {
  1077  			t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got)
  1078  		}
  1079  
  1080  		// Press the key
  1081  		r.Queue(key.Event{State: key.Press, Name: keyName})
  1082  		gtx.Ops.Reset()
  1083  		e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1084  		r.Frame(gtx.Ops)
  1085  
  1086  		if expected, got := "", e.SelectedText(); expected != got {
  1087  			t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got)
  1088  		}
  1089  	}
  1090  }
  1091  
  1092  func TestEditor_Read(t *testing.T) {
  1093  	s := "hello world"
  1094  	buf := make([]byte, len(s))
  1095  	e := new(Editor)
  1096  	e.SetText(s)
  1097  
  1098  	_, err := e.Seek(0, io.SeekStart)
  1099  	if err != nil {
  1100  		t.Error(err)
  1101  	}
  1102  	n, err := io.ReadFull(e, buf)
  1103  	if err != nil {
  1104  		t.Error(err)
  1105  	}
  1106  	if got, want := n, len(s); got != want {
  1107  		t.Errorf("got %d; want %d", got, want)
  1108  	}
  1109  	if got, want := string(buf), s; got != want {
  1110  		t.Errorf("got %q; want %q", got, want)
  1111  	}
  1112  }
  1113  
  1114  func TestEditor_WriteTo(t *testing.T) {
  1115  	s := "hello world"
  1116  	var buf bytes.Buffer
  1117  	e := new(Editor)
  1118  	e.SetText(s)
  1119  
  1120  	n, err := io.Copy(&buf, e)
  1121  	if err != nil {
  1122  		t.Error(err)
  1123  	}
  1124  	if got, want := int(n), len(s); got != want {
  1125  		t.Errorf("got %d; want %d", got, want)
  1126  	}
  1127  	if got, want := buf.String(), s; got != want {
  1128  		t.Errorf("got %q; want %q", got, want)
  1129  	}
  1130  }
  1131  
  1132  func TestEditor_MaxLen(t *testing.T) {
  1133  	e := new(Editor)
  1134  
  1135  	e.MaxLen = 8
  1136  	e.SetText("123456789")
  1137  	if got, want := e.Text(), "12345678"; got != want {
  1138  		t.Errorf("editor failed to cap SetText")
  1139  	}
  1140  
  1141  	e.SetText("2345678")
  1142  	r := new(input.Router)
  1143  	gtx := layout.Context{
  1144  		Ops:         new(op.Ops),
  1145  		Constraints: layout.Exact(image.Pt(100, 100)),
  1146  		Source:      r.Source(),
  1147  	}
  1148  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
  1149  	fontSize := unit.Sp(10)
  1150  	font := font.Font{}
  1151  	gtx.Execute(key.FocusCmd{Tag: e})
  1152  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1153  	r.Frame(gtx.Ops)
  1154  	r.Queue(
  1155  		key.EditEvent{Range: key.Range{Start: 0, End: 2}, Text: "1234"},
  1156  		key.SelectionEvent{Start: 4, End: 4},
  1157  	)
  1158  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1159  
  1160  	if got, want := e.Text(), "12345678"; got != want {
  1161  		t.Errorf("editor failed to cap EditEvent")
  1162  	}
  1163  	if start, end := e.Selection(); start != 3 || end != 3 {
  1164  		t.Errorf("editor failed to adjust SelectionEvent")
  1165  	}
  1166  }
  1167  
  1168  func TestEditor_Filter(t *testing.T) {
  1169  	e := new(Editor)
  1170  
  1171  	e.Filter = "123456789"
  1172  	e.SetText("abcde1234")
  1173  	if got, want := e.Text(), "1234"; got != want {
  1174  		t.Errorf("editor failed to filter SetText")
  1175  	}
  1176  
  1177  	e.SetText("2345678")
  1178  	r := new(input.Router)
  1179  	gtx := layout.Context{
  1180  		Ops:         new(op.Ops),
  1181  		Constraints: layout.Exact(image.Pt(100, 100)),
  1182  		Source:      r.Source(),
  1183  	}
  1184  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
  1185  	fontSize := unit.Sp(10)
  1186  	font := font.Font{}
  1187  	gtx.Execute(key.FocusCmd{Tag: e})
  1188  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1189  	r.Frame(gtx.Ops)
  1190  	r.Queue(
  1191  		key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1"},
  1192  		key.SelectionEvent{Start: 4, End: 4},
  1193  	)
  1194  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1195  
  1196  	if got, want := e.Text(), "12345678"; got != want {
  1197  		t.Errorf("editor failed to filter EditEvent")
  1198  	}
  1199  	if start, end := e.Selection(); start != 2 || end != 2 {
  1200  		t.Errorf("editor failed to adjust SelectionEvent")
  1201  	}
  1202  }
  1203  
  1204  func TestEditor_Submit(t *testing.T) {
  1205  	e := new(Editor)
  1206  	e.Submit = true
  1207  
  1208  	r := new(input.Router)
  1209  	gtx := layout.Context{
  1210  		Ops:         new(op.Ops),
  1211  		Constraints: layout.Exact(image.Pt(100, 100)),
  1212  		Source:      r.Source(),
  1213  	}
  1214  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
  1215  	fontSize := unit.Sp(10)
  1216  	font := font.Font{}
  1217  	gtx.Execute(key.FocusCmd{Tag: e})
  1218  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1219  	r.Frame(gtx.Ops)
  1220  	r.Queue(
  1221  		key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1\n"},
  1222  	)
  1223  
  1224  	got := []EditorEvent{}
  1225  	for {
  1226  		ev, ok := e.Update(gtx)
  1227  		if !ok {
  1228  			break
  1229  		}
  1230  		got = append(got, ev)
  1231  	}
  1232  	if got, want := e.Text(), "ab1"; got != want {
  1233  		t.Errorf("editor failed to filter newline")
  1234  	}
  1235  	want := []EditorEvent{
  1236  		ChangeEvent{},
  1237  		SubmitEvent{Text: e.Text()},
  1238  	}
  1239  	if !reflect.DeepEqual(want, got) {
  1240  		t.Errorf("editor failed to register submit")
  1241  	}
  1242  }
  1243  
  1244  func TestNoFilterAllocs(t *testing.T) {
  1245  	b := testing.Benchmark(func(b *testing.B) {
  1246  		r := new(input.Router)
  1247  		e := new(Editor)
  1248  		gtx := layout.Context{
  1249  			Ops: new(op.Ops),
  1250  			Constraints: layout.Constraints{
  1251  				Max: image.Pt(100, 100),
  1252  			},
  1253  			Locale: english,
  1254  			Source: r.Source(),
  1255  		}
  1256  		b.ReportAllocs()
  1257  		b.ResetTimer()
  1258  		for i := 0; i < b.N; i++ {
  1259  			e.Update(gtx)
  1260  		}
  1261  	})
  1262  	if allocs := b.AllocsPerOp(); allocs != 0 {
  1263  		t.Fatalf("expected 0 AllocsPerOp, got %d", allocs)
  1264  	}
  1265  }
  1266  
  1267  // textWidth is a text helper for building simple selection events.
  1268  // It assumes single-run lines, which isn't safe with non-test text
  1269  // data.
  1270  func textWidth(e *Editor, lineNum, colStart, colEnd int) float32 {
  1271  	start := e.text.closestToLineCol(lineNum, colStart)
  1272  	end := e.text.closestToLineCol(lineNum, colEnd)
  1273  	delta := start.x - end.x
  1274  	if delta < 0 {
  1275  		delta = -delta
  1276  	}
  1277  	return float32(delta.Round())
  1278  }
  1279  
  1280  // testBaseline returns the y coordinate of the baseline for the
  1281  // given line number.
  1282  func textBaseline(e *Editor, lineNum int) float32 {
  1283  	start := e.text.closestToLineCol(lineNum, 0)
  1284  	return float32(start.y)
  1285  }