gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/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 "math" 10 "strings" 11 "time" 12 "unicode" 13 "unicode/utf8" 14 15 "gioui.org/f32" 16 "gioui.org/font" 17 "gioui.org/gesture" 18 "gioui.org/io/clipboard" 19 "gioui.org/io/event" 20 "gioui.org/io/key" 21 "gioui.org/io/pointer" 22 "gioui.org/io/semantic" 23 "gioui.org/io/system" 24 "gioui.org/io/transfer" 25 "gioui.org/layout" 26 "gioui.org/op" 27 "gioui.org/op/clip" 28 "gioui.org/text" 29 "gioui.org/unit" 30 ) 31 32 // Editor implements an editable and scrollable text area. 33 type Editor struct { 34 // text manages the text buffer and provides shaping and cursor positioning 35 // services. 36 text textView 37 // Alignment controls the alignment of text within the editor. 38 Alignment text.Alignment 39 // LineHeight determines the gap between baselines of text. If zero, a sensible 40 // default will be used. 41 LineHeight unit.Sp 42 // LineHeightScale is multiplied by LineHeight to determine the final gap 43 // between baselines. If zero, a sensible default will be used. 44 LineHeightScale float32 45 // SingleLine force the text to stay on a single line. 46 // SingleLine also sets the scrolling direction to 47 // horizontal. 48 SingleLine bool 49 // ReadOnly controls whether the contents of the editor can be altered by 50 // user interaction. If set to true, the editor will allow selecting text 51 // and copying it interactively, but not modifying it. 52 ReadOnly bool 53 // Submit enabled translation of carriage return keys to SubmitEvents. 54 // If not enabled, carriage returns are inserted as newlines in the text. 55 Submit bool 56 // Mask replaces the visual display of each rune in the contents with the given rune. 57 // Newline characters are not masked. When non-zero, the unmasked contents 58 // are accessed by Len, Text, and SetText. 59 Mask rune 60 // InputHint specifies the type of on-screen keyboard to be displayed. 61 InputHint key.InputHint 62 // MaxLen limits the editor content to a maximum length. Zero means no limit. 63 MaxLen int 64 // Filter is the list of characters allowed in the Editor. If Filter is empty, 65 // all characters are allowed. 66 Filter string 67 // WrapPolicy configures how displayed text will be broken into lines. 68 WrapPolicy text.WrapPolicy 69 70 buffer *editBuffer 71 // scratch is a byte buffer that is reused to efficiently read portions of text 72 // from the textView. 73 scratch []byte 74 blinkStart time.Time 75 76 // ime tracks the state relevant to input methods. 77 ime struct { 78 imeState 79 scratch []byte 80 } 81 82 dragging bool 83 dragger gesture.Drag 84 scroller gesture.Scroll 85 scrollCaret bool 86 showCaret bool 87 88 clicker gesture.Click 89 90 // history contains undo history. 91 history []modification 92 // nextHistoryIdx is the index within the history of the next modification. This 93 // is only not len(history) immediately after undo operations occur. It is framed as the "next" value 94 // to make the zero value consistent. 95 nextHistoryIdx int 96 97 pending []EditorEvent 98 } 99 100 type offEntry struct { 101 runes int 102 bytes int 103 } 104 105 type imeState struct { 106 selection struct { 107 rng key.Range 108 caret key.Caret 109 } 110 snippet key.Snippet 111 start, end int 112 } 113 114 type maskReader struct { 115 // rr is the underlying reader. 116 rr io.RuneReader 117 maskBuf [utf8.UTFMax]byte 118 // mask is the utf-8 encoded mask rune. 119 mask []byte 120 // overflow contains excess mask bytes left over after the last Read call. 121 overflow []byte 122 } 123 124 type selectionAction int 125 126 const ( 127 selectionExtend selectionAction = iota 128 selectionClear 129 ) 130 131 func (m *maskReader) Reset(r io.Reader, mr rune) { 132 m.rr = bufio.NewReader(r) 133 n := utf8.EncodeRune(m.maskBuf[:], mr) 134 m.mask = m.maskBuf[:n] 135 } 136 137 // Read reads from the underlying reader and replaces every 138 // rune with the mask rune. 139 func (m *maskReader) Read(b []byte) (n int, err error) { 140 for len(b) > 0 { 141 var replacement []byte 142 if len(m.overflow) > 0 { 143 replacement = m.overflow 144 } else { 145 var r rune 146 r, _, err = m.rr.ReadRune() 147 if err != nil { 148 break 149 } 150 if r == '\n' { 151 replacement = []byte{'\n'} 152 } else { 153 replacement = m.mask 154 } 155 } 156 nn := copy(b, replacement) 157 m.overflow = replacement[nn:] 158 n += nn 159 b = b[nn:] 160 } 161 return n, err 162 } 163 164 type EditorEvent interface { 165 isEditorEvent() 166 } 167 168 // A ChangeEvent is generated for every user change to the text. 169 type ChangeEvent struct{} 170 171 // A SubmitEvent is generated when Submit is set 172 // and a carriage return key is pressed. 173 type SubmitEvent struct { 174 Text string 175 } 176 177 // A SelectEvent is generated when the user selects some text, or changes the 178 // selection (e.g. with a shift-click), including if they remove the 179 // selection. The selected text is not part of the event, on the theory that 180 // it could be a relatively expensive operation (for a large editor), most 181 // applications won't actually care about it, and those that do can call 182 // Editor.SelectedText() (which can be empty). 183 type SelectEvent struct{} 184 185 const ( 186 blinksPerSecond = 1 187 maxBlinkDuration = 10 * time.Second 188 ) 189 190 func (e *Editor) processEvents(gtx layout.Context) (ev EditorEvent, ok bool) { 191 if len(e.pending) > 0 { 192 out := e.pending[0] 193 e.pending = e.pending[:copy(e.pending, e.pending[1:])] 194 return out, true 195 } 196 selStart, selEnd := e.Selection() 197 defer func() { 198 afterSelStart, afterSelEnd := e.Selection() 199 if selStart != afterSelStart || selEnd != afterSelEnd { 200 if ok { 201 e.pending = append(e.pending, SelectEvent{}) 202 } else { 203 ev = SelectEvent{} 204 ok = true 205 } 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 scrollX, scrollY pointer.ScrollRange 232 textDims := e.text.FullDimensions() 233 visibleDims := e.text.Dimensions() 234 if e.SingleLine { 235 scrollOffX := e.text.ScrollOff().X 236 scrollX.Min = min(-scrollOffX, 0) 237 scrollX.Max = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X)) 238 } else { 239 scrollOffY := e.text.ScrollOff().Y 240 scrollY.Min = -scrollOffY 241 scrollY.Max = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y)) 242 } 243 sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollX, scrollY) 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.MoveLineStart(selectionClear) 316 e.text.MoveLineEnd(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.ModShortcut | key.ModShift}, 378 key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShortcut | 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 case key.NameHome: 525 e.text.MoveTextStart(selAct) 526 case key.NameEnd: 527 e.text.MoveTextEnd(selAct) 528 } 529 return nil, false 530 } 531 switch k.Name { 532 case key.NameReturn, key.NameEnter: 533 if !e.ReadOnly { 534 if e.Insert("\n") != 0 { 535 return ChangeEvent{}, true 536 } 537 } 538 case key.NameDeleteBackward: 539 if !e.ReadOnly { 540 if moveByWord { 541 if e.deleteWord(-1) != 0 { 542 return ChangeEvent{}, true 543 } 544 } else { 545 if e.Delete(-1) != 0 { 546 return ChangeEvent{}, true 547 } 548 } 549 } 550 case key.NameDeleteForward: 551 if !e.ReadOnly { 552 if moveByWord { 553 if e.deleteWord(1) != 0 { 554 return ChangeEvent{}, true 555 } 556 } else { 557 if e.Delete(1) != 0 { 558 return ChangeEvent{}, true 559 } 560 } 561 } 562 case key.NameUpArrow: 563 e.text.MoveLines(-1, selAct) 564 case key.NameDownArrow: 565 e.text.MoveLines(+1, selAct) 566 case key.NameLeftArrow: 567 if moveByWord { 568 e.text.MoveWord(-1*direction, selAct) 569 } else { 570 if selAct == selectionClear { 571 e.text.ClearSelection() 572 } 573 e.text.MoveCaret(-1*direction, -1*direction*int(selAct)) 574 } 575 case key.NameRightArrow: 576 if moveByWord { 577 e.text.MoveWord(1*direction, selAct) 578 } else { 579 if selAct == selectionClear { 580 e.text.ClearSelection() 581 } 582 e.text.MoveCaret(1*direction, int(selAct)*direction) 583 } 584 case key.NamePageUp: 585 e.text.MovePages(-1, selAct) 586 case key.NamePageDown: 587 e.text.MovePages(+1, selAct) 588 case key.NameHome: 589 e.text.MoveLineStart(selAct) 590 case key.NameEnd: 591 e.text.MoveLineEnd(selAct) 592 } 593 return nil, false 594 } 595 596 // initBuffer should be invoked first in every exported function that accesses 597 // text state. It ensures that the underlying text widget is both ready to use 598 // and has its fields synced with the editor. 599 func (e *Editor) initBuffer() { 600 if e.buffer == nil { 601 e.buffer = new(editBuffer) 602 e.text.SetSource(e.buffer) 603 } 604 e.text.Alignment = e.Alignment 605 e.text.LineHeight = e.LineHeight 606 e.text.LineHeightScale = e.LineHeightScale 607 e.text.SingleLine = e.SingleLine 608 e.text.Mask = e.Mask 609 e.text.WrapPolicy = e.WrapPolicy 610 } 611 612 // Update the state of the editor in response to input events. Update consumes editor 613 // input events until there are no remaining events or an editor event is generated. 614 // To fully update the state of the editor, callers should call Update until it returns 615 // false. 616 func (e *Editor) Update(gtx layout.Context) (EditorEvent, bool) { 617 e.initBuffer() 618 event, ok := e.processEvents(gtx) 619 // Notify IME of selection if it changed. 620 newSel := e.ime.selection 621 start, end := e.text.Selection() 622 newSel.rng = key.Range{ 623 Start: start, 624 End: end, 625 } 626 caretPos, carAsc, carDesc := e.text.CaretInfo() 627 newSel.caret = key.Caret{ 628 Pos: layout.FPt(caretPos), 629 Ascent: float32(carAsc), 630 Descent: float32(carDesc), 631 } 632 if newSel != e.ime.selection { 633 e.ime.selection = newSel 634 gtx.Execute(key.SelectionCmd{Tag: e, Range: newSel.rng, Caret: newSel.caret}) 635 } 636 637 e.updateSnippet(gtx, e.ime.start, e.ime.end) 638 return event, ok 639 } 640 641 // Layout lays out the editor using the provided textMaterial as the paint material 642 // for the text glyphs+caret and the selectMaterial as the paint material for the 643 // selection rectangle. 644 func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions { 645 for { 646 _, ok := e.Update(gtx) 647 if !ok { 648 break 649 } 650 } 651 652 e.text.Layout(gtx, lt, font, size) 653 return e.layout(gtx, textMaterial, selectMaterial) 654 } 655 656 // updateSnippet queues a key.SnippetCmd if the snippet content or position 657 // have changed. off and len are in runes. 658 func (e *Editor) updateSnippet(gtx layout.Context, start, end int) { 659 if start > end { 660 start, end = end, start 661 } 662 length := e.text.Len() 663 if start > length { 664 start = length 665 } 666 if end > length { 667 end = length 668 } 669 e.ime.start = start 670 e.ime.end = end 671 startOff := e.text.ByteOffset(start) 672 endOff := e.text.ByteOffset(end) 673 n := endOff - startOff 674 if n > int64(len(e.ime.scratch)) { 675 e.ime.scratch = make([]byte, n) 676 } 677 scratch := e.ime.scratch[:n] 678 read, _ := e.text.ReadAt(scratch, startOff) 679 if read != len(scratch) { 680 panic("e.rr.Read truncated data") 681 } 682 newSnip := key.Snippet{ 683 Range: key.Range{ 684 Start: e.ime.start, 685 End: e.ime.end, 686 }, 687 Text: e.ime.snippet.Text, 688 } 689 if string(scratch) != newSnip.Text { 690 newSnip.Text = string(scratch) 691 } 692 if newSnip == e.ime.snippet { 693 return 694 } 695 e.ime.snippet = newSnip 696 gtx.Execute(key.SnippetCmd{Tag: e, Snippet: newSnip}) 697 } 698 699 func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.CallOp) layout.Dimensions { 700 // Adjust scrolling for new viewport and layout. 701 e.text.ScrollRel(0, 0) 702 703 if e.scrollCaret { 704 e.scrollCaret = false 705 e.text.ScrollToCaret() 706 } 707 visibleDims := e.text.Dimensions() 708 709 defer clip.Rect(image.Rectangle{Max: visibleDims.Size}).Push(gtx.Ops).Pop() 710 pointer.CursorText.Add(gtx.Ops) 711 event.Op(gtx.Ops, e) 712 key.InputHintOp{Tag: e, Hint: e.InputHint}.Add(gtx.Ops) 713 714 e.scroller.Add(gtx.Ops) 715 716 e.clicker.Add(gtx.Ops) 717 e.dragger.Add(gtx.Ops) 718 e.showCaret = false 719 if gtx.Focused(e) { 720 now := gtx.Now 721 dt := now.Sub(e.blinkStart) 722 blinking := dt < maxBlinkDuration 723 const timePerBlink = time.Second / blinksPerSecond 724 nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2)) 725 if blinking { 726 gtx.Execute(op.InvalidateCmd{At: nextBlink}) 727 } 728 e.showCaret = !blinking || dt%timePerBlink < timePerBlink/2 729 } 730 semantic.Editor.Add(gtx.Ops) 731 if e.Len() > 0 { 732 e.paintSelection(gtx, selectMaterial) 733 e.paintText(gtx, textMaterial) 734 } 735 if gtx.Enabled() { 736 e.paintCaret(gtx, textMaterial) 737 } 738 return visibleDims 739 } 740 741 // paintSelection paints the contrasting background for selected text using the provided 742 // material to set the painting material for the selection. 743 func (e *Editor) paintSelection(gtx layout.Context, material op.CallOp) { 744 e.initBuffer() 745 if !gtx.Focused(e) { 746 return 747 } 748 e.text.PaintSelection(gtx, material) 749 } 750 751 // paintText paints the text glyphs using the provided material to set the fill of the 752 // glyphs. 753 func (e *Editor) paintText(gtx layout.Context, material op.CallOp) { 754 e.initBuffer() 755 e.text.PaintText(gtx, material) 756 } 757 758 // paintCaret paints the text glyphs using the provided material to set the fill material 759 // of the caret rectangle. 760 func (e *Editor) paintCaret(gtx layout.Context, material op.CallOp) { 761 e.initBuffer() 762 if !e.showCaret || e.ReadOnly { 763 return 764 } 765 e.text.PaintCaret(gtx, material) 766 } 767 768 // Len is the length of the editor contents, in runes. 769 func (e *Editor) Len() int { 770 e.initBuffer() 771 return e.text.Len() 772 } 773 774 // Text returns the contents of the editor. 775 func (e *Editor) Text() string { 776 e.initBuffer() 777 e.scratch = e.text.Text(e.scratch) 778 return string(e.scratch) 779 } 780 781 func (e *Editor) SetText(s string) { 782 e.initBuffer() 783 if e.SingleLine { 784 s = strings.ReplaceAll(s, "\n", " ") 785 } 786 e.replace(0, e.text.Len(), s, true) 787 // Reset xoff and move the caret to the beginning. 788 e.SetCaret(0, 0) 789 } 790 791 // CaretPos returns the line & column numbers of the caret. 792 func (e *Editor) CaretPos() (line, col int) { 793 e.initBuffer() 794 return e.text.CaretPos() 795 } 796 797 // CaretCoords returns the coordinates of the caret, relative to the 798 // editor itself. 799 func (e *Editor) CaretCoords() f32.Point { 800 e.initBuffer() 801 return e.text.CaretCoords() 802 } 803 804 // Delete runes from the caret position. The sign of the argument specifies the 805 // direction to delete: positive is forward, negative is backward. 806 // 807 // If there is a selection, it is deleted and counts as a single grapheme 808 // cluster. 809 func (e *Editor) Delete(graphemeClusters int) (deletedRunes int) { 810 e.initBuffer() 811 if graphemeClusters == 0 { 812 return 0 813 } 814 815 start, end := e.text.Selection() 816 if start != end { 817 graphemeClusters -= sign(graphemeClusters) 818 } 819 820 // Move caret by the target quantity of clusters. 821 e.text.MoveCaret(0, graphemeClusters) 822 // Get the new rune offsets of the selection. 823 start, end = e.text.Selection() 824 e.replace(start, end, "", true) 825 // Reset xoff. 826 e.text.MoveCaret(0, 0) 827 e.ClearSelection() 828 return end - start 829 } 830 831 func (e *Editor) Insert(s string) (insertedRunes int) { 832 e.initBuffer() 833 if e.SingleLine { 834 s = strings.ReplaceAll(s, "\n", " ") 835 } 836 start, end := e.text.Selection() 837 moves := e.replace(start, end, s, true) 838 if end < start { 839 start = end 840 } 841 // Reset xoff. 842 e.text.MoveCaret(0, 0) 843 e.SetCaret(start+moves, start+moves) 844 e.scrollCaret = true 845 return moves 846 } 847 848 // modification represents a change to the contents of the editor buffer. 849 // It contains the necessary information to both apply the change and 850 // reverse it, and is useful for implementing undo/redo. 851 type modification struct { 852 // StartRune is the inclusive index of the first rune 853 // modified. 854 StartRune int 855 // ApplyContent is the data inserted at StartRune to 856 // apply this operation. It overwrites len([]rune(ReverseContent)) runes. 857 ApplyContent string 858 // ReverseContent is the data inserted at StartRune to 859 // apply this operation. It overwrites len([]rune(ApplyContent)) runes. 860 ReverseContent string 861 } 862 863 // undo applies the modification at e.history[e.historyIdx] and decrements 864 // e.historyIdx. 865 func (e *Editor) undo() (EditorEvent, bool) { 866 e.initBuffer() 867 if len(e.history) < 1 || e.nextHistoryIdx == 0 { 868 return nil, false 869 } 870 mod := e.history[e.nextHistoryIdx-1] 871 replaceEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent) 872 e.replace(mod.StartRune, replaceEnd, mod.ReverseContent, false) 873 caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent) 874 e.SetCaret(caretEnd, mod.StartRune) 875 e.nextHistoryIdx-- 876 return ChangeEvent{}, true 877 } 878 879 // redo applies the modification at e.history[e.historyIdx] and increments 880 // e.historyIdx. 881 func (e *Editor) redo() (EditorEvent, bool) { 882 e.initBuffer() 883 if len(e.history) < 1 || e.nextHistoryIdx == len(e.history) { 884 return nil, false 885 } 886 mod := e.history[e.nextHistoryIdx] 887 end := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent) 888 e.replace(mod.StartRune, end, mod.ApplyContent, false) 889 caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent) 890 e.SetCaret(caretEnd, mod.StartRune) 891 e.nextHistoryIdx++ 892 return ChangeEvent{}, true 893 } 894 895 // replace the text between start and end with s. Indices are in runes. 896 // It returns the number of runes inserted. 897 // addHistory controls whether this modification is recorded in the undo 898 // history. replace can modify text in positions unrelated to the cursor 899 // position. 900 func (e *Editor) replace(start, end int, s string, addHistory bool) int { 901 length := e.text.Len() 902 if start > end { 903 start, end = end, start 904 } 905 start = min(start, length) 906 end = min(end, length) 907 replaceSize := end - start 908 el := e.Len() 909 var sc int 910 idx := 0 911 for idx < len(s) { 912 if e.MaxLen > 0 && el-replaceSize+sc >= e.MaxLen { 913 s = s[:idx] 914 break 915 } 916 _, n := utf8.DecodeRuneInString(s[idx:]) 917 if e.Filter != "" && !strings.Contains(e.Filter, s[idx:idx+n]) { 918 s = s[:idx] + s[idx+n:] 919 continue 920 } 921 idx += n 922 sc++ 923 } 924 925 if addHistory { 926 deleted := make([]rune, 0, replaceSize) 927 readPos := e.text.ByteOffset(start) 928 for i := 0; i < replaceSize; i++ { 929 ru, s, _ := e.text.ReadRuneAt(int64(readPos)) 930 readPos += int64(s) 931 deleted = append(deleted, ru) 932 } 933 if e.nextHistoryIdx < len(e.history) { 934 e.history = e.history[:e.nextHistoryIdx] 935 } 936 e.history = append(e.history, modification{ 937 StartRune: start, 938 ApplyContent: s, 939 ReverseContent: string(deleted), 940 }) 941 e.nextHistoryIdx++ 942 } 943 944 sc = e.text.Replace(start, end, s) 945 newEnd := start + sc 946 adjust := func(pos int) int { 947 switch { 948 case newEnd < pos && pos <= end: 949 pos = newEnd 950 case end < pos: 951 diff := newEnd - end 952 pos = pos + diff 953 } 954 return pos 955 } 956 e.ime.start = adjust(e.ime.start) 957 e.ime.end = adjust(e.ime.end) 958 return sc 959 } 960 961 // MoveCaret moves the caret (aka selection start) and the selection end 962 // relative to their current positions. Positive distances moves forward, 963 // negative distances moves backward. Distances are in grapheme clusters, 964 // which closely match what users perceive as "characters" even when the 965 // characters are multiple code points long. 966 func (e *Editor) MoveCaret(startDelta, endDelta int) { 967 e.initBuffer() 968 e.text.MoveCaret(startDelta, endDelta) 969 } 970 971 // deleteWord deletes the next word(s) in the specified direction. 972 // Unlike moveWord, deleteWord treats whitespace as a word itself. 973 // Positive is forward, negative is backward. 974 // Absolute values greater than one will delete that many words. 975 // The selection counts as a single word. 976 func (e *Editor) deleteWord(distance int) (deletedRunes int) { 977 if distance == 0 { 978 return 979 } 980 981 start, end := e.text.Selection() 982 if start != end { 983 deletedRunes = e.Delete(1) 984 distance -= sign(distance) 985 } 986 if distance == 0 { 987 return deletedRunes 988 } 989 990 // split the distance information into constituent parts to be 991 // used independently. 992 words, direction := distance, 1 993 if distance < 0 { 994 words, direction = distance*-1, -1 995 } 996 caret, _ := e.text.Selection() 997 // atEnd if offset is at or beyond either side of the buffer. 998 atEnd := func(runes int) bool { 999 idx := caret + runes*direction 1000 return idx <= 0 || idx >= e.Len() 1001 } 1002 // next returns the appropriate rune given the direction and offset in runes). 1003 next := func(runes int) rune { 1004 idx := caret + runes*direction 1005 if idx < 0 { 1006 idx = 0 1007 } else if idx > e.Len() { 1008 idx = e.Len() 1009 } 1010 off := e.text.ByteOffset(idx) 1011 var r rune 1012 if direction < 0 { 1013 r, _, _ = e.text.ReadRuneBefore(int64(off)) 1014 } else { 1015 r, _, _ = e.text.ReadRuneAt(int64(off)) 1016 } 1017 return r 1018 } 1019 runes := 1 1020 for ii := 0; ii < words; ii++ { 1021 r := next(runes) 1022 wantSpace := unicode.IsSpace(r) 1023 for r := next(runes); unicode.IsSpace(r) == wantSpace && !atEnd(runes); r = next(runes) { 1024 runes += 1 1025 } 1026 } 1027 deletedRunes += e.Delete(runes * direction) 1028 return deletedRunes 1029 } 1030 1031 // SelectionLen returns the length of the selection, in runes; it is 1032 // equivalent to utf8.RuneCountInString(e.SelectedText()). 1033 func (e *Editor) SelectionLen() int { 1034 e.initBuffer() 1035 return e.text.SelectionLen() 1036 } 1037 1038 // Selection returns the start and end of the selection, as rune offsets. 1039 // start can be > end. 1040 func (e *Editor) Selection() (start, end int) { 1041 e.initBuffer() 1042 return e.text.Selection() 1043 } 1044 1045 // SetCaret moves the caret to start, and sets the selection end to end. start 1046 // and end are in runes, and represent offsets into the editor text. 1047 func (e *Editor) SetCaret(start, end int) { 1048 e.initBuffer() 1049 e.text.SetCaret(start, end) 1050 e.scrollCaret = true 1051 e.scroller.Stop() 1052 } 1053 1054 // SelectedText returns the currently selected text (if any) from the editor. 1055 func (e *Editor) SelectedText() string { 1056 e.initBuffer() 1057 e.scratch = e.text.SelectedText(e.scratch) 1058 return string(e.scratch) 1059 } 1060 1061 // ClearSelection clears the selection, by setting the selection end equal to 1062 // the selection start. 1063 func (e *Editor) ClearSelection() { 1064 e.initBuffer() 1065 e.text.ClearSelection() 1066 } 1067 1068 // WriteTo implements io.WriterTo. 1069 func (e *Editor) WriteTo(w io.Writer) (int64, error) { 1070 e.initBuffer() 1071 return e.text.WriteTo(w) 1072 } 1073 1074 // Seek implements io.Seeker. 1075 func (e *Editor) Seek(offset int64, whence int) (int64, error) { 1076 e.initBuffer() 1077 return e.text.Seek(offset, whence) 1078 } 1079 1080 // Read implements io.Reader. 1081 func (e *Editor) Read(p []byte) (int, error) { 1082 e.initBuffer() 1083 return e.text.Read(p) 1084 } 1085 1086 // Regions returns visible regions covering the rune range [start,end). 1087 func (e *Editor) Regions(start, end int, regions []Region) []Region { 1088 e.initBuffer() 1089 return e.text.Regions(start, end, regions) 1090 } 1091 1092 func max(a, b int) int { 1093 if a > b { 1094 return a 1095 } 1096 return b 1097 } 1098 1099 func min(a, b int) int { 1100 if a < b { 1101 return a 1102 } 1103 return b 1104 } 1105 1106 func abs(n int) int { 1107 if n < 0 { 1108 return -n 1109 } 1110 return n 1111 } 1112 1113 func sign(n int) int { 1114 switch { 1115 case n < 0: 1116 return -1 1117 case n > 0: 1118 return 1 1119 default: 1120 return 0 1121 } 1122 } 1123 1124 func (s ChangeEvent) isEditorEvent() {} 1125 func (s SubmitEvent) isEditorEvent() {} 1126 func (s SelectEvent) isEditorEvent() {}