github.com/utopiagio/gio@v0.0.8/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  	"github.com/utopiagio/gio/f32"
    20  	"github.com/utopiagio/gio/font"
    21  	"github.com/utopiagio/gio/font/gofont"
    22  	"github.com/utopiagio/gio/font/opentype"
    23  	"github.com/utopiagio/gio/io/input"
    24  	"github.com/utopiagio/gio/io/key"
    25  	"github.com/utopiagio/gio/io/pointer"
    26  	"github.com/utopiagio/gio/io/system"
    27  	"github.com/utopiagio/gio/layout"
    28  	"github.com/utopiagio/gio/op"
    29  	"github.com/utopiagio/gio/text"
    30  	"github.com/utopiagio/gio/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.MoveEnd(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.MoveEnd(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.MoveEnd(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.MoveEnd(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.MoveEnd(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.MoveEnd(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.MoveEnd(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.MoveEnd(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.MoveEnd(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.MoveEnd(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  	moveStart
   552  	moveEnd
   553  	moveCoord
   554  	moveWord
   555  	deleteWord
   556  	moveLast // Mark end; never generated.
   557  )
   558  
   559  func TestEditorCaretConsistency(t *testing.T) {
   560  	gtx := layout.Context{
   561  		Ops:         new(op.Ops),
   562  		Constraints: layout.Exact(image.Pt(100, 100)),
   563  		Locale:      english,
   564  	}
   565  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
   566  	fontSize := unit.Sp(10)
   567  	font := font.Font{}
   568  	for _, a := range []text.Alignment{text.Start, text.Middle, text.End} {
   569  		e := &Editor{}
   570  		e.Alignment = a
   571  		e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   572  
   573  		consistent := func() error {
   574  			t.Helper()
   575  			gotLine, gotCol := e.CaretPos()
   576  			gotCoords := e.CaretCoords()
   577  			// Blow away index to re-compute position from scratch.
   578  			e.text.invalidate()
   579  			want := e.text.closestToRune(e.text.caret.start)
   580  			wantCoords := f32.Pt(float32(want.x)/64, float32(want.y))
   581  			if want.lineCol.line != gotLine || int(want.lineCol.col) != gotCol || gotCoords != wantCoords {
   582  				return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s",
   583  					gotLine, gotCol, gotCoords, want.lineCol.line, want.lineCol.col, wantCoords)
   584  			}
   585  			return nil
   586  		}
   587  		if err := consistent(); err != nil {
   588  			t.Errorf("initial editor inconsistency (alignment %s): %v", a, err)
   589  		}
   590  
   591  		move := func(mutation editMutation, str string, distance int8, x, y uint16) bool {
   592  			switch mutation {
   593  			case setText:
   594  				e.SetText(str)
   595  				e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   596  			case moveRune:
   597  				e.MoveCaret(int(distance), int(distance))
   598  			case moveLine:
   599  				e.text.MoveLines(int(distance), selectionClear)
   600  			case movePage:
   601  				e.text.MovePages(int(distance), selectionClear)
   602  			case moveStart:
   603  				e.text.MoveStart(selectionClear)
   604  			case moveEnd:
   605  				e.text.MoveEnd(selectionClear)
   606  			case moveCoord:
   607  				e.text.MoveCoord(image.Pt(int(x), int(y)))
   608  			case moveWord:
   609  				e.text.MoveWord(int(distance), selectionClear)
   610  			case deleteWord:
   611  				e.deleteWord(int(distance))
   612  			default:
   613  				return false
   614  			}
   615  			if err := consistent(); err != nil {
   616  				t.Error(err)
   617  				return false
   618  			}
   619  			return true
   620  		}
   621  		if err := quick.Check(move, nil); err != nil {
   622  			t.Errorf("editor inconsistency (alignment %s): %v", a, err)
   623  		}
   624  	}
   625  }
   626  
   627  func TestEditorMoveWord(t *testing.T) {
   628  	type Test struct {
   629  		Text  string
   630  		Start int
   631  		Skip  int
   632  		Want  int
   633  	}
   634  	tests := []Test{
   635  		{"", 0, 0, 0},
   636  		{"", 0, -1, 0},
   637  		{"", 0, 1, 0},
   638  		{"hello", 0, -1, 0},
   639  		{"hello", 0, 1, 5},
   640  		{"hello world", 3, 1, 5},
   641  		{"hello world", 3, -1, 0},
   642  		{"hello world", 8, -1, 6},
   643  		{"hello world", 8, 1, 11},
   644  		{"hello    world", 3, 1, 5},
   645  		{"hello    world", 3, 2, 14},
   646  		{"hello    world", 8, 1, 14},
   647  		{"hello    world", 8, -1, 0},
   648  		{"hello brave new world", 0, 3, 15},
   649  	}
   650  	setup := func(t string) *Editor {
   651  		e := new(Editor)
   652  		gtx := layout.Context{
   653  			Ops:         new(op.Ops),
   654  			Constraints: layout.Exact(image.Pt(100, 100)),
   655  			Locale:      english,
   656  		}
   657  		e.SetText(t)
   658  		e.Update(gtx)
   659  		return e
   660  	}
   661  	for ii, tt := range tests {
   662  		e := setup(tt.Text)
   663  		e.MoveCaret(tt.Start, tt.Start)
   664  		e.text.MoveWord(tt.Skip, selectionClear)
   665  		caretBytes := e.text.runeOffset(e.text.caret.start)
   666  		if caretBytes != tt.Want {
   667  			t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, caretBytes, tt.Want)
   668  		}
   669  	}
   670  }
   671  
   672  func TestEditorInsert(t *testing.T) {
   673  	type Test struct {
   674  		Text      string
   675  		Start     int
   676  		Selection int
   677  		Insertion string
   678  
   679  		Result string
   680  	}
   681  	tests := []Test{
   682  		// Nothing inserted
   683  		{"", 0, 0, "", ""},
   684  		{"", 0, -1, "", ""},
   685  		{"", 0, 1, "", ""},
   686  		{"", 0, -2, "", ""},
   687  		{"", 0, 2, "", ""},
   688  		{"world", 0, 0, "", "world"},
   689  		{"world", 0, -1, "", "world"},
   690  		{"world", 0, 1, "", "orld"},
   691  		{"world", 2, 0, "", "world"},
   692  		{"world", 2, -1, "", "wrld"},
   693  		{"world", 2, 1, "", "wold"},
   694  		{"world", 5, 0, "", "world"},
   695  		{"world", 5, -1, "", "worl"},
   696  		{"world", 5, 1, "", "world"},
   697  		// One rune inserted
   698  		{"", 0, 0, "_", "_"},
   699  		{"", 0, -1, "_", "_"},
   700  		{"", 0, 1, "_", "_"},
   701  		{"", 0, -2, "_", "_"},
   702  		{"", 0, 2, "_", "_"},
   703  		{"world", 0, 0, "_", "_world"},
   704  		{"world", 0, -1, "_", "_world"},
   705  		{"world", 0, 1, "_", "_orld"},
   706  		{"world", 2, 0, "_", "wo_rld"},
   707  		{"world", 2, -1, "_", "w_rld"},
   708  		{"world", 2, 1, "_", "wo_ld"},
   709  		{"world", 5, 0, "_", "world_"},
   710  		{"world", 5, -1, "_", "worl_"},
   711  		{"world", 5, 1, "_", "world_"},
   712  		// More runes inserted
   713  		{"", 0, 0, "-3-", "-3-"},
   714  		{"", 0, -1, "-3-", "-3-"},
   715  		{"", 0, 1, "-3-", "-3-"},
   716  		{"", 0, -2, "-3-", "-3-"},
   717  		{"", 0, 2, "-3-", "-3-"},
   718  		{"world", 0, 0, "-3-", "-3-world"},
   719  		{"world", 0, -1, "-3-", "-3-world"},
   720  		{"world", 0, 1, "-3-", "-3-orld"},
   721  		{"world", 2, 0, "-3-", "wo-3-rld"},
   722  		{"world", 2, -1, "-3-", "w-3-rld"},
   723  		{"world", 2, 1, "-3-", "wo-3-ld"},
   724  		{"world", 5, 0, "-3-", "world-3-"},
   725  		{"world", 5, -1, "-3-", "worl-3-"},
   726  		{"world", 5, 1, "-3-", "world-3-"},
   727  		// Runes with length > 1 inserted
   728  		{"", 0, 0, "éêè", "éêè"},
   729  		{"", 0, -1, "éêè", "éêè"},
   730  		{"", 0, 1, "éêè", "éêè"},
   731  		{"", 0, -2, "éêè", "éêè"},
   732  		{"", 0, 2, "éêè", "éêè"},
   733  		{"world", 0, 0, "éêè", "éêèworld"},
   734  		{"world", 0, -1, "éêè", "éêèworld"},
   735  		{"world", 0, 1, "éêè", "éêèorld"},
   736  		{"world", 2, 0, "éêè", "woéêèrld"},
   737  		{"world", 2, -1, "éêè", "wéêèrld"},
   738  		{"world", 2, 1, "éêè", "woéêèld"},
   739  		{"world", 5, 0, "éêè", "worldéêè"},
   740  		{"world", 5, -1, "éêè", "worléêè"},
   741  		{"world", 5, 1, "éêè", "worldéêè"},
   742  		// Runes with length > 1 deleted from selection
   743  		{"élançé", 0, 1, "", "lançé"},
   744  		{"élançé", 0, 1, "-3-", "-3-lançé"},
   745  		{"élançé", 3, 2, "-3-", "éla-3-é"},
   746  		{"élançé", 3, 3, "-3-", "éla-3-"},
   747  		{"élançé", 3, 10, "-3-", "éla-3-"},
   748  		{"élançé", 5, -1, "-3-", "élan-3-é"},
   749  		{"élançé", 6, -1, "-3-", "élanç-3-"},
   750  		{"élançé", 6, -3, "-3-", "éla-3-"},
   751  	}
   752  	setup := func(t string) *Editor {
   753  		e := new(Editor)
   754  		gtx := layout.Context{
   755  			Ops:         new(op.Ops),
   756  			Constraints: layout.Exact(image.Pt(100, 100)),
   757  			Locale:      english,
   758  		}
   759  		e.SetText(t)
   760  		e.Update(gtx)
   761  		return e
   762  	}
   763  	for ii, tt := range tests {
   764  		e := setup(tt.Text)
   765  		e.MoveCaret(tt.Start, tt.Start)
   766  		e.MoveCaret(0, tt.Selection)
   767  		e.Insert(tt.Insertion)
   768  		if e.Text() != tt.Result {
   769  			t.Fatalf("[%d] Insert: invalid result: got %q, want %q", ii, e.Text(), tt.Result)
   770  		}
   771  	}
   772  }
   773  
   774  func TestEditorDeleteWord(t *testing.T) {
   775  	type Test struct {
   776  		Text      string
   777  		Start     int
   778  		Selection int
   779  		Delete    int
   780  
   781  		Want   int
   782  		Result string
   783  	}
   784  	tests := []Test{
   785  		// No text selected
   786  		{"", 0, 0, 0, 0, ""},
   787  		{"", 0, 0, -1, 0, ""},
   788  		{"", 0, 0, 1, 0, ""},
   789  		{"", 0, 0, -2, 0, ""},
   790  		{"", 0, 0, 2, 0, ""},
   791  		{"hello", 0, 0, -1, 0, "hello"},
   792  		{"hello", 0, 0, 1, 0, ""},
   793  
   794  		// Document (imho) incorrect behavior w.r.t. deleting spaces following
   795  		// words.
   796  		{"hello world", 0, 0, 1, 0, " world"},   // Should be "world", if you ask me.
   797  		{"hello world", 0, 0, 2, 0, "world"},    // Should be "".
   798  		{"hello ", 0, 0, 1, 0, " "},             // Should be "".
   799  		{"hello world", 11, 0, -1, 6, "hello "}, // Should be "hello".
   800  		{"hello world", 11, 0, -2, 5, "hello"},  // Should be "".
   801  		{"hello ", 6, 0, -1, 0, ""},             // Correct result.
   802  
   803  		{"hello world", 3, 0, 1, 3, "hel world"},
   804  		{"hello world", 3, 0, -1, 0, "lo world"},
   805  		{"hello world", 8, 0, -1, 6, "hello rld"},
   806  		{"hello world", 8, 0, 1, 8, "hello wo"},
   807  		{"hello    world", 3, 0, 1, 3, "hel    world"},
   808  		{"hello    world", 3, 0, 2, 3, "helworld"},
   809  		{"hello    world", 8, 0, 1, 8, "hello   "},
   810  		{"hello    world", 8, 0, -1, 5, "hello world"},
   811  		{"hello brave new world", 0, 0, 3, 0, " new world"},
   812  		{"helléèçàô world", 3, 0, 1, 3, "hel world"}, // unicode char with length > 1 in deleted part
   813  		// Add selected text.
   814  		//
   815  		// Several permutations must be tested:
   816  		// - select from the left or right
   817  		// - Delete + or -
   818  		// - abs(Delete) == 1 or > 1
   819  		//
   820  		// "brave |" selected; caret at |
   821  		{"hello there brave new world", 12, 6, 1, 12, "hello there new world"}, // #16
   822  		{"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.
   823  		{"hello there brave new world", 12, 6, -1, 12, "hello there new world"},
   824  		{"hello there brave new world", 12, 6, -2, 6, "hello new world"},
   825  		{"hello there b®âve new world", 12, 6, 1, 12, "hello there new world"},  // unicode chars with length > 1 in selection
   826  		{"hello there b®âve new world", 12, 6, 2, 12, "hello there  world"},     // ditto
   827  		{"hello there b®âve new world", 12, 6, -1, 12, "hello there new world"}, // ditto
   828  		{"hello there b®âve new world", 12, 6, -2, 6, "hello new world"},        // ditto
   829  		// "|brave " selected
   830  		{"hello there brave new world", 18, -6, 1, 12, "hello there new world"}, // #20
   831  		{"hello there brave new world", 18, -6, 2, 12, "hello there  world"},    // ditto
   832  		{"hello there brave new world", 18, -6, -1, 12, "hello there new world"},
   833  		{"hello there brave new world", 18, -6, -2, 6, "hello new world"},
   834  		{"hello there b®âve new world", 18, -6, 1, 12, "hello there new world"}, // unicode chars with length > 1 in selection
   835  		// Random edge cases
   836  		{"hello there brave new world", 12, 6, 99, 12, "hello there "},
   837  		{"hello there brave new world", 18, -6, -99, 0, "new world"},
   838  	}
   839  	setup := func(t string) *Editor {
   840  		e := new(Editor)
   841  		gtx := layout.Context{
   842  			Ops:         new(op.Ops),
   843  			Constraints: layout.Exact(image.Pt(100, 100)),
   844  			Locale:      english,
   845  		}
   846  		e.SetText(t)
   847  		e.Update(gtx)
   848  		return e
   849  	}
   850  	for ii, tt := range tests {
   851  		e := setup(tt.Text)
   852  		e.MoveCaret(tt.Start, tt.Start)
   853  		e.MoveCaret(0, tt.Selection)
   854  		e.deleteWord(tt.Delete)
   855  		caretBytes := e.text.runeOffset(e.text.caret.start)
   856  		if caretBytes != tt.Want {
   857  			t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, caretBytes, tt.Want)
   858  		}
   859  		if e.Text() != tt.Result {
   860  			t.Fatalf("[%d] deleteWord: invalid result: got %q, want %q", ii, e.Text(), tt.Result)
   861  		}
   862  	}
   863  }
   864  
   865  func TestEditorNoLayout(t *testing.T) {
   866  	var e Editor
   867  	e.SetText("hi!\n")
   868  	e.MoveCaret(1, 1)
   869  }
   870  
   871  // Generate generates a value of itself, for testing/quick.
   872  func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value {
   873  	t := editMutation(rand.Intn(int(moveLast)))
   874  	return reflect.ValueOf(t)
   875  }
   876  
   877  // TestEditorSelect tests the selection code. It lays out an editor with several
   878  // lines in it, selects some text, verifies the selection, resizes the editor
   879  // to make it much narrower (which makes the lines in the editor reflow), and
   880  // then verifies that the updated (col, line) positions of the selected text
   881  // are where we expect.
   882  func TestEditorSelect(t *testing.T) {
   883  	e := new(Editor)
   884  	e.SetText(`a 2 4 6 8 a
   885  b 2 4 6 8 b
   886  c 2 4 6 8 c
   887  d 2 4 6 8 d
   888  e 2 4 6 8 e
   889  f 2 4 6 8 f
   890  g 2 4 6 8 g
   891  `)
   892  
   893  	r := new(input.Router)
   894  	gtx := layout.Context{
   895  		Ops:    new(op.Ops),
   896  		Locale: english,
   897  		Source: r.Source(),
   898  	}
   899  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
   900  	font := font.Font{}
   901  	fontSize := unit.Sp(10)
   902  
   903  	var tim time.Duration
   904  	selected := func(start, end int) string {
   905  		gtx.Execute(key.FocusCmd{Tag: e})
   906  		// Layout once with no events; populate e.lines.
   907  		e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   908  
   909  		r.Frame(gtx.Ops)
   910  		gtx.Source = r.Source()
   911  		// Build the selection events
   912  		startPos := e.text.closestToRune(start)
   913  		endPos := e.text.closestToRune(end)
   914  		r.Queue(
   915  			pointer.Event{
   916  				Buttons:  pointer.ButtonPrimary,
   917  				Kind:     pointer.Press,
   918  				Source:   pointer.Mouse,
   919  				Time:     tim,
   920  				Position: f32.Pt(textWidth(e, startPos.lineCol.line, 0, startPos.lineCol.col), textBaseline(e, startPos.lineCol.line)),
   921  			},
   922  			pointer.Event{
   923  				Kind:     pointer.Release,
   924  				Source:   pointer.Mouse,
   925  				Time:     tim,
   926  				Position: f32.Pt(textWidth(e, endPos.lineCol.line, 0, endPos.lineCol.col), textBaseline(e, endPos.lineCol.line)),
   927  			},
   928  		)
   929  		tim += time.Second // Avoid multi-clicks.
   930  
   931  		for {
   932  			_, ok := e.Update(gtx) // throw away any events from this layout
   933  			if !ok {
   934  				break
   935  			}
   936  		}
   937  		return e.SelectedText()
   938  	}
   939  	type screenPos image.Point
   940  	logicalPosMatch := func(t *testing.T, n int, label string, expected screenPos, actual combinedPos) {
   941  		t.Helper()
   942  		if actual.lineCol.line != expected.Y || actual.lineCol.col != expected.X {
   943  			t.Errorf("Test %d: Expected %s %#v; got %#v",
   944  				n, label,
   945  				expected, actual)
   946  		}
   947  	}
   948  
   949  	type testCase struct {
   950  		// input text offsets
   951  		start, end int
   952  
   953  		// expected selected text
   954  		selection string
   955  		// expected line/col positions of selection after resize
   956  		startPos, endPos screenPos
   957  	}
   958  
   959  	for n, tst := range []testCase{
   960  		{0, 1, "a", screenPos{}, screenPos{Y: 0, X: 1}},
   961  		{0, 4, "a 2 ", screenPos{}, screenPos{Y: 0, X: 4}},
   962  		{0, 11, "a 2 4 6 8 a", screenPos{}, screenPos{Y: 1, X: 3}},
   963  		{6, 10, "6 8 ", screenPos{Y: 0, X: 6}, screenPos{Y: 1, X: 2}},
   964  		{41, 66, " 6 8 d\ne 2 4 6 8 e\nf 2 4 ", screenPos{Y: 6, X: 5}, screenPos{Y: 10, X: 6}},
   965  	} {
   966  		gtx.Constraints = layout.Exact(image.Pt(100, 100))
   967  		if got := selected(tst.start, tst.end); got != tst.selection {
   968  			t.Errorf("Test %d pt1: Expected %q, got %q", n, tst.selection, got)
   969  			continue
   970  		}
   971  
   972  		// Constrain the editor to roughly 6 columns wide and redraw
   973  		gtx.Constraints = layout.Exact(image.Pt(36, 36))
   974  		// Keep existing selection
   975  		gtx = gtx.Disabled()
   976  		e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   977  
   978  		caretStart := e.text.closestToRune(e.text.caret.start)
   979  		caretEnd := e.text.closestToRune(e.text.caret.end)
   980  		logicalPosMatch(t, n, "start", tst.startPos, caretEnd)
   981  		logicalPosMatch(t, n, "end", tst.endPos, caretStart)
   982  	}
   983  }
   984  
   985  // Verify that an existing selection is dismissed when you press arrow keys.
   986  func TestSelectMove(t *testing.T) {
   987  	e := new(Editor)
   988  	e.SetText(`0123456789`)
   989  
   990  	r := new(input.Router)
   991  	gtx := layout.Context{
   992  		Ops:    new(op.Ops),
   993  		Locale: english,
   994  		Source: r.Source(),
   995  	}
   996  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
   997  	font := font.Font{}
   998  	fontSize := unit.Sp(10)
   999  
  1000  	// Layout once to populate e.lines and get focus.
  1001  	gtx.Execute(key.FocusCmd{Tag: e})
  1002  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1003  	r.Frame(gtx.Ops)
  1004  	// Set up selecton so the Editor key handler filters for all 4 directional keys.
  1005  	e.SetCaret(3, 6)
  1006  	gtx.Ops.Reset()
  1007  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1008  	r.Frame(gtx.Ops)
  1009  	gtx.Ops.Reset()
  1010  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1011  	r.Frame(gtx.Ops)
  1012  
  1013  	for _, keyName := range []key.Name{key.NameLeftArrow, key.NameRightArrow, key.NameUpArrow, key.NameDownArrow} {
  1014  		// Select 345
  1015  		e.SetCaret(3, 6)
  1016  		if expected, got := "345", e.SelectedText(); expected != got {
  1017  			t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got)
  1018  		}
  1019  
  1020  		// Press the key
  1021  		r.Queue(key.Event{State: key.Press, Name: keyName})
  1022  		gtx.Ops.Reset()
  1023  		e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1024  		r.Frame(gtx.Ops)
  1025  
  1026  		if expected, got := "", e.SelectedText(); expected != got {
  1027  			t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got)
  1028  		}
  1029  	}
  1030  }
  1031  
  1032  func TestEditor_Read(t *testing.T) {
  1033  	s := "hello world"
  1034  	buf := make([]byte, len(s))
  1035  	e := new(Editor)
  1036  	e.SetText(s)
  1037  
  1038  	_, err := e.Seek(0, io.SeekStart)
  1039  	if err != nil {
  1040  		t.Error(err)
  1041  	}
  1042  	n, err := io.ReadFull(e, buf)
  1043  	if err != nil {
  1044  		t.Error(err)
  1045  	}
  1046  	if got, want := n, len(s); got != want {
  1047  		t.Errorf("got %d; want %d", got, want)
  1048  	}
  1049  	if got, want := string(buf), s; got != want {
  1050  		t.Errorf("got %q; want %q", got, want)
  1051  	}
  1052  }
  1053  
  1054  func TestEditor_WriteTo(t *testing.T) {
  1055  	s := "hello world"
  1056  	var buf bytes.Buffer
  1057  	e := new(Editor)
  1058  	e.SetText(s)
  1059  
  1060  	n, err := io.Copy(&buf, e)
  1061  	if err != nil {
  1062  		t.Error(err)
  1063  	}
  1064  	if got, want := int(n), len(s); got != want {
  1065  		t.Errorf("got %d; want %d", got, want)
  1066  	}
  1067  	if got, want := buf.String(), s; got != want {
  1068  		t.Errorf("got %q; want %q", got, want)
  1069  	}
  1070  }
  1071  
  1072  func TestEditor_MaxLen(t *testing.T) {
  1073  	e := new(Editor)
  1074  
  1075  	e.MaxLen = 8
  1076  	e.SetText("123456789")
  1077  	if got, want := e.Text(), "12345678"; got != want {
  1078  		t.Errorf("editor failed to cap SetText")
  1079  	}
  1080  
  1081  	e.SetText("2345678")
  1082  	r := new(input.Router)
  1083  	gtx := layout.Context{
  1084  		Ops:         new(op.Ops),
  1085  		Constraints: layout.Exact(image.Pt(100, 100)),
  1086  		Source:      r.Source(),
  1087  	}
  1088  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
  1089  	fontSize := unit.Sp(10)
  1090  	font := font.Font{}
  1091  	gtx.Execute(key.FocusCmd{Tag: e})
  1092  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1093  	r.Frame(gtx.Ops)
  1094  	r.Queue(
  1095  		key.EditEvent{Range: key.Range{Start: 0, End: 2}, Text: "1234"},
  1096  		key.SelectionEvent{Start: 4, End: 4},
  1097  	)
  1098  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1099  
  1100  	if got, want := e.Text(), "12345678"; got != want {
  1101  		t.Errorf("editor failed to cap EditEvent")
  1102  	}
  1103  	if start, end := e.Selection(); start != 3 || end != 3 {
  1104  		t.Errorf("editor failed to adjust SelectionEvent")
  1105  	}
  1106  }
  1107  
  1108  func TestEditor_Filter(t *testing.T) {
  1109  	e := new(Editor)
  1110  
  1111  	e.Filter = "123456789"
  1112  	e.SetText("abcde1234")
  1113  	if got, want := e.Text(), "1234"; got != want {
  1114  		t.Errorf("editor failed to filter SetText")
  1115  	}
  1116  
  1117  	e.SetText("2345678")
  1118  	r := new(input.Router)
  1119  	gtx := layout.Context{
  1120  		Ops:         new(op.Ops),
  1121  		Constraints: layout.Exact(image.Pt(100, 100)),
  1122  		Source:      r.Source(),
  1123  	}
  1124  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
  1125  	fontSize := unit.Sp(10)
  1126  	font := font.Font{}
  1127  	gtx.Execute(key.FocusCmd{Tag: e})
  1128  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1129  	r.Frame(gtx.Ops)
  1130  	r.Queue(
  1131  		key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1"},
  1132  		key.SelectionEvent{Start: 4, End: 4},
  1133  	)
  1134  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1135  
  1136  	if got, want := e.Text(), "12345678"; got != want {
  1137  		t.Errorf("editor failed to filter EditEvent")
  1138  	}
  1139  	if start, end := e.Selection(); start != 2 || end != 2 {
  1140  		t.Errorf("editor failed to adjust SelectionEvent")
  1141  	}
  1142  }
  1143  
  1144  func TestEditor_Submit(t *testing.T) {
  1145  	e := new(Editor)
  1146  	e.Submit = true
  1147  
  1148  	r := new(input.Router)
  1149  	gtx := layout.Context{
  1150  		Ops:         new(op.Ops),
  1151  		Constraints: layout.Exact(image.Pt(100, 100)),
  1152  		Source:      r.Source(),
  1153  	}
  1154  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
  1155  	fontSize := unit.Sp(10)
  1156  	font := font.Font{}
  1157  	gtx.Execute(key.FocusCmd{Tag: e})
  1158  	e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
  1159  	r.Frame(gtx.Ops)
  1160  	r.Queue(
  1161  		key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1\n"},
  1162  	)
  1163  
  1164  	got := []EditorEvent{}
  1165  	for {
  1166  		ev, ok := e.Update(gtx)
  1167  		if !ok {
  1168  			break
  1169  		}
  1170  		got = append(got, ev)
  1171  	}
  1172  	if got, want := e.Text(), "ab1"; got != want {
  1173  		t.Errorf("editor failed to filter newline")
  1174  	}
  1175  	want := []EditorEvent{
  1176  		ChangeEvent{},
  1177  		SubmitEvent{Text: e.Text()},
  1178  	}
  1179  	if !reflect.DeepEqual(want, got) {
  1180  		t.Errorf("editor failed to register submit")
  1181  	}
  1182  }
  1183  
  1184  func TestNoFilterAllocs(t *testing.T) {
  1185  	b := testing.Benchmark(func(b *testing.B) {
  1186  		r := new(input.Router)
  1187  		e := new(Editor)
  1188  		gtx := layout.Context{
  1189  			Ops: new(op.Ops),
  1190  			Constraints: layout.Constraints{
  1191  				Max: image.Pt(100, 100),
  1192  			},
  1193  			Locale: english,
  1194  			Source: r.Source(),
  1195  		}
  1196  		b.ReportAllocs()
  1197  		b.ResetTimer()
  1198  		for i := 0; i < b.N; i++ {
  1199  			e.Update(gtx)
  1200  		}
  1201  	})
  1202  	if allocs := b.AllocsPerOp(); allocs != 0 {
  1203  		t.Fatalf("expected 0 AllocsPerOp, got %d", allocs)
  1204  	}
  1205  }
  1206  
  1207  // textWidth is a text helper for building simple selection events.
  1208  // It assumes single-run lines, which isn't safe with non-test text
  1209  // data.
  1210  func textWidth(e *Editor, lineNum, colStart, colEnd int) float32 {
  1211  	start := e.text.closestToLineCol(lineNum, colStart)
  1212  	end := e.text.closestToLineCol(lineNum, colEnd)
  1213  	delta := start.x - end.x
  1214  	if delta < 0 {
  1215  		delta = -delta
  1216  	}
  1217  	return float32(delta.Round())
  1218  }
  1219  
  1220  // testBaseline returns the y coordinate of the baseline for the
  1221  // given line number.
  1222  func textBaseline(e *Editor, lineNum int) float32 {
  1223  	start := e.text.closestToLineCol(lineNum, 0)
  1224  	return float32(start.y)
  1225  }