gemini-browser

A text-based gemini browser
git clone git://git.laack.co/gemini-browser.git
Log | Files | Refs | README

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 }