textarea.go (79941B)
1 package tview 2 3 import ( 4 "math" 5 "strings" 6 "unicode" 7 "unicode/utf8" 8 9 "github.com/gdamore/tcell/v2" 10 "github.com/rivo/uniseg" 11 ) 12 13 const ( 14 // The minimum capacity of the text area's piece chain slice. 15 pieceChainMinCap = 10 16 17 // The minimum capacity of the text area's edit buffer. 18 editBufferMinCap = 200 19 20 // The maximum number of bytes making up a grapheme cluster. In theory, this 21 // could be longer but it would be highly unusual. 22 maxGraphemeClusterSize = 40 23 24 // The default value for the [TextArea.minCursorPrefix] variable. 25 minCursorPrefixDefault = 5 26 27 // The default value for the [TextArea.minCursorSuffix] variable. 28 minCursorSuffixDefault = 3 29 ) 30 31 // Types of user actions on a text area. 32 type taAction int 33 34 const ( 35 taActionOther taAction = iota 36 taActionTypeSpace // Typing a space character. 37 taActionTypeNonSpace // Typing a non-space character. 38 taActionBackspace // Deleting the previous character. 39 taActionDelete // Deleting the next character. 40 ) 41 42 // NewLine is the string sequence to be inserted when hitting the Enter key in a 43 // TextArea. The default is "\n" but you may change it to "\r\n" if required. 44 var NewLine = "\n" 45 46 // textAreaSpan represents a range of text in a text area. The text area widget 47 // roughly follows the concept of Piece Chains outlined in 48 // http://www.catch22.net/tuts/neatpad/piece-chains with some modifications. 49 // This type represents a "span" (or "piece") and thus refers to a subset of the 50 // text in the editor as part of a doubly-linked list. 51 // 52 // In most places where we reference a position in the text, we use a 53 // three-element int array. The first element is the index of the referenced 54 // span in the piece chain. The second element is the offset into the span's 55 // referenced text (relative to the span's start), its value is always >= 0 and 56 // < span.length. The third element is the state of the text parser at that 57 // position. 58 // 59 // A range of text is represented by a span range which is a starting position 60 // (3-int array) and an ending position (3-int array). The starting position 61 // references the first character of the range, the ending position references 62 // the position after the last character of the range. The end of the text is 63 // therefore always [3]int{1, 0, 0}, position 0 of the ending sentinel. 64 // 65 // Sentinel spans are dummy spans not referring to any text. There are always 66 // two sentinel spans: the starting span at index 0 of the [TextArea.spans] 67 // slice and the ending span at index 1. 68 type textAreaSpan struct { 69 // Links to the previous and next textAreaSpan objects as indices into the 70 // [TextArea.spans] slice. The sentinel spans (index 0 and 1) have -1 as 71 // their previous or next links, respectively. 72 previous, next int 73 74 // The start index and the length of the text segment this span represents. 75 // If "length" is negative, the span represents a substring of 76 // [TextArea.initialText] and the actual length is its absolute value. If it 77 // is positive, the span represents a substring of [TextArea.editText]. For 78 // the sentinel spans (index 0 and 1), both values will be 0. Others will 79 // never have a zero length. 80 offset, length int 81 } 82 83 // textAreaUndoItem represents an undoable edit to the text area. It describes 84 // the two spans wrapping a text change. 85 type textAreaUndoItem struct { 86 before, after int // The index of the copied "before" and "after" spans into the "spans" slice. 87 originalBefore, originalAfter int // The original indices of the "before" and "after" spans. 88 pos [3]int // The cursor position to be assumed after applying an undo. 89 length int // The total text length at the time the undo item was created. 90 continuation bool // If true, this item is a continuation of the previous undo item. It is handled together with all other undo items in the same continuation sequence. 91 } 92 93 // TextArea implements a simple text editor for multi-line text. Multi-color 94 // text is not supported. Word-wrapping is enabled by default but can be turned 95 // off or be changed to character-wrapping. 96 // 97 // # Navigation and Editing 98 // 99 // A text area is always in editing mode and no other mode exists. The following 100 // keys can be used to move the cursor (subject to what the user's terminal 101 // supports and how it is configured): 102 // 103 // - Left arrow: Move left. 104 // - Right arrow: Move right. 105 // - Down arrow: Move down. 106 // - Up arrow: Move up. 107 // - Ctrl-A, Home: Move to the beginning of the current line. 108 // - Ctrl-E, End: Move to the end of the current line. 109 // - Ctrl-F, page down: Move down by one page. 110 // - Ctrl-B, page up: Move up by one page. 111 // - Alt-Up arrow: Scroll the page up, leaving the cursor in its position. 112 // - Alt-Down arrow: Scroll the page down, leaving the cursor in its position. 113 // - Alt-Left arrow: Scroll the page to the left, leaving the cursor in its 114 // position. Ignored if wrapping is enabled. 115 // - Alt-Right arrow: Scroll the page to the right, leaving the cursor in its 116 // position. Ignored if wrapping is enabled. 117 // - Alt-B, Ctrl-Left arrow: Jump to the beginning of the current or previous 118 // word. 119 // - Alt-F, Ctrl-Right arrow: Jump to the end of the current or next word. 120 // 121 // Words are defined according to [Unicode Standard Annex #29]. We skip any 122 // words that contain only spaces or punctuation. 123 // 124 // Entering a character will insert it at the current cursor location. 125 // Subsequent characters are shifted accordingly. If the cursor is outside the 126 // visible area, any changes to the text will move it into the visible area. The 127 // following keys can also be used to modify the text: 128 // 129 // - Enter: Insert a newline character (see [NewLine]). 130 // - Tab: Insert a tab character (\t). It will be rendered like [TabSize] 131 // spaces. (This may eventually be changed to behave like regular tabs.) 132 // - Ctrl-H, Backspace: Delete one character to the left of the cursor. 133 // - Ctrl-D, Delete: Delete the character under the cursor (or the first 134 // character on the next line if the cursor is at the end of a line). 135 // - Alt-Backspace: Delete the word to the left of the cursor. 136 // - Ctrl-K: Delete everything under and to the right of the cursor until the 137 // next newline character. 138 // - Ctrl-W: Delete from the start of the current word to the left of the 139 // cursor. 140 // - Ctrl-U: Delete the current line, i.e. everything after the last newline 141 // character before the cursor up until the next newline character. This may 142 // span multiple visible rows if wrapping is enabled. 143 // 144 // Text can be selected by moving the cursor while holding the Shift key, to the 145 // extent that this is supported by the user's terminal. The Ctrl-L key can be 146 // used to select the entire text. (Ctrl-A already binds to the "Home" key.) 147 // 148 // When text is selected: 149 // 150 // - Entering a character will replace the selected text with the new 151 // character. 152 // - Backspace, delete, Ctrl-H, Ctrl-D: Delete the selected text. 153 // - Ctrl-Q: Copy the selected text into the clipboard, unselect the text. 154 // - Ctrl-X: Copy the selected text into the clipboard and delete it. 155 // - Ctrl-V: Replace the selected text with the clipboard text. If no text is 156 // selected, the clipboard text will be inserted at the cursor location. 157 // 158 // The Ctrl-Q key was chosen for the "copy" function because the Ctrl-C key is 159 // the default key to stop the application. If your application frees up the 160 // global Ctrl-C key and you want to bind it to the "copy to clipboard" 161 // function, you may use [Box.SetInputCapture] to override the Ctrl-Q key to 162 // implement copying to the clipboard. Note that using your terminal's / 163 // operating system's key bindings for copy+paste functionality may not have the 164 // expected effect as tview will not be able to handle these keys. Pasting text 165 // using your operating system's or terminal's own methods may be very slow as 166 // each character will be pasted individually. However, some terminals support 167 // pasting text blocks which is supported by the text area, see 168 // [Application.EnablePaste] for details. 169 // 170 // The default clipboard is an internal text buffer local to this text area 171 // instance, i.e. the operating system's clipboard is not used. If you want to 172 // implement your own clipboard (or make use of your operating system's 173 // clipboard), you can use [TextArea.SetClipboard] which provides all the 174 // functionality needed to implement your own clipboard. 175 // 176 // The text area also supports Undo: 177 // 178 // - Ctrl-Z: Undo the last change. 179 // - Ctrl-Y: Redo the last Undo change. 180 // 181 // Undo does not affect the clipboard. 182 // 183 // If the mouse is enabled, the following actions are available: 184 // 185 // - Left click: Move the cursor to the clicked position or to the end of the 186 // line if past the last character. 187 // - Left double-click: Select the word under the cursor. 188 // - Left click while holding the Shift key: Select text. 189 // - Scroll wheel: Scroll the text. 190 // 191 // [Unicode Standard Annex #29]: https://unicode.org/reports/tr29/ 192 type TextArea struct { 193 *Box 194 195 // Whether or not this text area is disabled/read-only. 196 disabled bool 197 198 // The size of the text area. If set to 0, the text area will use the entire 199 // available space. 200 width, height int 201 202 // The text to be shown in the text area when it is empty. 203 placeholder string 204 205 // The label text shown, usually when part of a form. 206 label string 207 208 // The width of the text area's label. 209 labelWidth int 210 211 // Styles: 212 213 // The label style. 214 labelStyle tcell.Style 215 216 // The style of the text. Background colors different from the Box's 217 // background color may lead to unwanted artefacts. 218 textStyle tcell.Style 219 220 // The style of the selected text. 221 selectedStyle tcell.Style 222 223 // The style of the placeholder text. 224 placeholderStyle tcell.Style 225 226 // Text manipulation related fields: 227 228 // The text area's text prior to any editing. It is referenced by spans with 229 // a negative length. 230 initialText string 231 232 // Any text that's been added by the user at some point. We only ever append 233 // to this buffer. It is referenced by spans with a positive length. 234 editText strings.Builder 235 236 // The total length of all text in the text area. 237 length int 238 239 // The maximum number of bytes allowed in the text area. If 0, there is no 240 // limit. 241 maxLength int 242 243 // The piece chain. The first two spans are sentinel spans which don't 244 // reference anything and always remain in the same place. Spans are never 245 // deleted from this slice. 246 spans []textAreaSpan 247 248 // An optional function which transforms grapheme clusters. This can be used 249 // to hide characters from the screen while preserving the original text. 250 transform func(cluster, rest string, boundaries int) (newCluster string, newBoundaries int) 251 252 // Display, navigation, and cursor related fields: 253 254 // If set to true, lines that are longer than the available width are 255 // wrapped onto the next line. If set to false, any characters beyond the 256 // available width are discarded. 257 wrap bool 258 259 // If set to true and if wrap is also true, lines are split at spaces or 260 // after punctuation characters. 261 wordWrap bool 262 263 // The index of the first line shown in the text area. 264 rowOffset int 265 266 // The number of cells to be skipped on each line (not used in wrap mode). 267 columnOffset int 268 269 // The inner height and width of the text area the last time it was drawn. 270 lastHeight, lastWidth int 271 272 // The width of the currently known widest line, as determined by 273 // [TextArea.extendLines]. 274 widestLine int 275 276 // Text positions and states of the start of lines. Each element is a span 277 // position (see [textAreaSpan]). Not all lines of the text may be contained 278 // at any time, extend as needed with the [TextArea.extendLines] function. 279 lineStarts [][3]int 280 281 // The cursor always points to the next position where a new character would 282 // be placed. The selection start is the same as the cursor as long as there 283 // is no selection. When there is one, the selection is between 284 // selectionStart and cursor. 285 cursor, selectionStart struct { 286 // The row and column in screen space but relative to the start of the 287 // text which may be outside the text area's box. The column value may 288 // be larger than where the cursor actually is if the line the cursor 289 // is on is shorter. The actualColumn is the position as it is seen on 290 // screen. These three values may not be determined yet, in which case 291 // the row is negative. 292 row, column, actualColumn int 293 294 // The textAreaSpan position with state for the actual next character. 295 pos [3]int 296 } 297 298 // The minimum width of text (if available) to be shown left of the cursor. 299 minCursorPrefix int 300 301 // The minimum width of text (if available) to be shown right of the cursor. 302 minCursorSuffix int 303 304 // Set to true when the mouse is dragging to select text. 305 dragging bool 306 307 // Clipboard related fields: 308 309 // The internal clipboard. 310 clipboard string 311 312 // The function to call when the user copies/cuts a text selection to the 313 // clipboard. 314 copyToClipboard func(string) 315 316 // The function to call when the user pastes text from the clipboard. 317 pasteFromClipboard func() string 318 319 // Undo/redo related fields: 320 321 // The last action performed by the user. 322 lastAction taAction 323 324 // The undo stack's items. Each item is a copy of the span before the 325 // modified span range and a copy of the span after the modified span range. 326 // To undo an action, the two referenced spans are put back into their 327 // original place. Undos and redos decrease or increase the nextUndo value. 328 // Thus, the next undo action is not always the last item. 329 undoStack []textAreaUndoItem 330 331 // The current undo/redo position on the undo stack. If no undo or redo has 332 // been performed yet, this is the same as len(undoStack). 333 nextUndo int 334 335 // Event handlers: 336 337 // An optional function which is called when the input has changed. 338 changed func() 339 340 // An optional function which is called when the position of the cursor or 341 // the selection has changed. 342 moved func() 343 344 // A callback function set by the Form class and called when the user leaves 345 // this form item. 346 finished func(tcell.Key) 347 } 348 349 // NewTextArea returns a new text area. Use [TextArea.SetText] to set the 350 // initial text. 351 func NewTextArea() *TextArea { 352 t := &TextArea{ 353 Box: NewBox(), 354 wrap: true, 355 wordWrap: true, 356 placeholderStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.TertiaryTextColor), 357 labelStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor), 358 textStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.PrimaryTextColor), 359 selectedStyle: tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.PrimitiveBackgroundColor), 360 spans: make([]textAreaSpan, 2, pieceChainMinCap), // We reserve some space to avoid reallocations right when editing starts. 361 lastAction: taActionOther, 362 minCursorPrefix: minCursorPrefixDefault, 363 minCursorSuffix: minCursorSuffixDefault, 364 lastWidth: math.MaxInt / 2, // We need this so some functions work before the first draw. 365 lastHeight: 1, 366 } 367 t.editText.Grow(editBufferMinCap) 368 t.spans[0] = textAreaSpan{previous: -1, next: 1} 369 t.spans[1] = textAreaSpan{previous: 0, next: -1} 370 t.cursor.pos = [3]int{1, 0, -1} 371 t.selectionStart = t.cursor 372 t.SetClipboard(nil, nil) 373 374 return t 375 } 376 377 // SetText sets the text of the text area. All existing text is deleted and 378 // replaced with the new text. Any edits are discarded, no undos are available. 379 // This function is typically only used to initialize the text area with a text 380 // after it has been created. To clear the text area's text (again, no undos), 381 // provide an empty string. 382 // 383 // If cursorAtTheEnd is false, the cursor is placed at the start of the text. If 384 // it is true, it is placed at the end of the text. For very long texts, placing 385 // the cursor at the end can be an expensive operation because the entire text 386 // needs to be parsed and laid out. 387 // 388 // If you want to set text and preserve undo functionality, use 389 // [TextArea.Replace] instead. 390 func (t *TextArea) SetText(text string, cursorAtTheEnd bool) *TextArea { 391 t.spans = t.spans[:2] 392 t.initialText = text 393 t.editText.Reset() 394 t.lineStarts = nil 395 t.length = len(text) 396 t.rowOffset = 0 397 t.columnOffset = 0 398 t.reset() 399 t.cursor.row, t.cursor.actualColumn, t.cursor.column = 0, 0, 0 400 t.cursor.pos = [3]int{1, 0, -1} 401 t.undoStack = t.undoStack[:0] 402 t.nextUndo = 0 403 404 if len(text) > 0 { 405 t.spans = append(t.spans, textAreaSpan{ 406 previous: 0, 407 next: 1, 408 offset: 0, 409 length: -len(text), 410 }) 411 t.spans[0].next = 2 412 t.spans[1].previous = 2 413 if cursorAtTheEnd { 414 t.cursor.row = -1 415 if t.lastWidth > 0 { 416 t.findCursor(true, 0) 417 } 418 } else { 419 t.cursor.pos = [3]int{2, 0, -1} 420 } 421 } else { 422 t.spans[0].next = 1 423 t.spans[1].previous = 0 424 } 425 t.selectionStart = t.cursor 426 427 if t.changed != nil { 428 t.changed() 429 } 430 431 if t.lastWidth > 0 && t.moved != nil { 432 t.moved() 433 } 434 435 return t 436 } 437 438 // GetText returns the entire text of the text area. Note that this will newly 439 // allocate the entire text. 440 func (t *TextArea) GetText() string { 441 if t.length == 0 { 442 return "" 443 } 444 445 var text strings.Builder 446 text.Grow(t.length) 447 spanIndex := t.spans[0].next 448 for spanIndex != 1 { 449 span := &t.spans[spanIndex] 450 if span.length < 0 { 451 text.WriteString(t.initialText[span.offset : span.offset-span.length]) 452 } else { 453 text.WriteString(t.editText.String()[span.offset : span.offset+span.length]) 454 } 455 spanIndex = t.spans[spanIndex].next 456 } 457 458 return text.String() 459 } 460 461 // getTextBeforeCursor returns the text of the text area up until the cursor. 462 // Note that this will result in a new allocation for the returned text. 463 func (t *TextArea) getTextBeforeCursor() string { 464 if t.length == 0 || t.cursor.pos[0] == t.spans[0].next && t.cursor.pos[1] == 0 { 465 return "" 466 } 467 468 var text strings.Builder 469 spanIndex := t.spans[0].next 470 for spanIndex != 1 { 471 span := &t.spans[spanIndex] 472 length := span.length 473 if length < 0 { 474 if t.cursor.pos[0] == spanIndex { 475 length = -t.cursor.pos[1] 476 } 477 text.WriteString(t.initialText[span.offset : span.offset-length]) 478 } else { 479 if t.cursor.pos[0] == spanIndex { 480 length = t.cursor.pos[1] 481 } 482 text.WriteString(t.editText.String()[span.offset : span.offset+length]) 483 } 484 if t.cursor.pos[0] == spanIndex { 485 break 486 } 487 spanIndex = t.spans[spanIndex].next 488 } 489 490 return text.String() 491 } 492 493 // getTextAfterCursor returns the text of the text area after the cursor. Note 494 // that this will result in a new allocation for the returned text. 495 func (t *TextArea) getTextAfterCursor() string { 496 if t.length == 0 || t.cursor.pos[0] == 1 { 497 return "" 498 } 499 500 var text strings.Builder 501 spanIndex := t.cursor.pos[0] 502 cursorOffset := t.cursor.pos[1] 503 for spanIndex != 1 { 504 span := &t.spans[spanIndex] 505 length := span.length 506 if length < 0 { 507 text.WriteString(t.initialText[span.offset+cursorOffset : span.offset-length]) 508 } else { 509 text.WriteString(t.editText.String()[span.offset+cursorOffset : span.offset+length]) 510 } 511 spanIndex = t.spans[spanIndex].next 512 cursorOffset = 0 513 } 514 515 return text.String() 516 } 517 518 // HasSelection returns whether the selected text is non-empty. 519 func (t *TextArea) HasSelection() bool { 520 return t.selectionStart != t.cursor 521 } 522 523 // GetSelection returns the currently selected text and its start and end 524 // positions within the entire text as a half-open interval. If the returned 525 // text is an empty string, the start and end positions are the same and can be 526 // interpreted as the cursor position. 527 // 528 // Calling this function will result in string allocations as well as a search 529 // for text positions. This is expensive if the text has been edited extensively 530 // already. Use [TextArea.HasSelection] first if you are only interested in 531 // selected text. 532 func (t *TextArea) GetSelection() (text string, start int, end int) { 533 from, to := t.selectionStart.pos, t.cursor.pos 534 if t.cursor.row < t.selectionStart.row || (t.cursor.row == t.selectionStart.row && t.cursor.actualColumn < t.selectionStart.actualColumn) { 535 from, to = to, from 536 } 537 538 if from[0] == 1 { 539 start = t.length 540 } 541 if to[0] == 1 { 542 end = t.length 543 } 544 545 var ( 546 index int 547 selection strings.Builder 548 inside bool 549 ) 550 for span := t.spans[0].next; span != 1; span = t.spans[span].next { 551 var spanText string 552 length := t.spans[span].length 553 if length < 0 { 554 length = -length 555 spanText = t.initialText 556 } else { 557 spanText = t.editText.String() 558 } 559 spanText = spanText[t.spans[span].offset : t.spans[span].offset+length] 560 561 if from[0] == span && to[0] == span { 562 if from != to { 563 selection.WriteString(spanText[from[1]:to[1]]) 564 } 565 start = index + from[1] 566 end = index + to[1] 567 break 568 } else if from[0] == span { 569 if from != to { 570 selection.WriteString(spanText[from[1]:]) 571 } 572 start = index + from[1] 573 inside = true 574 } else if to[0] == span { 575 if from != to { 576 selection.WriteString(spanText[:to[1]]) 577 } 578 end = index + to[1] 579 break 580 } else if inside && from != to { 581 selection.WriteString(spanText) 582 } 583 584 index += length 585 } 586 587 if selection.Len() != 0 { 588 text = selection.String() 589 } 590 return 591 } 592 593 // GetCursor returns the current cursor position where the first character of 594 // the entire text is in row 0, column 0. If the user has selected text, the 595 // "from" values will refer to the beginning of the selection and the "to" 596 // values to the end of the selection (exclusive). They are the same if there 597 // is no selection. 598 func (t *TextArea) GetCursor() (fromRow, fromColumn, toRow, toColumn int) { 599 fromRow, fromColumn = t.selectionStart.row, t.selectionStart.actualColumn 600 toRow, toColumn = t.cursor.row, t.cursor.actualColumn 601 if toRow < fromRow || (toRow == fromRow && toColumn < fromColumn) { 602 fromRow, fromColumn, toRow, toColumn = toRow, toColumn, fromRow, fromColumn 603 } 604 if t.length > 0 && t.wrap && fromColumn >= t.lastWidth { // This happens when a row has text all the way until the end, pushing the cursor outside the viewport. 605 fromRow++ 606 fromColumn = 0 607 } 608 if t.length > 0 && t.wrap && toColumn >= t.lastWidth { 609 toRow++ 610 toColumn = 0 611 } 612 return 613 } 614 615 // GetTextLength returns the string length of the text in the text area. 616 func (t *TextArea) GetTextLength() int { 617 return t.length 618 } 619 620 // Replace replaces a section of the text with new text. The start and end 621 // positions refer to index positions within the entire text string (as a 622 // half-open interval). They may be the same, in which case text is inserted at 623 // the given position. If the text is an empty string, text between start and 624 // end is deleted. Index positions will be shifted to line up with character 625 // boundaries. A "changed" event will be triggered. 626 // 627 // Previous selections are cleared. The cursor will be located at the end of the 628 // replaced text. Scroll offsets will not be changed. A "moved" event will be 629 // triggered. 630 // 631 // The effects of this function can be undone (and redone) by the user. 632 func (t *TextArea) Replace(start, end int, text string) *TextArea { 633 t.Select(start, end) 634 row := t.selectionStart.row 635 t.cursor.pos = t.replace(t.selectionStart.pos, t.cursor.pos, text, false) 636 t.cursor.row = -1 637 t.truncateLines(row - 1) 638 t.findCursor(false, row) 639 t.selectionStart = t.cursor 640 if t.moved != nil { 641 t.moved() 642 } 643 // The "changed" event will have been triggered by the "replace" function. 644 return t 645 } 646 647 // Select selects a section of the text. The start and end positions refer to 648 // index positions within the entire text string (as a half-open interval). They 649 // may be the same, in which case the cursor is placed at the given position. 650 // Any previous selection is removed. Scroll offsets will be preserved. 651 // 652 // Index positions will be shifted to line up with character boundaries. 653 func (t *TextArea) Select(start, end int) *TextArea { 654 oldFrom, oldTo := t.selectionStart, t.cursor 655 defer func() { 656 if (oldFrom != t.selectionStart || oldTo != t.cursor) && t.moved != nil { 657 t.moved() 658 } 659 }() 660 661 // Clamp input values. 662 if start < 0 { 663 start = 0 664 } 665 if start > t.length { 666 start = t.length 667 } 668 if end < 0 { 669 end = 0 670 } 671 if end > t.length { 672 end = t.length 673 } 674 if end < start { 675 start, end = end, start 676 } 677 678 // Find the cursor positions. 679 var row, index int 680 t.cursor.row, t.cursor.pos = -1, [3]int{1, 0, -1} 681 t.selectionStart = t.cursor 682 RowLoop: 683 for { 684 if row >= len(t.lineStarts) { 685 t.extendLines(t.lastWidth, row) 686 if row >= len(t.lineStarts) { 687 break 688 } 689 } 690 691 // Check the spans of this row. 692 pos := t.lineStarts[row] 693 var ( 694 next [3]int 695 lineIndex int 696 ) 697 if row+1 < len(t.lineStarts) { 698 next = t.lineStarts[row+1] 699 } else { 700 next = [3]int{1, 0, -1} 701 } 702 for { 703 if pos[0] == next[0] { 704 if start >= index+lineIndex && start < index+lineIndex+next[1]-pos[1] || 705 end >= index+lineIndex && end < index+lineIndex+next[1]-pos[1] || 706 next[0] == 1 && (start == t.length || end == t.length) { // Special case for the end of the text. 707 break 708 } 709 index += lineIndex + next[1] - pos[1] 710 row++ 711 continue RowLoop // Move on to the next row. 712 } else { 713 length := t.spans[pos[0]].length 714 if length < 0 { 715 length = -length 716 } 717 if start >= index+lineIndex && start < index+lineIndex+length-pos[1] || 718 end >= index+lineIndex && end < index+lineIndex+length-pos[1] || 719 next[0] == 1 && (start == t.length || end == t.length) { // Special case for the end of the text. 720 break 721 } 722 lineIndex += length - pos[1] 723 pos[0], pos[1] = t.spans[pos[0]].next, 0 724 } 725 } 726 727 // One of the indices is in this row. Step through it. 728 pos = t.lineStarts[row] 729 endPos := pos 730 var ( 731 cluster, text string 732 column, width int 733 ) 734 for pos != next { 735 if t.selectionStart.row < 0 && start <= index { 736 t.selectionStart.row, t.selectionStart.column, t.selectionStart.actualColumn = row, column, column 737 t.selectionStart.pos = pos 738 } 739 if t.cursor.row < 0 && end <= index { 740 t.cursor.row, t.cursor.column, t.cursor.actualColumn = row, column, column 741 t.cursor.pos = pos 742 break RowLoop 743 } 744 cluster, text, _, width, pos, endPos = t.step(text, pos, endPos) 745 index += len(cluster) 746 column += width 747 } 748 row++ 749 } 750 751 if t.cursor.row < 0 { 752 t.findCursor(false, 0) // This only happens if we couldn't find the locations above. 753 t.selectionStart = t.cursor 754 } 755 756 return t 757 } 758 759 // SetWrap sets the flag that, if true, leads to lines that are longer than the 760 // available width being wrapped onto the next line. If false, any characters 761 // beyond the available width are not displayed. 762 func (t *TextArea) SetWrap(wrap bool) *TextArea { 763 if t.wrap != wrap { 764 t.wrap = wrap 765 t.reset() 766 } 767 return t 768 } 769 770 // SetWordWrap sets the flag that causes lines that are longer than the 771 // available width to be wrapped onto the next line at spaces or after 772 // punctuation marks (according to [Unicode Standard Annex #14]). This flag is 773 // ignored if the flag set with [TextArea.SetWrap] is false. The text area's 774 // default is word-wrapping. 775 // 776 // [Unicode Standard Annex #14]: https://www.unicode.org/reports/tr14/ 777 func (t *TextArea) SetWordWrap(wrapOnWords bool) *TextArea { 778 if t.wordWrap != wrapOnWords { 779 t.wordWrap = wrapOnWords 780 t.reset() 781 } 782 return t 783 } 784 785 // SetPlaceholder sets the text to be displayed when the text area is empty. 786 func (t *TextArea) SetPlaceholder(placeholder string) *TextArea { 787 t.placeholder = placeholder 788 return t 789 } 790 791 // SetLabel sets the text to be displayed before the text area. 792 func (t *TextArea) SetLabel(label string) *TextArea { 793 t.label = label 794 return t 795 } 796 797 // GetLabel returns the text to be displayed before the text area. 798 func (t *TextArea) GetLabel() string { 799 return t.label 800 } 801 802 // SetLabelWidth sets the screen width of the label. A value of 0 will cause the 803 // primitive to use the width of the label string. 804 func (t *TextArea) SetLabelWidth(width int) *TextArea { 805 t.labelWidth = width 806 return t 807 } 808 809 // GetLabelWidth returns the screen width of the label. 810 func (t *TextArea) GetLabelWidth() int { 811 return t.labelWidth 812 } 813 814 // SetSize sets the screen size of the input element of the text area. The input 815 // element is always located next to the label which is always located in the 816 // top left corner. If any of the values are 0 or larger than the available 817 // space, the available space will be used. 818 func (t *TextArea) SetSize(rows, columns int) *TextArea { 819 t.width = columns 820 t.height = rows 821 return t 822 } 823 824 // GetFieldWidth returns this primitive's field width. 825 func (t *TextArea) GetFieldWidth() int { 826 return t.width 827 } 828 829 // GetFieldHeight returns this primitive's field height. 830 func (t *TextArea) GetFieldHeight() int { 831 return t.height 832 } 833 834 // SetDisabled sets whether or not the item is disabled / read-only. 835 func (t *TextArea) SetDisabled(disabled bool) FormItem { 836 t.disabled = disabled 837 if t.finished != nil { 838 t.finished(-1) 839 } 840 return t 841 } 842 843 // GetDisabled returns whether or not the item is disabled / read-only. 844 func (t *TextArea) GetDisabled() bool { 845 return t.disabled 846 } 847 848 // SetMaxLength sets the maximum number of bytes allowed in the text area. A 849 // value of 0 means there is no limit. If the text area currently contains more 850 // bytes than this, it may violate this constraint. 851 func (t *TextArea) SetMaxLength(maxLength int) *TextArea { 852 t.maxLength = maxLength 853 return t 854 } 855 856 // setMinCursorPadding sets a minimum width to be reserved left and right of the 857 // cursor. This is ignored if wrapping is enabled. 858 func (t *TextArea) setMinCursorPadding(prefix, suffix int) *TextArea { 859 t.minCursorPrefix = prefix 860 t.minCursorSuffix = suffix 861 return t 862 } 863 864 // SetLabelStyle sets the style of the label. 865 func (t *TextArea) SetLabelStyle(style tcell.Style) *TextArea { 866 t.labelStyle = style 867 return t 868 } 869 870 // GetLabelStyle returns the style of the label. 871 func (t *TextArea) GetLabelStyle() tcell.Style { 872 return t.labelStyle 873 } 874 875 // SetTextStyle sets the style of the text. 876 func (t *TextArea) SetTextStyle(style tcell.Style) *TextArea { 877 t.textStyle = style 878 return t 879 } 880 881 // GetTextStyle returns the style of the text. 882 func (t *TextArea) GetTextStyle() tcell.Style { 883 return t.textStyle 884 } 885 886 // SetSelectedStyle sets the style of the selected text. 887 func (t *TextArea) SetSelectedStyle(style tcell.Style) *TextArea { 888 t.selectedStyle = style 889 return t 890 } 891 892 // SetPlaceholderStyle sets the style of the placeholder text. 893 func (t *TextArea) SetPlaceholderStyle(style tcell.Style) *TextArea { 894 t.placeholderStyle = style 895 return t 896 } 897 898 // GetPlaceholderStyle returns the style of the placeholder text. 899 func (t *TextArea) GetPlaceholderStyle() tcell.Style { 900 return t.placeholderStyle 901 } 902 903 // GetOffset returns the text's offset, that is, the number of rows and columns 904 // skipped during drawing at the top or on the left, respectively. Note that the 905 // column offset is ignored if wrapping is enabled. 906 func (t *TextArea) GetOffset() (row, column int) { 907 return t.rowOffset, t.columnOffset 908 } 909 910 // SetOffset sets the text's offset, that is, the number of rows and columns 911 // skipped during drawing at the top or on the left, respectively. If wrapping 912 // is enabled, the column offset is ignored. These values may get adjusted 913 // automatically to ensure that some text is always visible. 914 func (t *TextArea) SetOffset(row, column int) *TextArea { 915 t.rowOffset, t.columnOffset = row, column 916 return t 917 } 918 919 // SetClipboard allows you to implement your own clipboard by providing a 920 // function that is called when the user wishes to store text in the clipboard 921 // (copyToClipboard) and a function that is called when the user wishes to 922 // retrieve text from the clipboard (pasteFromClipboard). 923 // 924 // Providing nil values will cause the default clipboard implementation to be 925 // used. Note that the default clipboard is local to this text area instance. 926 // Copying text to other widgets will not work. 927 func (t *TextArea) SetClipboard(copyToClipboard func(string), pasteFromClipboard func() string) *TextArea { 928 t.copyToClipboard = copyToClipboard 929 if t.copyToClipboard == nil { 930 t.copyToClipboard = func(text string) { 931 t.clipboard = text 932 } 933 } 934 935 t.pasteFromClipboard = pasteFromClipboard 936 if t.pasteFromClipboard == nil { 937 t.pasteFromClipboard = func() string { 938 return t.clipboard 939 } 940 } 941 942 return t 943 } 944 945 // GetClipboardText returns the current text of the clipboard by calling the 946 // pasteFromClipboard function set with [TextArea.SetClipboard]. 947 func (t *TextArea) GetClipboardText() string { 948 return t.pasteFromClipboard() 949 } 950 951 // SetChangedFunc sets a handler which is called whenever the text of the text 952 // area has changed. 953 func (t *TextArea) SetChangedFunc(handler func()) *TextArea { 954 t.changed = handler 955 return t 956 } 957 958 // SetMovedFunc sets a handler which is called whenever the cursor position or 959 // the text selection has changed. 960 func (t *TextArea) SetMovedFunc(handler func()) *TextArea { 961 t.moved = handler 962 return t 963 } 964 965 // SetFinishedFunc sets a callback invoked when the user leaves this form item. 966 func (t *TextArea) SetFinishedFunc(handler func(key tcell.Key)) FormItem { 967 t.finished = handler 968 return t 969 } 970 971 // Focus is called when this primitive receives focus. 972 func (t *TextArea) Focus(delegate func(p Primitive)) { 973 // If we're part of a form and this item is disabled, there's nothing the 974 // user can do here so we're finished. 975 if t.finished != nil && t.disabled { 976 t.finished(-1) 977 return 978 } 979 980 t.Box.Focus(delegate) 981 } 982 983 // SetFormAttributes sets attributes shared by all form items. 984 func (t *TextArea) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { 985 t.labelWidth = labelWidth 986 t.backgroundColor = bgColor 987 t.labelStyle = t.labelStyle.Foreground(labelColor) 988 t.textStyle = tcell.StyleDefault.Foreground(fieldTextColor).Background(fieldBgColor) 989 return t 990 } 991 992 // replace deletes a range of text and inserts the given text at that position. 993 // If the resulting text would exceed the maximum length, the function does not 994 // do anything. The function returns the end position of the deleted/inserted 995 // range. 996 // 997 // The function can hang if "deleteStart" is located after "deleteEnd". 998 // 999 // Undo events are always generated unless continuation is true and text is 1000 // either appended to the end of a span or a span is shortened at the beginning 1001 // or the end (and nothing else). 1002 // 1003 // This function only modifies [TextArea.lineStarts] to update span references 1004 // but does not change it to reflect the new layout. 1005 // 1006 // A "changed" event will be triggered. 1007 func (t *TextArea) replace(deleteStart, deleteEnd [3]int, insert string, continuation bool) [3]int { 1008 // Maybe nothing needs to be done? 1009 if deleteStart == deleteEnd && insert == "" || t.maxLength > 0 && len(insert) > 0 && t.length+len(insert) >= t.maxLength { 1010 return deleteEnd 1011 } 1012 1013 // Notify at the end. 1014 if t.changed != nil { 1015 defer t.changed() 1016 } 1017 1018 // Handle a few cases where we don't put anything onto the undo stack for 1019 // increased efficiency. 1020 if continuation { 1021 // Same action as the one before. An undo item was already generated for 1022 // this block of (same) actions. We're also only changing one character. 1023 switch { 1024 case insert == "" && deleteStart[1] != 0 && deleteEnd[1] == 0: 1025 // Simple backspace. Just shorten this span. 1026 length := t.spans[deleteStart[0]].length 1027 if length < 0 { 1028 t.length -= -length - deleteStart[1] 1029 length = -deleteStart[1] 1030 } else { 1031 t.length -= length - deleteStart[1] 1032 length = deleteStart[1] 1033 } 1034 t.spans[deleteStart[0]].length = length 1035 return deleteEnd 1036 case insert == "" && deleteStart[1] == 0 && deleteEnd[1] != 0: 1037 // Simple delete. Just clip the beginning of this span. 1038 t.spans[deleteEnd[0]].offset += deleteEnd[1] 1039 if t.spans[deleteEnd[0]].length < 0 { 1040 t.spans[deleteEnd[0]].length += deleteEnd[1] 1041 } else { 1042 t.spans[deleteEnd[0]].length -= deleteEnd[1] 1043 } 1044 t.length -= deleteEnd[1] 1045 deleteEnd[1] = 0 1046 return deleteEnd 1047 case insert != "" && deleteStart == deleteEnd && deleteEnd[1] == 0: 1048 previous := t.spans[deleteStart[0]].previous 1049 bufferSpan := t.spans[previous] 1050 if bufferSpan.length > 0 && bufferSpan.offset+bufferSpan.length == t.editText.Len() { 1051 // Typing individual characters. Simply extend the edit buffer. 1052 length, _ := t.editText.WriteString(insert) 1053 t.spans[previous].length += length 1054 t.length += length 1055 return deleteEnd 1056 } 1057 } 1058 } 1059 1060 // All other cases generate an undo item. 1061 before := t.spans[deleteStart[0]].previous 1062 after := deleteEnd[0] 1063 if deleteEnd[1] > 0 { 1064 after = t.spans[deleteEnd[0]].next 1065 } 1066 t.undoStack = t.undoStack[:t.nextUndo] 1067 t.undoStack = append(t.undoStack, textAreaUndoItem{ 1068 before: len(t.spans), 1069 after: len(t.spans) + 1, 1070 originalBefore: before, 1071 originalAfter: after, 1072 length: t.length, 1073 pos: t.cursor.pos, 1074 continuation: continuation, 1075 }) 1076 t.spans = append(t.spans, t.spans[before]) 1077 t.spans = append(t.spans, t.spans[after]) 1078 t.nextUndo++ 1079 1080 // Adjust total text length by subtracting everything between "before" and 1081 // "after". Inserted spans will be added back. 1082 for index := deleteStart[0]; index != after; index = t.spans[index].next { 1083 if t.spans[index].length < 0 { 1084 t.length += t.spans[index].length 1085 } else { 1086 t.length -= t.spans[index].length 1087 } 1088 } 1089 t.spans[before].next = after 1090 t.spans[after].previous = before 1091 1092 // We go from left to right, connecting new spans as needed. We update 1093 // "before" as the span to connect new spans to. 1094 1095 // If we start deleting in the middle of a span, connect a partial span. 1096 if deleteStart[1] != 0 { 1097 span := textAreaSpan{ 1098 previous: before, 1099 next: after, 1100 offset: t.spans[deleteStart[0]].offset, 1101 length: deleteStart[1], 1102 } 1103 if t.spans[deleteStart[0]].length < 0 { 1104 span.length = -span.length 1105 } 1106 t.length += deleteStart[1] // This was previously subtracted. 1107 t.spans[before].next = len(t.spans) 1108 t.spans[after].previous = len(t.spans) 1109 before = len(t.spans) 1110 for row, lineStart := range t.lineStarts { // Also redirect line starts until the end of this new span. 1111 if lineStart[0] == deleteStart[0] { 1112 if lineStart[1] >= deleteStart[1] { 1113 t.lineStarts = t.lineStarts[:row] // Everything else is unknown at this point. 1114 break 1115 } 1116 t.lineStarts[row][0] = len(t.spans) 1117 } 1118 } 1119 t.spans = append(t.spans, span) 1120 } 1121 1122 // If we insert text, connect a new span. 1123 if insert != "" { 1124 span := textAreaSpan{ 1125 previous: before, 1126 next: after, 1127 offset: t.editText.Len(), 1128 } 1129 span.length, _ = t.editText.WriteString(insert) 1130 t.length += span.length 1131 t.spans[before].next = len(t.spans) 1132 t.spans[after].previous = len(t.spans) 1133 before = len(t.spans) 1134 t.spans = append(t.spans, span) 1135 } 1136 1137 // If we stop deleting in the middle of a span, connect a partial span. 1138 if deleteEnd[1] != 0 { 1139 span := textAreaSpan{ 1140 previous: before, 1141 next: after, 1142 offset: t.spans[deleteEnd[0]].offset + deleteEnd[1], 1143 } 1144 length := t.spans[deleteEnd[0]].length 1145 if length < 0 { 1146 span.length = length + deleteEnd[1] 1147 t.length -= span.length // This was previously subtracted. 1148 } else { 1149 span.length = length - deleteEnd[1] 1150 t.length += span.length // This was previously subtracted. 1151 } 1152 t.spans[before].next = len(t.spans) 1153 t.spans[after].previous = len(t.spans) 1154 deleteEnd[0], deleteEnd[1] = len(t.spans), 0 1155 t.spans = append(t.spans, span) 1156 } 1157 1158 return deleteEnd 1159 } 1160 1161 // Draw draws this primitive onto the screen. 1162 func (t *TextArea) Draw(screen tcell.Screen) { 1163 t.Box.DrawForSubclass(screen, t) 1164 1165 // Prepare 1166 x, y, width, height := t.GetInnerRect() 1167 if width <= 0 || height <= 0 { 1168 return // We have no space for anything. 1169 } 1170 columnOffset := t.columnOffset 1171 if t.wrap { 1172 columnOffset = 0 1173 } 1174 1175 // Draw label. 1176 _, labelBg, _ := t.labelStyle.Decompose() 1177 if t.labelWidth > 0 { 1178 labelWidth := t.labelWidth 1179 if labelWidth > width { 1180 labelWidth = width 1181 } 1182 printWithStyle(screen, t.label, x, y, 0, labelWidth, AlignLeft, t.labelStyle, labelBg == tcell.ColorDefault) 1183 x += labelWidth 1184 width -= labelWidth 1185 } else { 1186 _, _, drawnWidth := printWithStyle(screen, t.label, x, y, 0, width, AlignLeft, t.labelStyle, labelBg == tcell.ColorDefault) 1187 x += drawnWidth 1188 width -= drawnWidth 1189 } 1190 1191 // What's the space for the input element? 1192 if t.width > 0 && t.width < width { 1193 width = t.width 1194 } 1195 if t.height > 0 && t.height < height { 1196 height = t.height 1197 } 1198 if width <= 0 { 1199 return // No space left for the text area. 1200 } 1201 1202 // Draw the input element if necessary. 1203 _, bg, _ := t.textStyle.Decompose() 1204 if t.disabled { 1205 bg = t.backgroundColor 1206 } 1207 if bg != t.backgroundColor { 1208 for row := 0; row < height; row++ { 1209 for column := 0; column < width; column++ { 1210 screen.SetContent(x+column, y+row, ' ', nil, t.textStyle) 1211 } 1212 } 1213 } 1214 1215 // Show/hide the cursor at the end. 1216 defer func() { 1217 if t.HasFocus() { 1218 row, column := t.cursor.row, t.cursor.actualColumn 1219 if t.length > 0 && t.wrap && column >= t.lastWidth { // This happens when a row has text all the way until the end, pushing the cursor outside the viewport. 1220 row++ 1221 column = 0 1222 } 1223 if row >= 0 && 1224 row-t.rowOffset >= 0 && row-t.rowOffset < height && 1225 column-columnOffset >= 0 && column-columnOffset < width { 1226 screen.ShowCursor(x+column-columnOffset, y+row-t.rowOffset) 1227 } else { 1228 screen.HideCursor() 1229 } 1230 } 1231 }() 1232 1233 // No text, show placeholder. 1234 if t.length == 0 { 1235 t.lastHeight, t.lastWidth = height, width 1236 t.cursor.row, t.cursor.column, t.cursor.actualColumn, t.cursor.pos = 0, 0, 0, [3]int{1, 0, -1} 1237 t.rowOffset, t.columnOffset = 0, 0 1238 if len(t.placeholder) > 0 { 1239 t.drawPlaceholder(screen, x, y, width, height) 1240 } 1241 return // We're done already. 1242 } 1243 1244 // Make sure the visible lines are broken over. 1245 firstDrawing := t.lastWidth == 0 1246 if t.lastWidth != width && t.lineStarts != nil { 1247 t.reset() 1248 } 1249 t.lastHeight, t.lastWidth = height, width 1250 t.extendLines(width, t.rowOffset+height) 1251 if len(t.lineStarts) <= t.rowOffset { 1252 return // It's scrolled out of view. 1253 } 1254 1255 // If the cursor position is unknown, find it. This usually only happens 1256 // before the screen is drawn for the first time. 1257 if t.cursor.row < 0 { 1258 t.findCursor(true, 0) 1259 if t.selectionStart.row < 0 { 1260 t.selectionStart = t.cursor 1261 } 1262 if firstDrawing && t.moved != nil { 1263 t.moved() 1264 } 1265 } 1266 1267 // Print the text. 1268 var cluster, text string 1269 line := t.rowOffset 1270 pos := t.lineStarts[line] 1271 endPos := pos 1272 posX, posY := 0, 0 1273 for pos[0] != 1 { 1274 var clusterWidth int 1275 cluster, text, _, clusterWidth, pos, endPos = t.step(text, pos, endPos) 1276 1277 // Prepare drawing. 1278 runes := []rune(cluster) 1279 style := t.selectedStyle 1280 fromRow, fromColumn := t.cursor.row, t.cursor.actualColumn 1281 toRow, toColumn := t.selectionStart.row, t.selectionStart.actualColumn 1282 if fromRow > toRow || fromRow == toRow && fromColumn > toColumn { 1283 fromRow, fromColumn, toRow, toColumn = toRow, toColumn, fromRow, fromColumn 1284 } 1285 if toRow < line || 1286 toRow == line && toColumn <= posX || 1287 fromRow > line || 1288 fromRow == line && fromColumn > posX { 1289 style = t.textStyle 1290 if t.disabled { 1291 style = style.Background(t.backgroundColor) 1292 } 1293 } 1294 1295 // Selected tabs are a bit special. 1296 if cluster == "\t" && style == t.selectedStyle { 1297 for colX := 0; colX < clusterWidth && posX+colX-columnOffset < width; colX++ { 1298 screen.SetContent(x+posX+colX-columnOffset, y+posY, ' ', nil, style) 1299 } 1300 } 1301 1302 // Draw character. 1303 if posX+clusterWidth-columnOffset <= width && posX-columnOffset >= 0 && clusterWidth > 0 { 1304 screen.SetContent(x+posX-columnOffset, y+posY, runes[0], runes[1:], style) 1305 } 1306 1307 // Advance. 1308 posX += clusterWidth 1309 if line+1 < len(t.lineStarts) && t.lineStarts[line+1] == pos { 1310 // We must break over. 1311 posY++ 1312 if posY >= height { 1313 break // Done. 1314 } 1315 posX = 0 1316 line++ 1317 } 1318 } 1319 } 1320 1321 // drawPlaceholder draws the placeholder text into the given rectangle. It does 1322 // not do anything if the text area already contains text or if there is no 1323 // placeholder text. 1324 func (t *TextArea) drawPlaceholder(screen tcell.Screen, x, y, width, height int) { 1325 // We use a TextView to draw the placeholder. It will take care of word 1326 // wrapping etc. 1327 textView := NewTextView(). 1328 SetText(t.placeholder). 1329 SetTextStyle(t.placeholderStyle) 1330 textView.SetRect(x, y, width, height) 1331 textView.Draw(screen) 1332 } 1333 1334 // reset resets many of the local variables of the text area because they cannot 1335 // be used anymore and must be recalculated, typically after the text area's 1336 // size has changed. 1337 func (t *TextArea) reset() { 1338 t.truncateLines(0) 1339 if t.wrap { 1340 t.cursor.row = -1 1341 t.selectionStart.row = -1 1342 } 1343 t.widestLine = 0 1344 } 1345 1346 // extendLines traverses the current text and extends [TextArea.lineStarts] such 1347 // that it describes at least maxLines+1 lines (or less if the text is shorter). 1348 // Text is laid out for the given width while respecting the wrapping settings. 1349 // It is assumed that if [TextArea.lineStarts] already has entries, they obey 1350 // the same rules. 1351 // 1352 // If width is 0, nothing happens. 1353 func (t *TextArea) extendLines(width, maxLines int) { 1354 if width <= 0 { 1355 return 1356 } 1357 1358 // Start with the first span. 1359 if len(t.lineStarts) == 0 { 1360 if len(t.spans) > 2 { 1361 t.lineStarts = append(t.lineStarts, [3]int{t.spans[0].next, 0, -1}) 1362 } else { 1363 return // No text. 1364 } 1365 } 1366 1367 // Determine starting positions and starting spans. 1368 pos := t.lineStarts[len(t.lineStarts)-1] // The starting position is the last known line. 1369 endPos := pos 1370 var ( 1371 cluster, text string 1372 lineWidth, clusterWidth, boundaries int 1373 lastGraphemeBreak, lastLineBreak [3]int 1374 widthSinceLineBreak int 1375 ) 1376 for pos[0] != 1 { 1377 // Get the next grapheme cluster. 1378 cluster, text, boundaries, clusterWidth, pos, endPos = t.step(text, pos, endPos) 1379 lineWidth += clusterWidth 1380 widthSinceLineBreak += clusterWidth 1381 1382 // Any line breaks? 1383 if !t.wrap || lineWidth <= width { 1384 if boundaries&uniseg.MaskLine == uniseg.LineMustBreak && (len(text) > 0 || uniseg.HasTrailingLineBreakInString(cluster)) { 1385 // We must break over. 1386 t.lineStarts = append(t.lineStarts, pos) 1387 if lineWidth > t.widestLine { 1388 t.widestLine = lineWidth 1389 } 1390 lineWidth = 0 1391 lastGraphemeBreak = [3]int{} 1392 lastLineBreak = [3]int{} 1393 widthSinceLineBreak = 0 1394 if len(t.lineStarts) > maxLines { 1395 break // We have enough lines, we can stop. 1396 } 1397 continue 1398 } 1399 } else { // t.wrap && lineWidth > width 1400 if !t.wordWrap || lastLineBreak == [3]int{} { 1401 if lastGraphemeBreak != [3]int{} { // We have at least one character on each line. 1402 // Break after last grapheme. 1403 t.lineStarts = append(t.lineStarts, lastGraphemeBreak) 1404 if lineWidth > t.widestLine { 1405 t.widestLine = lineWidth 1406 } 1407 lineWidth = clusterWidth 1408 lastLineBreak = [3]int{} 1409 } 1410 } else { // t.wordWrap && lastLineBreak != [3]int{} 1411 // Break after last line break opportunity. 1412 t.lineStarts = append(t.lineStarts, lastLineBreak) 1413 if lineWidth > t.widestLine { 1414 t.widestLine = lineWidth 1415 } 1416 lineWidth = widthSinceLineBreak 1417 lastLineBreak = [3]int{} 1418 } 1419 } 1420 1421 // Analyze break opportunities. 1422 if boundaries&uniseg.MaskLine == uniseg.LineCanBreak { 1423 lastLineBreak = pos 1424 widthSinceLineBreak = 0 1425 } 1426 lastGraphemeBreak = pos 1427 1428 // Can we stop? 1429 if len(t.lineStarts) > maxLines { 1430 break 1431 } 1432 } 1433 1434 if lineWidth > t.widestLine { 1435 t.widestLine = lineWidth 1436 } 1437 } 1438 1439 // truncateLines truncates the trailing lines of the [TextArea.lineStarts] 1440 // slice such that len(lineStarts) <= fromLine. If fromLine is negative, a value 1441 // of 0 is assumed. If it is greater than the length of lineStarts, nothing 1442 // happens. 1443 func (t *TextArea) truncateLines(fromLine int) { 1444 if fromLine < 0 { 1445 fromLine = 0 1446 } 1447 if fromLine < len(t.lineStarts) { 1448 t.lineStarts = t.lineStarts[:fromLine] 1449 } 1450 } 1451 1452 // findCursor determines the cursor position if its "row" value is < 0 1453 // (=unknown) but only its span position ("pos" value) is known. If the cursor 1454 // position is already known (row >= 0), it can also be used to modify row and 1455 // column offsets such that the cursor is visible during the next call to 1456 // [TextArea.Draw], by setting "clamp" to true. 1457 // 1458 // To determine the cursor position, "startRow" helps reduce processing time by 1459 // indicating the lowest row in which searching should start. Set this to 0 if 1460 // you don't have any information where the cursor might be (but know that this 1461 // is expensive for long texts). 1462 // 1463 // The cursor's desired column will be set to its actual column. 1464 func (t *TextArea) findCursor(clamp bool, startRow int) { 1465 defer func() { 1466 t.cursor.column = t.cursor.actualColumn 1467 }() 1468 1469 if !clamp && t.cursor.row >= 0 || t.lastWidth <= 0 { 1470 return // Nothing to do. 1471 } 1472 1473 // Clamp to viewport. 1474 if clamp && t.cursor.row >= 0 { 1475 cursorRow := t.cursor.row 1476 if t.wrap && t.cursor.actualColumn >= t.lastWidth { 1477 cursorRow++ // A row can push the cursor just outside the viewport. It will wrap onto the next line. 1478 } 1479 if cursorRow < t.rowOffset { 1480 // We're above the viewport. 1481 t.rowOffset = cursorRow 1482 } else if cursorRow >= t.rowOffset+t.lastHeight { 1483 // We're below the viewport. 1484 t.rowOffset = cursorRow - t.lastHeight + 1 1485 if t.rowOffset >= len(t.lineStarts) { 1486 t.extendLines(t.lastWidth, t.rowOffset) 1487 if t.rowOffset >= len(t.lineStarts) { 1488 t.rowOffset = len(t.lineStarts) - 1 1489 if t.rowOffset < 0 { 1490 t.rowOffset = 0 1491 } 1492 } 1493 } 1494 } 1495 if !t.wrap { 1496 if t.cursor.actualColumn < t.columnOffset+t.minCursorPrefix { 1497 // We're left of the viewport. 1498 t.columnOffset = t.cursor.actualColumn - t.minCursorPrefix 1499 if t.columnOffset < 0 { 1500 t.columnOffset = 0 1501 } 1502 } else if t.cursor.actualColumn >= t.columnOffset+t.lastWidth-t.minCursorSuffix { 1503 // We're right of the viewport. 1504 t.columnOffset = t.cursor.actualColumn - t.lastWidth + t.minCursorSuffix 1505 if t.columnOffset >= t.widestLine { 1506 t.columnOffset = t.widestLine - 1 1507 if t.columnOffset < 0 { 1508 t.columnOffset = 0 1509 } 1510 } 1511 } 1512 } 1513 return 1514 } 1515 1516 // The screen position of the cursor is unknown. Find it. This can be 1517 // expensive. First, find the row. 1518 row := startRow 1519 if row < 0 { 1520 row = 0 1521 } 1522 RowLoop: 1523 for { 1524 // Examine the current row. 1525 if row+1 >= len(t.lineStarts) { 1526 t.extendLines(t.lastWidth, row+1) 1527 } 1528 if row >= len(t.lineStarts) { 1529 t.cursor.row, t.cursor.actualColumn, t.cursor.pos = row, 0, [3]int{1, 0, -1} 1530 break // It's the end of the text. 1531 } 1532 1533 // Check this row's spans to see if the cursor is in this row. 1534 pos := t.lineStarts[row] 1535 for pos[0] != 1 { 1536 if row+1 >= len(t.lineStarts) { 1537 break // It's the last row so the cursor must be in this row. 1538 } 1539 if t.cursor.pos[0] == pos[0] { 1540 // The cursor is in this span. 1541 if t.lineStarts[row+1][0] == pos[0] { 1542 // The next row starts with the same span. 1543 if t.cursor.pos[1] >= t.lineStarts[row+1][1] { 1544 // The cursor is not in this row. 1545 row++ 1546 continue RowLoop 1547 } else { 1548 // The cursor is in this row. 1549 break 1550 } 1551 } else { 1552 // The next row starts with a different span. The cursor 1553 // must be in this row. 1554 break 1555 } 1556 } else { 1557 // The cursor is in a different span. 1558 if t.lineStarts[row+1][0] == pos[0] { 1559 // The next row starts with the same span. This row is 1560 // irrelevant. 1561 row++ 1562 continue RowLoop 1563 } else { 1564 // The next row starts with a different span. Move towards it. 1565 pos = [3]int{t.spans[pos[0]].next, 0, -1} 1566 } 1567 } 1568 } 1569 1570 // Try to find the screen position in this row. 1571 pos = t.lineStarts[row] 1572 endPos := pos 1573 column := 0 1574 var text string 1575 for { 1576 if pos[0] == 1 || t.cursor.pos[0] == pos[0] && t.cursor.pos[1] == pos[1] { 1577 // We found the position. We're done. 1578 t.cursor.row, t.cursor.actualColumn, t.cursor.pos = row, column, pos 1579 break RowLoop 1580 } 1581 var clusterWidth int 1582 _, text, _, clusterWidth, pos, endPos = t.step(text, pos, endPos) 1583 if row+1 < len(t.lineStarts) && t.lineStarts[row+1] == pos { 1584 // We reached the end of the line. Go to the next one. 1585 row++ 1586 continue RowLoop 1587 } 1588 column += clusterWidth 1589 } 1590 } 1591 1592 if clamp && t.cursor.row >= 0 { 1593 // We know the position now. Adapt offsets. 1594 t.findCursor(true, startRow) 1595 } 1596 } 1597 1598 // setTransform sets the transform function to be used when drawing the text. 1599 // This function is called for each grapheme cluster and can be used to modify 1600 // the cluster, the cluster's screen width, and the cluster's boundaries. The 1601 // function is called with the original cluster, the rest of the text, the 1602 // original cluster's width, and the original cluster's boundaries. The function 1603 // must return the new cluster, the new width, and the new boundaries. This only 1604 // affects the drawing of the text, not the text content itself. The boundaries 1605 // values correspond to the values returned by 1606 // [github.com/rivo/uniseg.StepString]. 1607 func (t *TextArea) setTransform(transform func(cluster, rest string, boundaries int) (newCluster string, newBoundaries int)) { 1608 t.transform = transform 1609 } 1610 1611 // step is similar to [github.com/rivo/uniseg.StepString] but it iterates over 1612 // the piece chain, starting with "pos", a span position plus state (which may 1613 // be -1 for the start of the text). The returned "boundaries" value is the same 1614 // value returned by [github.com/rivo/uniseg.StepString], "width" is the screen 1615 // width of the grapheme. The "pos" and "endPos" positions refer to the start 1616 // and the end of the "text" string, respectively. For the first call, text may 1617 // be empty and pos/endPos may be the same. For consecutive calls, provide 1618 // "rest" as the text and "newPos" and "newEndPos" as the new positions/states. 1619 // An empty "rest" string indicates the end of the text. The "endPos" state is 1620 // irrelevant. 1621 func (t *TextArea) step(text string, pos, endPos [3]int) (cluster, rest string, boundaries, width int, newPos, newEndPos [3]int) { 1622 if pos[0] == 1 { 1623 return // We're already past the end. 1624 } 1625 1626 // We want to make sure we have a text at least the size of a grapheme 1627 // cluster. 1628 span := t.spans[pos[0]] 1629 if len(text) < maxGraphemeClusterSize && 1630 (span.length < 0 && -span.length-pos[1] >= maxGraphemeClusterSize || 1631 span.length > 0 && t.spans[pos[0]].length-pos[1] >= maxGraphemeClusterSize) { 1632 // We can use a substring of one span. 1633 if span.length < 0 { 1634 text = t.initialText[span.offset+pos[1] : span.offset-span.length] 1635 } else { 1636 text = t.editText.String()[span.offset+pos[1] : span.offset+span.length] 1637 } 1638 endPos = [3]int{span.next, 0, -1} 1639 } else { 1640 // We have to compose the text from multiple spans. 1641 for len(text) < maxGraphemeClusterSize && endPos[0] != 1 { 1642 endSpan := t.spans[endPos[0]] 1643 var moreText string 1644 if endSpan.length < 0 { 1645 moreText = t.initialText[endSpan.offset+endPos[1] : endSpan.offset-endSpan.length] 1646 } else { 1647 moreText = t.editText.String()[endSpan.offset+endPos[1] : endSpan.offset+endSpan.length] 1648 } 1649 if len(moreText) > maxGraphemeClusterSize { 1650 moreText = moreText[:maxGraphemeClusterSize] 1651 } 1652 text += moreText 1653 endPos[1] += len(moreText) 1654 if endPos[1] >= endSpan.length { 1655 endPos[0], endPos[1] = endSpan.next, 0 1656 } 1657 } 1658 } 1659 1660 // Run the grapheme cluster iterator. 1661 cluster, text, boundaries, pos[2] = uniseg.StepString(text, pos[2]) 1662 pos[1] += len(cluster) 1663 for pos[0] != 1 && (span.length < 0 && pos[1] >= -span.length || span.length >= 0 && pos[1] >= span.length) { 1664 pos[0] = span.next 1665 if span.length < 0 { 1666 pos[1] += span.length 1667 } else { 1668 pos[1] -= span.length 1669 } 1670 span = t.spans[pos[0]] 1671 } 1672 1673 if t.transform != nil { 1674 cluster, boundaries = t.transform(cluster, text, boundaries) 1675 } 1676 1677 if cluster == "\t" { 1678 width = TabSize 1679 } else { 1680 width = boundaries >> uniseg.ShiftWidth 1681 } 1682 1683 return cluster, text, boundaries, width, pos, endPos 1684 } 1685 1686 // moveCursor sets the cursor's screen position and span position for the given 1687 // row and column which are screen space coordinates relative to the top-left 1688 // corner of the text area's full text (visible or not). The column value may be 1689 // negative, in which case, the cursor will be placed at the end of the line. 1690 // The cursor's actual position will be aligned with a grapheme cluster 1691 // boundary. The next call to [TextArea.Draw] will attempt to keep the cursor in 1692 // the viewport. 1693 func (t *TextArea) moveCursor(row, column int) { 1694 // Are we within the range of rows? 1695 if len(t.lineStarts) <= row { 1696 // No. Extent the line buffer. 1697 t.extendLines(t.lastWidth, row) 1698 } 1699 if len(t.lineStarts) == 0 { 1700 return // No lines. Nothing to do. 1701 } 1702 if row < 0 { 1703 // We're at the start of the text. 1704 row = 0 1705 column = 0 1706 } else if row >= len(t.lineStarts) { 1707 // We're already past the end. 1708 row = len(t.lineStarts) - 1 1709 column = -1 1710 } 1711 1712 // Iterate through this row until we find the position. 1713 t.cursor.row, t.cursor.actualColumn = row, 0 1714 if t.wrap { 1715 t.cursor.actualColumn = 0 1716 } 1717 pos := t.lineStarts[row] 1718 endPos := pos 1719 var text string 1720 for pos[0] != 1 { 1721 var clusterWidth int 1722 oldPos := pos // We may have to revert to this position. 1723 _, text, _, clusterWidth, pos, endPos = t.step(text, pos, endPos) 1724 if len(t.lineStarts) > row+1 && pos == t.lineStarts[row+1] || // We've reached the end of the line. 1725 column >= 0 && t.cursor.actualColumn+clusterWidth > column { // We're past the requested column. 1726 pos = oldPos 1727 break 1728 } 1729 t.cursor.actualColumn += clusterWidth 1730 } 1731 1732 if column < 0 { 1733 t.cursor.column = t.cursor.actualColumn 1734 } else { 1735 t.cursor.column = column 1736 } 1737 t.cursor.pos = pos 1738 t.findCursor(true, row) 1739 } 1740 1741 // moveWordRight moves the cursor to the end of the current or next word. If 1742 // after is set to true, the cursor will be placed after the word. If false, the 1743 // cursor will be placed on the last character of the word. If clamp is set to 1744 // true, the cursor will be visible during the next call to [TextArea.Draw]. 1745 func (t *TextArea) moveWordRight(after, clamp bool) { 1746 // Because we rely on clampToCursor to calculate the new screen position, 1747 // this is an expensive operation for large texts. 1748 pos := t.cursor.pos 1749 endPos := pos 1750 var ( 1751 cluster, text string 1752 inWord bool 1753 ) 1754 for pos[0] != 0 { 1755 var boundaries int 1756 oldPos := pos 1757 cluster, text, boundaries, _, pos, endPos = t.step(text, pos, endPos) 1758 if oldPos == t.cursor.pos { 1759 continue // Skip the first character. 1760 } 1761 firstRune, _ := utf8.DecodeRuneInString(cluster) 1762 if !unicode.IsSpace(firstRune) && !unicode.IsPunct(firstRune) { 1763 inWord = true 1764 } 1765 if inWord && boundaries&uniseg.MaskWord != 0 { 1766 if !after { 1767 pos = oldPos 1768 } 1769 break 1770 } 1771 } 1772 startRow := t.cursor.row 1773 t.cursor.row, t.cursor.column, t.cursor.actualColumn = -1, 0, 0 1774 t.cursor.pos = pos 1775 t.findCursor(clamp, startRow) 1776 } 1777 1778 // moveWordLeft moves the cursor to the beginning of the current or previous 1779 // word. If clamp is true, the cursor will be visible during the next call to 1780 // [TextArea.Draw]. 1781 func (t *TextArea) moveWordLeft(clamp bool) { 1782 // We go back row by row, trying to find the last word boundary before the 1783 // cursor. 1784 row := t.cursor.row 1785 if row+1 < len(t.lineStarts) { 1786 t.extendLines(t.lastWidth, row+1) 1787 } 1788 if row >= len(t.lineStarts) { 1789 row = len(t.lineStarts) - 1 1790 } 1791 for row >= 0 { 1792 pos := t.lineStarts[row] 1793 endPos := pos 1794 var lastWordBoundary [3]int 1795 var ( 1796 cluster, text string 1797 inWord bool 1798 boundaries int 1799 ) 1800 for pos[0] != 1 && pos != t.cursor.pos { 1801 oldBoundaries := boundaries 1802 oldPos := pos 1803 cluster, text, boundaries, _, pos, endPos = t.step(text, pos, endPos) 1804 firstRune, _ := utf8.DecodeRuneInString(cluster) 1805 wordRune := !unicode.IsSpace(firstRune) && !unicode.IsPunct(firstRune) 1806 if oldBoundaries&uniseg.MaskWord != 0 { 1807 if pos != t.cursor.pos && !inWord && wordRune { 1808 // A boundary transitioning from a space/punctuation word to 1809 // a letter word. 1810 lastWordBoundary = oldPos 1811 } 1812 inWord = false 1813 } 1814 if wordRune { 1815 inWord = true 1816 } 1817 } 1818 if lastWordBoundary[0] != 0 { 1819 // We found something. 1820 t.cursor.pos = lastWordBoundary 1821 break 1822 } 1823 row-- 1824 } 1825 if row < 0 { 1826 // We didn't find anything. We're at the start of the text. 1827 t.cursor.pos = [3]int{t.spans[0].next, 0, -1} 1828 row = 0 1829 } 1830 t.cursor.row, t.cursor.column, t.cursor.actualColumn = -1, 0, 0 1831 t.findCursor(clamp, row) 1832 } 1833 1834 // deleteLine deletes all characters between the last newline before the cursor 1835 // and the next newline after the cursor (inclusive). 1836 func (t *TextArea) deleteLine() { 1837 // We go back row by row, trying to find the last mandatory line break 1838 // before the cursor. 1839 startRow := t.cursor.row 1840 if t.cursor.actualColumn == 0 && t.cursor.pos[0] == 1 { 1841 startRow-- // If we're at the very end, delete the row before. 1842 } 1843 if startRow+1 < len(t.lineStarts) { 1844 t.extendLines(t.lastWidth, startRow+1) 1845 } 1846 if len(t.lineStarts) == 0 { 1847 return // Nothing to delete. 1848 } 1849 if startRow >= len(t.lineStarts) { 1850 startRow = len(t.lineStarts) - 1 1851 } 1852 for startRow >= 0 { 1853 // What's the last rune before the start of the line? 1854 pos := t.lineStarts[startRow] 1855 span := t.spans[pos[0]] 1856 var text string 1857 if pos[1] > 0 { 1858 // Extract text from this span. 1859 if span.length < 0 { 1860 text = t.initialText 1861 } else { 1862 text = t.editText.String() 1863 } 1864 text = text[:span.offset+pos[1]] 1865 } else { 1866 // Extract text from the previous span. 1867 if span.previous != 0 { 1868 span = t.spans[span.previous] 1869 if span.length < 0 { 1870 text = t.initialText[:span.offset-span.length] 1871 } else { 1872 text = t.editText.String()[:span.offset+span.length] 1873 } 1874 } 1875 } 1876 if uniseg.HasTrailingLineBreakInString(text) { 1877 // The row before this one ends with a mandatory line break. This is 1878 // the first line we will delete. 1879 break 1880 } 1881 startRow-- 1882 } 1883 if startRow < 0 { 1884 // We didn't find anything. It'll be the first line. 1885 startRow = 0 1886 } 1887 1888 // Find the next line break after the cursor. 1889 pos := t.cursor.pos 1890 endPos := pos 1891 var cluster, text string 1892 for pos[0] != 1 { 1893 cluster, text, _, _, pos, endPos = t.step(text, pos, endPos) 1894 if uniseg.HasTrailingLineBreakInString(cluster) { 1895 break 1896 } 1897 } 1898 1899 // Delete the text. 1900 t.cursor.pos = t.replace(t.lineStarts[startRow], pos, "", false) 1901 t.cursor.row = -1 1902 t.truncateLines(startRow) 1903 t.findCursor(true, startRow) 1904 } 1905 1906 // getSelection returns the current selection as span locations where the first 1907 // returned location is always before or the same as the second returned 1908 // location. This assumes that the cursor and selection positions are known. The 1909 // third return value is the starting row of the selection. 1910 func (t *TextArea) getSelection() ([3]int, [3]int, int) { 1911 from := t.selectionStart.pos 1912 to := t.cursor.pos 1913 row := t.selectionStart.row 1914 if t.cursor.row < t.selectionStart.row || 1915 (t.cursor.row == t.selectionStart.row && t.cursor.actualColumn < t.selectionStart.actualColumn) { 1916 from, to = to, from 1917 row = t.cursor.row 1918 } 1919 return from, to, row 1920 } 1921 1922 // getSelectedText returns the text of the current selection. 1923 func (t *TextArea) getSelectedText() string { 1924 var text strings.Builder 1925 1926 from, to, _ := t.getSelection() 1927 for from[0] != to[0] { 1928 span := t.spans[from[0]] 1929 if span.length < 0 { 1930 text.WriteString(t.initialText[span.offset+from[1] : span.offset-span.length]) 1931 } else { 1932 text.WriteString(t.editText.String()[span.offset+from[1] : span.offset+span.length]) 1933 } 1934 from[0], from[1] = span.next, 0 1935 } 1936 if from[0] != 1 && from[1] < to[1] { 1937 span := t.spans[from[0]] 1938 if span.length < 0 { 1939 text.WriteString(t.initialText[span.offset+from[1] : span.offset+to[1]]) 1940 } else { 1941 text.WriteString(t.editText.String()[span.offset+from[1] : span.offset+to[1]]) 1942 } 1943 } 1944 1945 return text.String() 1946 } 1947 1948 // InputHandler returns the handler for this primitive. 1949 func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { 1950 return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { 1951 if t.disabled { 1952 return 1953 } 1954 1955 // All actions except a few specific ones are "other" actions. 1956 newLastAction := taActionOther 1957 defer func() { 1958 t.lastAction = newLastAction 1959 }() 1960 1961 // Trigger a "moved" event if requested. 1962 if t.moved != nil { 1963 selectionStart, cursor := t.selectionStart, t.cursor 1964 defer func() { 1965 if selectionStart != t.selectionStart || cursor != t.cursor { 1966 t.moved() 1967 } 1968 }() 1969 } 1970 1971 // Process the different key events. 1972 switch key := event.Key(); key { 1973 case tcell.KeyLeft: // Move one grapheme cluster to the left. 1974 if event.Modifiers()&tcell.ModAlt == 0 { 1975 // Regular movement. 1976 if event.Modifiers()&tcell.ModShift == 0 && t.selectionStart.pos != t.cursor.pos { 1977 // Move to the start of the selection. 1978 if t.selectionStart.row < t.cursor.row || (t.selectionStart.row == t.cursor.row && t.selectionStart.actualColumn < t.cursor.actualColumn) { 1979 t.cursor = t.selectionStart 1980 } 1981 t.findCursor(true, t.cursor.row) 1982 } else if event.Modifiers()&tcell.ModMeta != 0 || event.Modifiers()&tcell.ModCtrl != 0 { 1983 // This captures Ctrl-Left on some systems. 1984 t.moveWordLeft(event.Modifiers()&tcell.ModShift != 0) 1985 } else if t.cursor.actualColumn == 0 { 1986 // Move to the end of the previous row. 1987 if t.cursor.row > 0 { 1988 t.moveCursor(t.cursor.row-1, -1) 1989 } 1990 } else { 1991 // Move one grapheme cluster to the left. 1992 t.moveCursor(t.cursor.row, t.cursor.actualColumn-1) 1993 } 1994 if event.Modifiers()&tcell.ModShift == 0 { 1995 t.selectionStart = t.cursor 1996 } 1997 } else if !t.wrap { // This doesn't work on all terminals. 1998 // Just scroll. 1999 t.columnOffset-- 2000 if t.columnOffset < 0 { 2001 t.columnOffset = 0 2002 } 2003 } 2004 case tcell.KeyRight: // Move one grapheme cluster to the right. 2005 if event.Modifiers()&tcell.ModAlt == 0 { 2006 // Regular movement. 2007 if event.Modifiers()&tcell.ModShift == 0 && t.selectionStart.pos != t.cursor.pos { 2008 // Move to the end of the selection. 2009 if t.selectionStart.row > t.cursor.row || (t.selectionStart.row == t.cursor.row && t.selectionStart.actualColumn > t.cursor.actualColumn) { 2010 t.cursor = t.selectionStart 2011 } 2012 t.findCursor(true, t.cursor.row) 2013 } else if t.cursor.pos[0] != 1 { 2014 if event.Modifiers()&tcell.ModMeta != 0 || event.Modifiers()&tcell.ModCtrl != 0 { 2015 // This captures Ctrl-Right on some systems. 2016 t.moveWordRight(event.Modifiers()&tcell.ModShift != 0, true) 2017 } else { 2018 // Move one grapheme cluster to the right. 2019 var clusterWidth int 2020 _, _, _, clusterWidth, t.cursor.pos, _ = t.step("", t.cursor.pos, t.cursor.pos) 2021 if len(t.lineStarts) <= t.cursor.row+1 { 2022 t.extendLines(t.lastWidth, t.cursor.row+1) 2023 } 2024 if t.cursor.row+1 < len(t.lineStarts) && t.lineStarts[t.cursor.row+1] == t.cursor.pos { 2025 // We've reached the end of the line. 2026 t.cursor.row++ 2027 t.cursor.actualColumn = 0 2028 t.cursor.column = 0 2029 t.findCursor(true, t.cursor.row) 2030 } else { 2031 // Move one character to the right. 2032 t.moveCursor(t.cursor.row, t.cursor.actualColumn+clusterWidth) 2033 } 2034 } 2035 } 2036 if event.Modifiers()&tcell.ModShift == 0 { 2037 t.selectionStart = t.cursor 2038 } 2039 } else if !t.wrap { // This doesn't work on all terminals. 2040 // Just scroll. 2041 t.columnOffset++ 2042 if t.columnOffset >= t.widestLine { 2043 t.columnOffset = t.widestLine - 1 2044 if t.columnOffset < 0 { 2045 t.columnOffset = 0 2046 } 2047 } 2048 } 2049 case tcell.KeyDown: // Move one row down. 2050 if event.Modifiers()&tcell.ModAlt == 0 { 2051 // Regular movement. 2052 column := t.cursor.column 2053 t.moveCursor(t.cursor.row+1, t.cursor.column) 2054 t.cursor.column = column 2055 if event.Modifiers()&tcell.ModShift == 0 { 2056 t.selectionStart = t.cursor 2057 } 2058 } else { 2059 // Just scroll. 2060 t.rowOffset++ 2061 if t.rowOffset >= len(t.lineStarts) { 2062 t.extendLines(t.lastWidth, t.rowOffset) 2063 if t.rowOffset >= len(t.lineStarts) { 2064 t.rowOffset = len(t.lineStarts) - 1 2065 if t.rowOffset < 0 { 2066 t.rowOffset = 0 2067 } 2068 } 2069 } 2070 } 2071 case tcell.KeyUp: // Move one row up. 2072 if event.Modifiers()&tcell.ModAlt == 0 { 2073 // Regular movement. 2074 column := t.cursor.column 2075 t.moveCursor(t.cursor.row-1, t.cursor.column) 2076 t.cursor.column = column 2077 if event.Modifiers()&tcell.ModShift == 0 { 2078 t.selectionStart = t.cursor 2079 } 2080 } else { 2081 // Just scroll. 2082 t.rowOffset-- 2083 if t.rowOffset < 0 { 2084 t.rowOffset = 0 2085 } 2086 } 2087 case tcell.KeyHome, tcell.KeyCtrlA: // Move to the start of the line. 2088 t.moveCursor(t.cursor.row, 0) 2089 if event.Modifiers()&tcell.ModShift == 0 { 2090 t.selectionStart = t.cursor 2091 } 2092 case tcell.KeyEnd, tcell.KeyCtrlE: // Move to the end of the line. 2093 t.moveCursor(t.cursor.row, -1) 2094 if event.Modifiers()&tcell.ModShift == 0 { 2095 t.selectionStart = t.cursor 2096 } 2097 case tcell.KeyPgDn, tcell.KeyCtrlF: // Move one page down. 2098 column := t.cursor.column 2099 t.moveCursor(t.cursor.row+t.lastHeight, t.cursor.column) 2100 t.cursor.column = column 2101 if event.Modifiers()&tcell.ModShift == 0 { 2102 t.selectionStart = t.cursor 2103 } 2104 case tcell.KeyPgUp, tcell.KeyCtrlB: // Move one page up. 2105 column := t.cursor.column 2106 t.moveCursor(t.cursor.row-t.lastHeight, t.cursor.column) 2107 t.cursor.column = column 2108 if event.Modifiers()&tcell.ModShift == 0 { 2109 t.selectionStart = t.cursor 2110 } 2111 case tcell.KeyEnter: // Insert a newline. 2112 from, to, row := t.getSelection() 2113 t.cursor.pos = t.replace(from, to, NewLine, t.lastAction == taActionTypeSpace) 2114 t.cursor.row = -1 2115 t.truncateLines(row - 1) 2116 t.findCursor(true, row) 2117 t.selectionStart = t.cursor 2118 newLastAction = taActionTypeSpace 2119 case tcell.KeyTab: // Insert a tab character. It will be rendered as TabSize spaces. 2120 // But forwarding takes precedence. 2121 if t.finished != nil { 2122 t.finished(key) 2123 return 2124 } 2125 2126 from, to, row := t.getSelection() 2127 t.cursor.pos = t.replace(from, to, "\t", t.lastAction == taActionTypeSpace) 2128 t.cursor.row = -1 2129 t.truncateLines(row - 1) 2130 t.findCursor(true, row) 2131 t.selectionStart = t.cursor 2132 newLastAction = taActionTypeSpace 2133 case tcell.KeyBacktab, tcell.KeyEscape: // Only used in forms. 2134 if t.finished != nil { 2135 t.finished(key) 2136 return 2137 } 2138 case tcell.KeyRune: 2139 if event.Modifiers()&tcell.ModAlt > 0 { 2140 // We accept some Alt- key combinations. 2141 switch event.Rune() { 2142 case 'f': 2143 if event.Modifiers()&tcell.ModShift == 0 { 2144 t.moveWordRight(false, true) 2145 t.selectionStart = t.cursor 2146 } else { 2147 t.moveWordRight(true, true) 2148 } 2149 case 'b': 2150 t.moveWordLeft(true) 2151 if event.Modifiers()&tcell.ModShift == 0 { 2152 t.selectionStart = t.cursor 2153 } 2154 } 2155 } else { 2156 // Other keys are simply accepted as regular characters. 2157 r := event.Rune() 2158 from, to, row := t.getSelection() 2159 newLastAction = taActionTypeNonSpace 2160 if unicode.IsSpace(r) { 2161 newLastAction = taActionTypeSpace 2162 } 2163 t.cursor.pos = t.replace(from, to, string(r), newLastAction == t.lastAction || t.lastAction == taActionTypeNonSpace && newLastAction == taActionTypeSpace) 2164 t.cursor.row = -1 2165 t.truncateLines(row - 1) 2166 t.findCursor(true, row) 2167 t.selectionStart = t.cursor 2168 } 2169 case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete backwards. tcell.KeyBackspace is the same as tcell.CtrlH. 2170 from, to, row := t.getSelection() 2171 if from != to { 2172 // Simply delete the current selection. 2173 t.cursor.pos = t.replace(from, to, "", false) 2174 t.cursor.row = -1 2175 t.truncateLines(row - 1) 2176 t.findCursor(true, row) 2177 t.selectionStart = t.cursor 2178 break 2179 } 2180 2181 beforeCursor := t.cursor 2182 if event.Modifiers()&tcell.ModAlt == 0 { 2183 // Move the cursor back by one grapheme cluster. 2184 if t.cursor.actualColumn == 0 { 2185 // Move to the end of the previous row. 2186 if t.cursor.row > 0 { 2187 t.moveCursor(t.cursor.row-1, -1) 2188 } 2189 } else { 2190 // Move one grapheme cluster to the left. 2191 t.moveCursor(t.cursor.row, t.cursor.actualColumn-1) 2192 } 2193 newLastAction = taActionBackspace 2194 } else { 2195 // Move the cursor back by one word. 2196 t.moveWordLeft(false) 2197 } 2198 2199 // Remove that last grapheme cluster. 2200 if t.cursor.pos != beforeCursor.pos { 2201 t.cursor, beforeCursor = beforeCursor, t.cursor // So we put the right position on the stack. 2202 t.cursor.pos = t.replace(beforeCursor.pos, t.cursor.pos, "", t.lastAction == taActionBackspace) // Delete the character. 2203 t.cursor.row = -1 2204 t.truncateLines(beforeCursor.row - 1) 2205 t.findCursor(true, beforeCursor.row-1) 2206 } 2207 t.selectionStart = t.cursor 2208 case tcell.KeyDelete, tcell.KeyCtrlD: // Delete forward. 2209 from, to, row := t.getSelection() 2210 if from != to { 2211 // Simply delete the current selection. 2212 t.cursor.pos = t.replace(from, to, "", false) 2213 t.cursor.row = -1 2214 t.truncateLines(row - 1) 2215 t.findCursor(true, row) 2216 t.selectionStart = t.cursor 2217 break 2218 } 2219 2220 if t.cursor.pos[0] != 1 { 2221 _, _, _, _, endPos, _ := t.step("", t.cursor.pos, t.cursor.pos) 2222 t.cursor.pos = t.replace(t.cursor.pos, endPos, "", t.lastAction == taActionDelete) // Delete the character. 2223 t.cursor.pos[2] = endPos[2] 2224 t.truncateLines(t.cursor.row - 1) 2225 t.findCursor(true, t.cursor.row) 2226 newLastAction = taActionDelete 2227 } 2228 t.selectionStart = t.cursor 2229 case tcell.KeyCtrlK: // Delete everything under and to the right of the cursor until before the next newline character. 2230 pos := t.cursor.pos 2231 endPos := pos 2232 var cluster, text string 2233 for pos[0] != 1 { 2234 var boundaries int 2235 oldPos := pos 2236 cluster, text, boundaries, _, pos, endPos = t.step(text, pos, endPos) 2237 if boundaries&uniseg.MaskLine == uniseg.LineMustBreak { 2238 if uniseg.HasTrailingLineBreakInString(cluster) { 2239 pos = oldPos 2240 } 2241 break 2242 } 2243 } 2244 t.cursor.pos = t.replace(t.cursor.pos, pos, "", false) 2245 row := t.cursor.row 2246 t.cursor.row = -1 2247 t.truncateLines(row - 1) 2248 t.findCursor(true, row) 2249 t.selectionStart = t.cursor 2250 case tcell.KeyCtrlW: // Delete from the start of the current word to the left of the cursor. 2251 pos := t.cursor.pos 2252 t.moveWordLeft(true) 2253 t.cursor.pos = t.replace(t.cursor.pos, pos, "", false) 2254 row := t.cursor.row - 1 2255 t.cursor.row = -1 2256 t.truncateLines(row) 2257 t.findCursor(true, row) 2258 t.selectionStart = t.cursor 2259 case tcell.KeyCtrlU: // Delete the current line. 2260 t.deleteLine() 2261 t.selectionStart = t.cursor 2262 case tcell.KeyCtrlL: // Select everything. 2263 t.selectionStart.row, t.selectionStart.column, t.selectionStart.actualColumn = 0, 0, 0 2264 t.selectionStart.pos = [3]int{t.spans[0].next, 0, -1} 2265 row := t.cursor.row 2266 t.cursor.row = -1 2267 t.cursor.pos = [3]int{1, 0, -1} 2268 t.findCursor(false, row) 2269 case tcell.KeyCtrlQ: // Copy to clipboard. 2270 if t.cursor != t.selectionStart { 2271 t.copyToClipboard(t.getSelectedText()) 2272 t.selectionStart = t.cursor 2273 } 2274 case tcell.KeyCtrlX: // Cut to clipboard. 2275 if t.cursor != t.selectionStart { 2276 t.copyToClipboard(t.getSelectedText()) 2277 from, to, row := t.getSelection() 2278 t.cursor.pos = t.replace(from, to, "", false) 2279 t.cursor.row = -1 2280 t.truncateLines(row - 1) 2281 t.findCursor(true, row) 2282 t.selectionStart = t.cursor 2283 } 2284 case tcell.KeyCtrlV: // Paste from clipboard. 2285 from, to, row := t.getSelection() 2286 t.cursor.pos = t.replace(from, to, t.pasteFromClipboard(), false) 2287 t.cursor.row = -1 2288 t.truncateLines(row - 1) 2289 t.findCursor(true, row) 2290 t.selectionStart = t.cursor 2291 case tcell.KeyCtrlZ: // Undo. 2292 if t.nextUndo <= 0 { 2293 break 2294 } 2295 for t.nextUndo > 0 { 2296 t.nextUndo-- 2297 undo := t.undoStack[t.nextUndo] 2298 t.spans[undo.originalBefore], t.spans[undo.before] = t.spans[undo.before], t.spans[undo.originalBefore] 2299 t.spans[undo.originalAfter], t.spans[undo.after] = t.spans[undo.after], t.spans[undo.originalAfter] 2300 t.cursor.pos, t.undoStack[t.nextUndo].pos = undo.pos, t.cursor.pos 2301 t.length, t.undoStack[t.nextUndo].length = undo.length, t.length 2302 if !undo.continuation { 2303 break 2304 } 2305 } 2306 t.cursor.row = -1 2307 t.truncateLines(0) // This is why Undo is expensive for large texts. (t.lineStarts can get largely unusable after an undo.) 2308 t.findCursor(true, 0) 2309 t.selectionStart = t.cursor 2310 if t.changed != nil { 2311 defer t.changed() 2312 } 2313 case tcell.KeyCtrlY: // Redo. 2314 if t.nextUndo >= len(t.undoStack) { 2315 break 2316 } 2317 for t.nextUndo < len(t.undoStack) { 2318 undo := t.undoStack[t.nextUndo] 2319 t.spans[undo.originalBefore], t.spans[undo.before] = t.spans[undo.before], t.spans[undo.originalBefore] 2320 t.spans[undo.originalAfter], t.spans[undo.after] = t.spans[undo.after], t.spans[undo.originalAfter] 2321 t.cursor.pos, t.undoStack[t.nextUndo].pos = undo.pos, t.cursor.pos 2322 t.length, t.undoStack[t.nextUndo].length = undo.length, t.length 2323 t.nextUndo++ 2324 if t.nextUndo < len(t.undoStack) && !t.undoStack[t.nextUndo].continuation { 2325 break 2326 } 2327 } 2328 t.cursor.row = -1 2329 t.truncateLines(0) // This is why Redo is expensive for large texts. (t.lineStarts can get largely unusable after an undo.) 2330 t.findCursor(true, 0) 2331 t.selectionStart = t.cursor 2332 if t.changed != nil { 2333 defer t.changed() 2334 } 2335 } 2336 }) 2337 } 2338 2339 // MouseHandler returns the mouse handler for this primitive. 2340 func (t *TextArea) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { 2341 return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { 2342 if t.disabled { 2343 return false, nil 2344 } 2345 2346 x, y := event.Position() 2347 rectX, rectY, _, _ := t.GetInnerRect() 2348 if !t.InRect(x, y) { 2349 return false, nil 2350 } 2351 2352 // Trigger a "moved" event at the end if requested. 2353 if t.moved != nil { 2354 selectionStart, cursor := t.selectionStart, t.cursor 2355 defer func() { 2356 if selectionStart != t.selectionStart || cursor != t.cursor { 2357 t.moved() 2358 } 2359 }() 2360 } 2361 2362 // Turn mouse coordinates into text coordinates. 2363 labelWidth := t.labelWidth 2364 if labelWidth == 0 && t.label != "" { 2365 labelWidth = TaggedStringWidth(t.label) 2366 } 2367 column := x - rectX - labelWidth 2368 row := y - rectY 2369 if !t.wrap { 2370 column += t.columnOffset 2371 } 2372 row += t.rowOffset 2373 2374 // Process mouse actions. 2375 switch action { 2376 case MouseLeftDown: 2377 t.moveCursor(row, column) 2378 if event.Modifiers()&tcell.ModShift == 0 { 2379 t.selectionStart = t.cursor 2380 } 2381 setFocus(t) 2382 consumed = true 2383 capture = t 2384 t.dragging = true 2385 case MouseMove: 2386 if !t.dragging { 2387 break 2388 } 2389 t.moveCursor(row, column) 2390 consumed = true 2391 case MouseLeftUp: 2392 t.moveCursor(row, column) 2393 consumed = true 2394 capture = nil 2395 t.dragging = false 2396 case MouseLeftDoubleClick: // Select word. 2397 // Left down/up was already triggered so we are at the correct 2398 // position. 2399 t.moveWordLeft(false) 2400 t.selectionStart = t.cursor 2401 t.moveWordRight(true, false) 2402 consumed = true 2403 case MouseScrollUp: 2404 if t.rowOffset > 0 { 2405 t.rowOffset-- 2406 } 2407 consumed = true 2408 case MouseScrollDown: 2409 t.rowOffset++ 2410 if t.rowOffset >= len(t.lineStarts) { 2411 t.rowOffset = len(t.lineStarts) - 1 2412 if t.rowOffset < 0 { 2413 t.rowOffset = 0 2414 } 2415 } 2416 consumed = true 2417 case MouseScrollLeft: 2418 if t.columnOffset > 0 { 2419 t.columnOffset-- 2420 } 2421 consumed = true 2422 case MouseScrollRight: 2423 t.columnOffset++ 2424 if t.columnOffset >= t.widestLine { 2425 t.columnOffset = t.widestLine - 1 2426 if t.columnOffset < 0 { 2427 t.columnOffset = 0 2428 } 2429 } 2430 consumed = true 2431 } 2432 2433 return 2434 }) 2435 } 2436 2437 // PasteHandler returns the handler for this primitive. 2438 func (t *TextArea) PasteHandler() func(pastedText string, setFocus func(p Primitive)) { 2439 return t.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) { 2440 from, to, row := t.getSelection() 2441 t.cursor.pos = t.replace(from, to, pastedText, false) 2442 t.cursor.row = -1 2443 t.truncateLines(row - 1) 2444 t.findCursor(true, row) 2445 t.selectionStart = t.cursor 2446 }) 2447 }