textarea.go 74 KB

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