...

Source file src/github.com/rivo/tview/textarea.go

Documentation: github.com/rivo/tview

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

View as plain text