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