github.com/utopiagio/gio@v0.0.8/widget/editor.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package widget 4 5 import ( 6 "bufio" 7 "image" 8 "io" 9 //"log" 10 "math" 11 "strings" 12 "time" 13 "unicode" 14 "unicode/utf8" 15 16 "github.com/utopiagio/gio/f32" 17 "github.com/utopiagio/gio/font" 18 "github.com/utopiagio/gio/gesture" 19 "github.com/utopiagio/gio/io/clipboard" 20 "github.com/utopiagio/gio/io/event" 21 "github.com/utopiagio/gio/io/key" 22 "github.com/utopiagio/gio/io/pointer" 23 "github.com/utopiagio/gio/io/semantic" 24 "github.com/utopiagio/gio/io/system" 25 "github.com/utopiagio/gio/io/transfer" 26 "github.com/utopiagio/gio/layout" 27 "github.com/utopiagio/gio/op" 28 "github.com/utopiagio/gio/op/clip" 29 "github.com/utopiagio/gio/text" 30 "github.com/utopiagio/gio/unit" 31 ) 32 33 // Editor implements an editable and scrollable text area. 34 type Editor struct { 35 // text manages the text buffer and provides shaping and cursor positioning 36 // services. 37 text textView 38 // Alignment controls the alignment of text within the editor. 39 Alignment text.Alignment 40 // LineHeight determines the gap between baselines of text. If zero, a sensible 41 // default will be used. 42 LineHeight unit.Sp 43 // LineHeightScale is multiplied by LineHeight to determine the final gap 44 // between baselines. If zero, a sensible default will be used. 45 LineHeightScale float32 46 // SingleLine force the text to stay on a single line. 47 // SingleLine also sets the scrolling direction to 48 // horizontal. 49 SingleLine bool 50 // ReadOnly controls whether the contents of the editor can be altered by 51 // user interaction. If set to true, the editor will allow selecting text 52 // and copying it interactively, but not modifying it. 53 ReadOnly bool 54 // Submit enabled translation of carriage return keys to SubmitEvents. 55 // If not enabled, carriage returns are inserted as newlines in the text. 56 Submit bool 57 // Mask replaces the visual display of each rune in the contents with the given rune. 58 // Newline characters are not masked. When non-zero, the unmasked contents 59 // are accessed by Len, Text, and SetText. 60 Mask rune 61 // InputHint specifies the type of on-screen keyboard to be displayed. 62 InputHint key.InputHint 63 // MaxLen limits the editor content to a maximum length. Zero means no limit. 64 MaxLen int 65 // Filter is the list of characters allowed in the Editor. If Filter is empty, 66 // all characters are allowed. 67 Filter string 68 // WrapPolicy configures how displayed text will be broken into lines. 69 WrapPolicy text.WrapPolicy 70 71 buffer *editBuffer 72 // scratch is a byte buffer that is reused to efficiently read portions of text 73 // from the textView. 74 scratch []byte 75 blinkStart time.Time 76 77 // ime tracks the state relevant to input methods. 78 ime struct { 79 imeState 80 scratch []byte 81 } 82 83 dragging bool 84 dragger gesture.Drag 85 scroller gesture.Scroll 86 scrollCaret bool 87 showCaret bool 88 89 clicker gesture.Click 90 91 // history contains undo history. 92 history []modification 93 // nextHistoryIdx is the index within the history of the next modification. This 94 // is only not len(history) immediately after undo operations occur. It is framed as the "next" value 95 // to make the zero value consistent. 96 nextHistoryIdx int 97 98 pending []EditorEvent 99 } 100 101 type offEntry struct { 102 runes int 103 bytes int 104 } 105 106 type imeState struct { 107 selection struct { 108 rng key.Range 109 caret key.Caret 110 } 111 snippet key.Snippet 112 start, end int 113 } 114 115 type maskReader struct { 116 // rr is the underlying reader. 117 rr io.RuneReader 118 maskBuf [utf8.UTFMax]byte 119 // mask is the utf-8 encoded mask rune. 120 mask []byte 121 // overflow contains excess mask bytes left over after the last Read call. 122 overflow []byte 123 } 124 125 type selectionAction int 126 127 const ( 128 selectionExtend selectionAction = iota 129 selectionClear 130 ) 131 132 func (m *maskReader) Reset(r io.Reader, mr rune) { 133 m.rr = bufio.NewReader(r) 134 n := utf8.EncodeRune(m.maskBuf[:], mr) 135 m.mask = m.maskBuf[:n] 136 } 137 138 // Read reads from the underlying reader and replaces every 139 // rune with the mask rune. 140 func (m *maskReader) Read(b []byte) (n int, err error) { 141 for len(b) > 0 { 142 var replacement []byte 143 if len(m.overflow) > 0 { 144 replacement = m.overflow 145 } else { 146 var r rune 147 r, _, err = m.rr.ReadRune() 148 if err != nil { 149 break 150 } 151 if r == '\n' { 152 replacement = []byte{'\n'} 153 } else { 154 replacement = m.mask 155 } 156 } 157 nn := copy(b, replacement) 158 m.overflow = replacement[nn:] 159 n += nn 160 b = b[nn:] 161 } 162 return n, err 163 } 164 165 type EditorEvent interface { 166 isEditorEvent() 167 } 168 169 // A ChangeEvent is generated for every user change to the text. 170 type ChangeEvent struct{} 171 172 // A SubmitEvent is generated when Submit is set 173 // and a carriage return key is pressed. 174 type SubmitEvent struct { 175 Text string 176 } 177 178 // A SelectEvent is generated when the user selects some text, or changes the 179 // selection (e.g. with a shift-click), including if they remove the 180 // selection. The selected text is not part of the event, on the theory that 181 // it could be a relatively expensive operation (for a large editor), most 182 // applications won't actually care about it, and those that do can call 183 // Editor.SelectedText() (which can be empty). 184 type SelectEvent struct{} 185 186 const ( 187 blinksPerSecond = 1 188 maxBlinkDuration = 10 * time.Second 189 ) 190 191 func (e *Editor) processEvents(gtx layout.Context) (ev EditorEvent, ok bool) { 192 if len(e.pending) > 0 { 193 out := e.pending[0] 194 e.pending = e.pending[:copy(e.pending, e.pending[1:])] 195 return out, true 196 } 197 selStart, selEnd := e.Selection() 198 defer func() { 199 afterSelStart, afterSelEnd := e.Selection() 200 if selStart != afterSelStart || selEnd != afterSelEnd { 201 if ok { 202 e.pending = append(e.pending, SelectEvent{}) 203 } else { 204 ev = SelectEvent{} 205 ok = true 206 } 207 } 208 }() 209 ev, ok = e.processPointer(gtx) 210 if ok { 211 return ev, ok 212 } 213 ev, ok = e.processKey(gtx) 214 if ok { 215 return ev, ok 216 } 217 return nil, false 218 } 219 220 func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) { 221 sbounds := e.text.ScrollBounds() 222 var smin, smax int 223 var axis gesture.Axis 224 if e.SingleLine { 225 axis = gesture.Horizontal 226 smin, smax = sbounds.Min.X, sbounds.Max.X 227 } else { 228 axis = gesture.Vertical 229 smin, smax = sbounds.Min.Y, sbounds.Max.Y 230 } 231 var scrollRange image.Rectangle 232 textDims := e.text.FullDimensions() 233 visibleDims := e.text.Dimensions() 234 if e.SingleLine { 235 scrollOffX := e.text.ScrollOff().X 236 scrollRange.Min.X = min(-scrollOffX, 0) 237 scrollRange.Max.X = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X)) 238 } else { 239 scrollOffY := e.text.ScrollOff().Y 240 scrollRange.Min.Y = -scrollOffY 241 scrollRange.Max.Y = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y)) 242 } 243 sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollRange) 244 var soff int 245 if e.SingleLine { 246 e.text.ScrollRel(sdist, 0) 247 soff = e.text.ScrollOff().X 248 } else { 249 e.text.ScrollRel(0, sdist) 250 soff = e.text.ScrollOff().Y 251 } 252 for { 253 evt, ok := e.clicker.Update(gtx.Source) 254 if !ok { 255 break 256 } 257 ev, ok := e.processPointerEvent(gtx, evt) 258 if ok { 259 return ev, ok 260 } 261 } 262 for { 263 evt, ok := e.dragger.Update(gtx.Metric, gtx.Source, gesture.Both) 264 if !ok { 265 break 266 } 267 ev, ok := e.processPointerEvent(gtx, evt) 268 if ok { 269 return ev, ok 270 } 271 } 272 273 if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) { 274 e.scroller.Stop() 275 } 276 return nil, false 277 } 278 279 func (e *Editor) processPointerEvent(gtx layout.Context, ev event.Event) (EditorEvent, bool) { 280 switch evt := ev.(type) { 281 case gesture.ClickEvent: 282 switch { 283 case evt.Kind == gesture.KindPress && evt.Source == pointer.Mouse, 284 evt.Kind == gesture.KindClick && evt.Source != pointer.Mouse: 285 prevCaretPos, _ := e.text.Selection() 286 e.blinkStart = gtx.Now 287 e.text.MoveCoord(image.Point{ 288 X: int(math.Round(float64(evt.Position.X))), 289 Y: int(math.Round(float64(evt.Position.Y))), 290 }) 291 gtx.Execute(key.FocusCmd{Tag: e}) 292 if e.scroller.State() != gesture.StateFlinging { 293 e.scrollCaret = true 294 } 295 296 if evt.Modifiers == key.ModShift { 297 start, end := e.text.Selection() 298 // If they clicked closer to the end, then change the end to 299 // where the caret used to be (effectively swapping start & end). 300 if abs(end-start) < abs(start-prevCaretPos) { 301 e.text.SetCaret(start, prevCaretPos) 302 } 303 } else { 304 e.text.ClearSelection() 305 } 306 e.dragging = true 307 308 // Process multi-clicks. 309 switch { 310 case evt.NumClicks == 2: 311 e.text.MoveWord(-1, selectionClear) 312 e.text.MoveWord(1, selectionExtend) 313 e.dragging = false 314 case evt.NumClicks >= 3: 315 e.text.MoveStart(selectionClear) 316 e.text.MoveEnd(selectionExtend) 317 e.dragging = false 318 } 319 } 320 case pointer.Event: 321 release := false 322 switch { 323 case evt.Kind == pointer.Release && evt.Source == pointer.Mouse: 324 release = true 325 fallthrough 326 case evt.Kind == pointer.Drag && evt.Source == pointer.Mouse: 327 if e.dragging { 328 e.blinkStart = gtx.Now 329 e.text.MoveCoord(image.Point{ 330 X: int(math.Round(float64(evt.Position.X))), 331 Y: int(math.Round(float64(evt.Position.Y))), 332 }) 333 e.scrollCaret = true 334 335 if release { 336 e.dragging = false 337 } 338 } 339 } 340 } 341 return nil, false 342 } 343 344 func condFilter(pred bool, f key.Filter) event.Filter { 345 if pred { 346 return f 347 } else { 348 return nil 349 } 350 } 351 352 func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) { 353 if e.text.Changed() { 354 return ChangeEvent{}, true 355 } 356 caret, _ := e.text.Selection() 357 atBeginning := caret == 0 358 atEnd := caret == e.text.Len() 359 if gtx.Locale.Direction.Progression() != system.FromOrigin { 360 atEnd, atBeginning = atBeginning, atEnd 361 } 362 filters := []event.Filter{ 363 key.FocusFilter{Target: e}, 364 transfer.TargetFilter{Target: e, Type: "application/text"}, 365 key.Filter{Focus: e, Name: key.NameEnter, Optional: key.ModShift}, 366 key.Filter{Focus: e, Name: key.NameReturn, Optional: key.ModShift}, 367 368 key.Filter{Focus: e, Name: "Z", Required: key.ModShortcut, Optional: key.ModShift}, 369 key.Filter{Focus: e, Name: "C", Required: key.ModShortcut}, 370 key.Filter{Focus: e, Name: "V", Required: key.ModShortcut}, 371 key.Filter{Focus: e, Name: "X", Required: key.ModShortcut}, 372 key.Filter{Focus: e, Name: "A", Required: key.ModShortcut}, 373 374 key.Filter{Focus: e, Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift}, 375 key.Filter{Focus: e, Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift}, 376 377 key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShift}, 378 key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShift}, 379 key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift}, 380 key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift}, 381 condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}), 382 condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}), 383 condFilter(!atEnd, key.Filter{Focus: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}), 384 condFilter(!atEnd, key.Filter{Focus: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}), 385 } 386 // adjust keeps track of runes dropped because of MaxLen. 387 var adjust int 388 for { 389 ke, ok := gtx.Event(filters...) 390 if !ok { 391 break 392 } 393 e.blinkStart = gtx.Now 394 switch ke := ke.(type) { 395 case key.FocusEvent: 396 // Reset IME state. 397 e.ime.imeState = imeState{} 398 if ke.Focus { 399 gtx.Execute(key.SoftKeyboardCmd{Show: true}) 400 } 401 case key.Event: 402 if !gtx.Focused(e) || ke.State != key.Press { 403 break 404 } 405 if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) { 406 if !ke.Modifiers.Contain(key.ModShift) { 407 e.scratch = e.text.Text(e.scratch) 408 return SubmitEvent{ 409 Text: string(e.scratch), 410 }, true 411 } 412 } 413 e.scrollCaret = true 414 e.scroller.Stop() 415 ev, ok := e.command(gtx, ke) 416 if ok { 417 return ev, ok 418 } 419 case key.SnippetEvent: 420 e.updateSnippet(gtx, ke.Start, ke.End) 421 case key.EditEvent: 422 if e.ReadOnly { 423 break 424 } 425 e.scrollCaret = true 426 e.scroller.Stop() 427 s := ke.Text 428 moves := 0 429 submit := false 430 switch { 431 case e.Submit: 432 if i := strings.IndexByte(s, '\n'); i != -1 { 433 submit = true 434 moves += len(s) - i 435 s = s[:i] 436 } 437 case e.SingleLine: 438 s = strings.ReplaceAll(s, "\n", " ") 439 } 440 moves += e.replace(ke.Range.Start, ke.Range.End, s, true) 441 adjust += utf8.RuneCountInString(ke.Text) - moves 442 // Reset caret xoff. 443 e.text.MoveCaret(0, 0) 444 if submit { 445 e.scratch = e.text.Text(e.scratch) 446 submitEvent := SubmitEvent{ 447 Text: string(e.scratch), 448 } 449 if e.text.Changed() { 450 e.pending = append(e.pending, submitEvent) 451 return ChangeEvent{}, true 452 } 453 return submitEvent, true 454 } 455 // Complete a paste event, initiated by Shortcut-V in Editor.command(). 456 case transfer.DataEvent: 457 e.scrollCaret = true 458 e.scroller.Stop() 459 content, err := io.ReadAll(ke.Open()) 460 if err == nil { 461 if e.Insert(string(content)) != 0 { 462 return ChangeEvent{}, true 463 } 464 } 465 case key.SelectionEvent: 466 e.scrollCaret = true 467 e.scroller.Stop() 468 ke.Start -= adjust 469 ke.End -= adjust 470 adjust = 0 471 e.text.SetCaret(ke.Start, ke.End) 472 } 473 } 474 if e.text.Changed() { 475 return ChangeEvent{}, true 476 } 477 return nil, false 478 } 479 480 func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) { 481 direction := 1 482 if gtx.Locale.Direction.Progression() == system.TowardOrigin { 483 direction = -1 484 } 485 moveByWord := k.Modifiers.Contain(key.ModShortcutAlt) 486 selAct := selectionClear 487 if k.Modifiers.Contain(key.ModShift) { 488 selAct = selectionExtend 489 } 490 if k.Modifiers.Contain(key.ModShortcut) { 491 switch k.Name { 492 // Initiate a paste operation, by requesting the clipboard contents; other 493 // half is in Editor.processKey() under clipboard.Event. 494 case "V": 495 if !e.ReadOnly { 496 gtx.Execute(clipboard.ReadCmd{Tag: e}) 497 } 498 // Copy or Cut selection -- ignored if nothing selected. 499 case "C", "X": 500 e.scratch = e.text.SelectedText(e.scratch) 501 if text := string(e.scratch); text != "" { 502 gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))}) 503 if k.Name == "X" && !e.ReadOnly { 504 if e.Delete(1) != 0 { 505 return ChangeEvent{}, true 506 } 507 } 508 } 509 // Select all 510 case "A": 511 e.text.SetCaret(0, e.text.Len()) 512 case "Z": 513 if !e.ReadOnly { 514 if k.Modifiers.Contain(key.ModShift) { 515 if ev, ok := e.redo(); ok { 516 return ev, ok 517 } 518 } else { 519 if ev, ok := e.undo(); ok { 520 return ev, ok 521 } 522 } 523 } 524 } 525 return nil, false 526 } 527 switch k.Name { 528 case key.NameReturn, key.NameEnter: 529 if !e.ReadOnly { 530 if e.Insert("\n") != 0 { 531 return ChangeEvent{}, true 532 } 533 } 534 case key.NameDeleteBackward: 535 if !e.ReadOnly { 536 if moveByWord { 537 if e.deleteWord(-1) != 0 { 538 return ChangeEvent{}, true 539 } 540 } else { 541 if e.Delete(-1) != 0 { 542 return ChangeEvent{}, true 543 } 544 } 545 } 546 case key.NameDeleteForward: 547 if !e.ReadOnly { 548 if moveByWord { 549 if e.deleteWord(1) != 0 { 550 return ChangeEvent{}, true 551 } 552 } else { 553 if e.Delete(1) != 0 { 554 return ChangeEvent{}, true 555 } 556 } 557 } 558 case key.NameUpArrow: 559 e.text.MoveLines(-1, selAct) 560 case key.NameDownArrow: 561 e.text.MoveLines(+1, selAct) 562 case key.NameLeftArrow: 563 if moveByWord { 564 e.text.MoveWord(-1*direction, selAct) 565 } else { 566 if selAct == selectionClear { 567 e.text.ClearSelection() 568 } 569 e.text.MoveCaret(-1*direction, -1*direction*int(selAct)) 570 } 571 case key.NameRightArrow: 572 if moveByWord { 573 e.text.MoveWord(1*direction, selAct) 574 } else { 575 if selAct == selectionClear { 576 e.text.ClearSelection() 577 } 578 e.text.MoveCaret(1*direction, int(selAct)*direction) 579 } 580 case key.NamePageUp: 581 e.text.MovePages(-1, selAct) 582 case key.NamePageDown: 583 e.text.MovePages(+1, selAct) 584 case key.NameHome: 585 e.text.MoveStart(selAct) 586 case key.NameEnd: 587 e.text.MoveEnd(selAct) 588 } 589 return nil, false 590 } 591 592 // initBuffer should be invoked first in every exported function that accesses 593 // text state. It ensures that the underlying text widget is both ready to use 594 // and has its fields synced with the editor. 595 func (e *Editor) initBuffer() { 596 if e.buffer == nil { 597 e.buffer = new(editBuffer) 598 e.text.SetSource(e.buffer) 599 } 600 e.text.Alignment = e.Alignment 601 e.text.LineHeight = e.LineHeight 602 e.text.LineHeightScale = e.LineHeightScale 603 e.text.SingleLine = e.SingleLine 604 e.text.Mask = e.Mask 605 e.text.WrapPolicy = e.WrapPolicy 606 } 607 608 // Update the state of the editor in response to input events. Update consumes editor 609 // input events until there are no remaining events or an editor event is generated. 610 // To fully update the state of the editor, callers should call Update until it returns 611 // false. 612 func (e *Editor) Update(gtx layout.Context) (EditorEvent, bool) { 613 e.initBuffer() 614 event, ok := e.processEvents(gtx) 615 // Notify IME of selection if it changed. 616 newSel := e.ime.selection 617 start, end := e.text.Selection() 618 newSel.rng = key.Range{ 619 Start: start, 620 End: end, 621 } 622 caretPos, carAsc, carDesc := e.text.CaretInfo() 623 newSel.caret = key.Caret{ 624 Pos: layout.FPt(caretPos), 625 Ascent: float32(carAsc), 626 Descent: float32(carDesc), 627 } 628 if newSel != e.ime.selection { 629 e.ime.selection = newSel 630 gtx.Execute(key.SelectionCmd{Tag: e, Range: newSel.rng, Caret: newSel.caret}) 631 } 632 633 e.updateSnippet(gtx, e.ime.start, e.ime.end) 634 return event, ok 635 } 636 637 // Layout lays out the editor using the provided textMaterial as the paint material 638 // for the text glyphs+caret and the selectMaterial as the paint material for the 639 // selection rectangle. 640 func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions { 641 for { 642 _, ok := e.Update(gtx) 643 if !ok { 644 break 645 } 646 } 647 648 e.text.Layout(gtx, lt, font, size) 649 return e.layout(gtx, textMaterial, selectMaterial) 650 } 651 652 // updateSnippet queues a key.SnippetCmd if the snippet content or position 653 // have changed. off and len are in runes. 654 func (e *Editor) updateSnippet(gtx layout.Context, start, end int) { 655 if start > end { 656 start, end = end, start 657 } 658 length := e.text.Len() 659 if start > length { 660 start = length 661 } 662 if end > length { 663 end = length 664 } 665 e.ime.start = start 666 e.ime.end = end 667 startOff := e.text.ByteOffset(start) 668 endOff := e.text.ByteOffset(end) 669 n := endOff - startOff 670 if n > int64(len(e.ime.scratch)) { 671 e.ime.scratch = make([]byte, n) 672 } 673 scratch := e.ime.scratch[:n] 674 read, _ := e.text.ReadAt(scratch, startOff) 675 if read != len(scratch) { 676 panic("e.rr.Read truncated data") 677 } 678 newSnip := key.Snippet{ 679 Range: key.Range{ 680 Start: e.ime.start, 681 End: e.ime.end, 682 }, 683 Text: e.ime.snippet.Text, 684 } 685 if string(scratch) != newSnip.Text { 686 newSnip.Text = string(scratch) 687 } 688 if newSnip == e.ime.snippet { 689 return 690 } 691 e.ime.snippet = newSnip 692 gtx.Execute(key.SnippetCmd{Tag: e, Snippet: newSnip}) 693 } 694 695 func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.CallOp) layout.Dimensions { 696 // Adjust scrolling for new viewport and layout. 697 e.text.ScrollRel(0, 0) 698 699 if e.scrollCaret { 700 e.scrollCaret = false 701 e.text.ScrollToCaret() 702 } 703 visibleDims := e.text.Dimensions() 704 705 defer clip.Rect(image.Rectangle{Max: visibleDims.Size}).Push(gtx.Ops).Pop() 706 pointer.CursorText.Add(gtx.Ops) 707 event.Op(gtx.Ops, e) 708 key.InputHintOp{Tag: e, Hint: e.InputHint}.Add(gtx.Ops) 709 710 e.scroller.Add(gtx.Ops) 711 712 e.clicker.Add(gtx.Ops) 713 e.dragger.Add(gtx.Ops) 714 e.showCaret = false 715 if gtx.Focused(e) { 716 now := gtx.Now 717 dt := now.Sub(e.blinkStart) 718 blinking := dt < maxBlinkDuration 719 const timePerBlink = time.Second / blinksPerSecond 720 nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2)) 721 if blinking { 722 gtx.Execute(op.InvalidateCmd{At: nextBlink}) 723 } 724 e.showCaret = !blinking || dt%timePerBlink < timePerBlink/2 725 } 726 semantic.Editor.Add(gtx.Ops) 727 if e.Len() > 0 { 728 e.paintSelection(gtx, selectMaterial) 729 e.paintText(gtx, textMaterial) 730 } 731 if gtx.Enabled() { 732 e.paintCaret(gtx, textMaterial) 733 } 734 return visibleDims 735 } 736 737 // paintSelection paints the contrasting background for selected text using the provided 738 // material to set the painting material for the selection. 739 func (e *Editor) paintSelection(gtx layout.Context, material op.CallOp) { 740 e.initBuffer() 741 if !gtx.Focused(e) { 742 return 743 } 744 e.text.PaintSelection(gtx, material) 745 } 746 747 // paintText paints the text glyphs using the provided material to set the fill of the 748 // glyphs. 749 func (e *Editor) paintText(gtx layout.Context, material op.CallOp) { 750 e.initBuffer() 751 e.text.PaintText(gtx, material) 752 } 753 754 // paintCaret paints the text glyphs using the provided material to set the fill material 755 // of the caret rectangle. 756 func (e *Editor) paintCaret(gtx layout.Context, material op.CallOp) { 757 e.initBuffer() 758 if !e.showCaret || e.ReadOnly { 759 return 760 } 761 e.text.PaintCaret(gtx, material) 762 } 763 764 // Len is the length of the editor contents, in runes. 765 func (e *Editor) Len() int { 766 e.initBuffer() 767 return e.text.Len() 768 } 769 770 // Text returns the contents of the editor. 771 func (e *Editor) Text() string { 772 e.initBuffer() 773 e.scratch = e.text.Text(e.scratch) 774 return string(e.scratch) 775 } 776 777 func (e *Editor) SetText(s string) { 778 e.initBuffer() 779 if e.SingleLine { 780 s = strings.ReplaceAll(s, "\n", " ") 781 } 782 e.replace(0, e.text.Len(), s, true) 783 // Reset xoff and move the caret to the beginning. 784 e.SetCaret(0, 0) 785 } 786 787 // CaretPos returns the line & column numbers of the caret. 788 func (e *Editor) CaretPos() (line, col int) { 789 e.initBuffer() 790 return e.text.CaretPos() 791 } 792 793 // CaretCoords returns the coordinates of the caret, relative to the 794 // editor itself. 795 func (e *Editor) CaretCoords() f32.Point { 796 e.initBuffer() 797 return e.text.CaretCoords() 798 } 799 800 // Delete runes from the caret position. The sign of the argument specifies the 801 // direction to delete: positive is forward, negative is backward. 802 // 803 // If there is a selection, it is deleted and counts as a single grapheme 804 // cluster. 805 func (e *Editor) Delete(graphemeClusters int) (deletedRunes int) { 806 e.initBuffer() 807 if graphemeClusters == 0 { 808 return 0 809 } 810 811 start, end := e.text.Selection() 812 if start != end { 813 graphemeClusters -= sign(graphemeClusters) 814 } 815 816 // Move caret by the target quantity of clusters. 817 e.text.MoveCaret(0, graphemeClusters) 818 // Get the new rune offsets of the selection. 819 start, end = e.text.Selection() 820 e.replace(start, end, "", true) 821 // Reset xoff. 822 e.text.MoveCaret(0, 0) 823 e.ClearSelection() 824 return end - start 825 } 826 827 func (e *Editor) Insert(s string) (insertedRunes int) { 828 e.initBuffer() 829 if e.SingleLine { 830 s = strings.ReplaceAll(s, "\n", " ") 831 } 832 start, end := e.text.Selection() 833 moves := e.replace(start, end, s, true) 834 if end < start { 835 start = end 836 } 837 // Reset xoff. 838 e.text.MoveCaret(0, 0) 839 e.SetCaret(start+moves, start+moves) 840 e.scrollCaret = true 841 return moves 842 } 843 844 // modification represents a change to the contents of the editor buffer. 845 // It contains the necessary information to both apply the change and 846 // reverse it, and is useful for implementing undo/redo. 847 type modification struct { 848 // StartRune is the inclusive index of the first rune 849 // modified. 850 StartRune int 851 // ApplyContent is the data inserted at StartRune to 852 // apply this operation. It overwrites len([]rune(ReverseContent)) runes. 853 ApplyContent string 854 // ReverseContent is the data inserted at StartRune to 855 // apply this operation. It overwrites len([]rune(ApplyContent)) runes. 856 ReverseContent string 857 } 858 859 // undo applies the modification at e.history[e.historyIdx] and decrements 860 // e.historyIdx. 861 func (e *Editor) undo() (EditorEvent, bool) { 862 e.initBuffer() 863 if len(e.history) < 1 || e.nextHistoryIdx == 0 { 864 return nil, false 865 } 866 mod := e.history[e.nextHistoryIdx-1] 867 replaceEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent) 868 e.replace(mod.StartRune, replaceEnd, mod.ReverseContent, false) 869 caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent) 870 e.SetCaret(caretEnd, mod.StartRune) 871 e.nextHistoryIdx-- 872 return ChangeEvent{}, true 873 } 874 875 // redo applies the modification at e.history[e.historyIdx] and increments 876 // e.historyIdx. 877 func (e *Editor) redo() (EditorEvent, bool) { 878 e.initBuffer() 879 if len(e.history) < 1 || e.nextHistoryIdx == len(e.history) { 880 return nil, false 881 } 882 mod := e.history[e.nextHistoryIdx] 883 end := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent) 884 e.replace(mod.StartRune, end, mod.ApplyContent, false) 885 caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent) 886 e.SetCaret(caretEnd, mod.StartRune) 887 e.nextHistoryIdx++ 888 return ChangeEvent{}, true 889 } 890 891 // replace the text between start and end with s. Indices are in runes. 892 // It returns the number of runes inserted. 893 // addHistory controls whether this modification is recorded in the undo 894 // history. replace can modify text in positions unrelated to the cursor 895 // position. 896 func (e *Editor) replace(start, end int, s string, addHistory bool) int { 897 length := e.text.Len() 898 if start > end { 899 start, end = end, start 900 } 901 start = min(start, length) 902 end = min(end, length) 903 replaceSize := end - start 904 el := e.Len() 905 var sc int 906 idx := 0 907 for idx < len(s) { 908 if e.MaxLen > 0 && el-replaceSize+sc >= e.MaxLen { 909 s = s[:idx] 910 break 911 } 912 _, n := utf8.DecodeRuneInString(s[idx:]) 913 if e.Filter != "" && !strings.Contains(e.Filter, s[idx:idx+n]) { 914 s = s[:idx] + s[idx+n:] 915 continue 916 } 917 idx += n 918 sc++ 919 } 920 921 if addHistory { 922 deleted := make([]rune, 0, replaceSize) 923 readPos := e.text.ByteOffset(start) 924 for i := 0; i < replaceSize; i++ { 925 ru, s, _ := e.text.ReadRuneAt(int64(readPos)) 926 readPos += int64(s) 927 deleted = append(deleted, ru) 928 } 929 if e.nextHistoryIdx < len(e.history) { 930 e.history = e.history[:e.nextHistoryIdx] 931 } 932 e.history = append(e.history, modification{ 933 StartRune: start, 934 ApplyContent: s, 935 ReverseContent: string(deleted), 936 }) 937 e.nextHistoryIdx++ 938 } 939 940 sc = e.text.Replace(start, end, s) 941 newEnd := start + sc 942 adjust := func(pos int) int { 943 switch { 944 case newEnd < pos && pos <= end: 945 pos = newEnd 946 case end < pos: 947 diff := newEnd - end 948 pos = pos + diff 949 } 950 return pos 951 } 952 e.ime.start = adjust(e.ime.start) 953 e.ime.end = adjust(e.ime.end) 954 return sc 955 } 956 957 // MoveCaret moves the caret (aka selection start) and the selection end 958 // relative to their current positions. Positive distances moves forward, 959 // negative distances moves backward. Distances are in grapheme clusters, 960 // which closely match what users perceive as "characters" even when the 961 // characters are multiple code points long. 962 func (e *Editor) MoveCaret(startDelta, endDelta int) { 963 e.initBuffer() 964 e.text.MoveCaret(startDelta, endDelta) 965 } 966 967 // deleteWord deletes the next word(s) in the specified direction. 968 // Unlike moveWord, deleteWord treats whitespace as a word itself. 969 // Positive is forward, negative is backward. 970 // Absolute values greater than one will delete that many words. 971 // The selection counts as a single word. 972 func (e *Editor) deleteWord(distance int) (deletedRunes int) { 973 if distance == 0 { 974 return 975 } 976 977 start, end := e.text.Selection() 978 if start != end { 979 deletedRunes = e.Delete(1) 980 distance -= sign(distance) 981 } 982 if distance == 0 { 983 return deletedRunes 984 } 985 986 // split the distance information into constituent parts to be 987 // used independently. 988 words, direction := distance, 1 989 if distance < 0 { 990 words, direction = distance*-1, -1 991 } 992 caret, _ := e.text.Selection() 993 // atEnd if offset is at or beyond either side of the buffer. 994 atEnd := func(runes int) bool { 995 idx := caret + runes*direction 996 return idx <= 0 || idx >= e.Len() 997 } 998 // next returns the appropriate rune given the direction and offset in runes). 999 next := func(runes int) rune { 1000 idx := caret + runes*direction 1001 if idx < 0 { 1002 idx = 0 1003 } else if idx > e.Len() { 1004 idx = e.Len() 1005 } 1006 off := e.text.ByteOffset(idx) 1007 var r rune 1008 if direction < 0 { 1009 r, _, _ = e.text.ReadRuneBefore(int64(off)) 1010 } else { 1011 r, _, _ = e.text.ReadRuneAt(int64(off)) 1012 } 1013 return r 1014 } 1015 runes := 1 1016 for ii := 0; ii < words; ii++ { 1017 r := next(runes) 1018 wantSpace := unicode.IsSpace(r) 1019 for r := next(runes); unicode.IsSpace(r) == wantSpace && !atEnd(runes); r = next(runes) { 1020 runes += 1 1021 } 1022 } 1023 deletedRunes += e.Delete(runes * direction) 1024 return deletedRunes 1025 } 1026 1027 // SelectionLen returns the length of the selection, in runes; it is 1028 // equivalent to utf8.RuneCountInString(e.SelectedText()). 1029 func (e *Editor) SelectionLen() int { 1030 e.initBuffer() 1031 return e.text.SelectionLen() 1032 } 1033 1034 // Selection returns the start and end of the selection, as rune offsets. 1035 // start can be > end. 1036 func (e *Editor) Selection() (start, end int) { 1037 e.initBuffer() 1038 return e.text.Selection() 1039 } 1040 1041 // SetCaret moves the caret to start, and sets the selection end to end. start 1042 // and end are in runes, and represent offsets into the editor text. 1043 func (e *Editor) SetCaret(start, end int) { 1044 e.initBuffer() 1045 e.text.SetCaret(start, end) 1046 e.scrollCaret = true 1047 e.scroller.Stop() 1048 } 1049 1050 // SelectedText returns the currently selected text (if any) from the editor. 1051 func (e *Editor) SelectedText() string { 1052 e.initBuffer() 1053 e.scratch = e.text.SelectedText(e.scratch) 1054 return string(e.scratch) 1055 } 1056 1057 // ClearSelection clears the selection, by setting the selection end equal to 1058 // the selection start. 1059 func (e *Editor) ClearSelection() { 1060 e.initBuffer() 1061 e.text.ClearSelection() 1062 } 1063 1064 // WriteTo implements io.WriterTo. 1065 func (e *Editor) WriteTo(w io.Writer) (int64, error) { 1066 e.initBuffer() 1067 return e.text.WriteTo(w) 1068 } 1069 1070 // Seek implements io.Seeker. 1071 func (e *Editor) Seek(offset int64, whence int) (int64, error) { 1072 e.initBuffer() 1073 return e.text.Seek(offset, whence) 1074 } 1075 1076 // Read implements io.Reader. 1077 func (e *Editor) Read(p []byte) (int, error) { 1078 e.initBuffer() 1079 return e.text.Read(p) 1080 } 1081 1082 // Regions returns visible regions covering the rune range [start,end). 1083 func (e *Editor) Regions(start, end int, regions []Region) []Region { 1084 e.initBuffer() 1085 return e.text.Regions(start, end, regions) 1086 } 1087 1088 func max(a, b int) int { 1089 if a > b { 1090 return a 1091 } 1092 return b 1093 } 1094 1095 func min(a, b int) int { 1096 if a < b { 1097 return a 1098 } 1099 return b 1100 } 1101 1102 func abs(n int) int { 1103 if n < 0 { 1104 return -n 1105 } 1106 return n 1107 } 1108 1109 func sign(n int) int { 1110 switch { 1111 case n < 0: 1112 return -1 1113 case n > 0: 1114 return 1 1115 default: 1116 return 0 1117 } 1118 } 1119 1120 func (s ChangeEvent) isEditorEvent() {} 1121 func (s SubmitEvent) isEditorEvent() {} 1122 func (s SelectEvent) isEditorEvent() {}