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 }