From 0d48f2a5369aca0b037fef151fcae84cef159f57 Mon Sep 17 00:00:00 2001 From: rivo <480930+rivo@users.noreply.github.com> Date: Sat, 3 Sep 2022 11:54:42 +0200 Subject: [PATCH] Revert "Publishing the new `TextArea` primitive" --- README.md | 1 - box.go | 6 +- button.go | 4 +- checkbox.go | 16 +- demos/textarea/README.md | 1 - demos/textarea/main.go | 133 -- demos/textarea/screenshot.png | Bin 89771 -> 0 bytes doc.go | 159 +-- form.go | 4 +- frame.go | 44 +- go.mod | 2 +- go.sum | 3 +- grid.go | 25 +- inputfield.go | 36 +- list.go | 2 +- modal.go | 2 +- table.go | 14 +- textarea.go | 2195 --------------------------------- textview.go | 43 +- treeview.go | 8 +- util.go | 26 +- 21 files changed, 170 insertions(+), 2554 deletions(-) delete mode 100644 demos/textarea/README.md delete mode 100644 demos/textarea/main.go delete mode 100644 demos/textarea/screenshot.png delete mode 100644 textarea.go diff --git a/README.md b/README.md index 76002f6..af9ccb0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ Among these components are: - __Input forms__ (include __input/password fields__, __drop-down selections__, __checkboxes__, and __buttons__) - Navigable multi-color __text views__ -- Editable multi-line __text areas__ - Sophisticated navigable __table views__ - Flexible __tree views__ - Selectable __lists__ diff --git a/box.go b/box.go index f236d29..d1c1c69 100644 --- a/box.go +++ b/box.go @@ -125,7 +125,7 @@ func (b *Box) GetInnerRect() (int, int, int, int) { // if this primitive is part of a layout (e.g. Flex, Grid) or if it was added // like this: // -// application.SetRoot(p, true) +// application.SetRoot(b, true) func (b *Box) SetRect(x, y, width, height int) { b.x = x b.y = y @@ -215,7 +215,7 @@ func (b *Box) WrapMouseHandler(mouseHandler func(MouseAction, *tcell.EventMouse, // MouseHandler returns nil. func (b *Box) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - if action == MouseLeftDown && b.InRect(event.Position()) { + if action == MouseLeftClick && b.InRect(event.Position()) { setFocus(b) consumed = true } @@ -272,7 +272,7 @@ func (b *Box) SetBorderColor(color tcell.Color) *Box { // SetBorderAttributes sets the border's style attributes. You can combine // different attributes using bitmask operations: // -// box.SetBorderAttributes(tcell.AttrUnderline | tcell.AttrBold) +// box.SetBorderAttributes(tcell.AttrUnderline | tcell.AttrBold) func (b *Box) SetBorderAttributes(attr tcell.AttrMask) *Box { b.borderStyle = b.borderStyle.Attributes(attr) return b diff --git a/button.go b/button.go index 50a4d22..ec0408c 100644 --- a/button.go +++ b/button.go @@ -145,10 +145,8 @@ func (b *Button) MouseHandler() func(action MouseAction, event *tcell.EventMouse } // Process mouse event. - if action == MouseLeftDown { + if action == MouseLeftClick { setFocus(b) - consumed = true - } else if action == MouseLeftClick { if b.selected != nil { b.selected() } diff --git a/checkbox.go b/checkbox.go index 84c81af..d9a8633 100644 --- a/checkbox.go +++ b/checkbox.go @@ -226,17 +226,13 @@ func (c *Checkbox) MouseHandler() func(action MouseAction, event *tcell.EventMou } // Process mouse event. - if y == rectY { - if action == MouseLeftDown { - setFocus(c) - consumed = true - } else if action == MouseLeftClick { - c.checked = !c.checked - if c.changed != nil { - c.changed(c.checked) - } - consumed = true + if action == MouseLeftClick && y == rectY { + setFocus(c) + c.checked = !c.checked + if c.changed != nil { + c.changed(c.checked) } + consumed = true } return diff --git a/demos/textarea/README.md b/demos/textarea/README.md deleted file mode 100644 index 4a14e6c..0000000 --- a/demos/textarea/README.md +++ /dev/null @@ -1 +0,0 @@ -![Screenshot](screenshot.png) diff --git a/demos/textarea/main.go b/demos/textarea/main.go deleted file mode 100644 index 74aad99..0000000 --- a/demos/textarea/main.go +++ /dev/null @@ -1,133 +0,0 @@ -// Demo code for the TextArea primitive. -package main - -import ( - "fmt" - - "github.com/gdamore/tcell/v2" - "github.com/rivo/tview" -) - -func main() { - app := tview.NewApplication() - - textArea := tview.NewTextArea(). - SetPlaceholder("Enter text here...") - textArea.SetTitle("Text Area").SetBorder(true) - helpInfo := tview.NewTextView(). - SetText(" Press F1 for help, press Ctrl-C to exit") - position := tview.NewTextView(). - SetDynamicColors(true). - SetTextAlign(tview.AlignRight) - pages := tview.NewPages() - - updateInfos := func() { - fromRow, fromColumn, toRow, toColumn := textArea.GetCursor() - if fromRow == toRow && fromColumn == toColumn { - position.SetText(fmt.Sprintf("Row: [yellow]%d[white], Column: [yellow]%d ", fromRow, fromColumn)) - } else { - position.SetText(fmt.Sprintf("[red]From[white] Row: [yellow]%d[white], Column: [yellow]%d[white] - [red]To[white] Row: [yellow]%d[white], To Column: [yellow]%d ", fromRow, fromColumn, toRow, toColumn)) - } - } - - textArea.SetMovedFunc(updateInfos) - updateInfos() - - mainView := tview.NewGrid(). - SetRows(0, 1). - AddItem(textArea, 0, 0, 1, 2, 0, 0, true). - AddItem(helpInfo, 1, 0, 1, 1, 0, 0, false). - AddItem(position, 1, 1, 1, 1, 0, 0, false) - - help1 := tview.NewTextView(). - SetDynamicColors(true). - SetText(`[green]Navigation - -[yellow]Left arrow[white]: Move left. -[yellow]Right arrow[white]: Move right. -[yellow]Down arrow[white]: Move down. -[yellow]Up arrow[white]: Move up. -[yellow]Ctrl-A, Home[white]: Move to the beginning of the current line. -[yellow]Ctrl-E, End[white]: Move to the end of the current line. -[yellow]Ctrl-F, page down[white]: Move down by one page. -[yellow]Ctrl-B, page up[white]: Move up by one page. -[yellow]Alt-Up arrow[white]: Scroll the page up. -[yellow]Alt-Down arrow[white]: Scroll the page down. -[yellow]Alt-Left arrow[white]: Scroll the page to the left. -[yellow]Alt-Right arrow[white]: Scroll the page to the right. -[yellow]Alt-B, Ctrl-Left arrow[white]: Move back by one word. -[yellow]Alt-F, Ctrl-Right arrow[white]: Move forward by one word. - -[blue]Press Enter for more help, press Escape to return.`) - help2 := tview.NewTextView(). - SetDynamicColors(true). - SetText(`[green]Editing[white] - -Type to enter text. -[yellow]Ctrl-H, Backspace[white]: Delete the left character. -[yellow]Ctrl-D, Delete[white]: Delete the right character. -[yellow]Ctrl-K[white]: Delete until the end of the line. -[yellow]Ctrl-W[white]: Delete the rest of the word. -[yellow]Ctrl-U[white]: Delete the current line. - -[blue]Press Enter for more help, press Escape to return.`) - help3 := tview.NewTextView(). - SetDynamicColors(true). - SetText(`[green]Selecting Text[white] - -Move while holding Shift or drag the mouse. -Double-click to select a word. - -[green]Clipboard - -[yellow]Ctrl-Q[white]: Copy. -[yellow]Ctrl-X[white]: Cut. -[yellow]Ctrl-V[white]: Paste. - -[green]Undo - -[yellow]Ctrl-Z[white]: Undo. -[yellow]Ctrl-Y[white]: Redo. - -[blue]Press Enter for more help, press Escape to return.`) - help := tview.NewFrame(help1). - SetBorders(1, 1, 0, 0, 2, 2) - help.SetBorder(true). - SetTitle("Help"). - SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Key() == tcell.KeyEscape { - pages.SwitchToPage("main") - return nil - } else if event.Key() == tcell.KeyEnter { - switch { - case help.GetPrimitive() == help1: - help.SetPrimitive(help2) - case help.GetPrimitive() == help2: - help.SetPrimitive(help3) - case help.GetPrimitive() == help3: - help.SetPrimitive(help1) - } - return nil - } - return event - }) - - pages.AddAndSwitchToPage("main", mainView, true). - AddPage("help", tview.NewGrid(). - SetColumns(0, 64, 0). - SetRows(0, 22, 0). - AddItem(help, 1, 1, 1, 1, 0, 0, true), true, false) - - app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Key() == tcell.KeyF1 { - pages.ShowPage("help") //TODO: Check when clicking outside help window with the mouse. Then clicking help again. - return nil - } - return event - }) - - if err := app.SetRoot(pages, - true).EnableMouse(true).Run(); err != nil { - panic(err) - } -} diff --git a/demos/textarea/screenshot.png b/demos/textarea/screenshot.png deleted file mode 100644 index 41663438fc7538585f989ce895a1ee730af9c794..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89771 zcmdSAWmp|cvj&R0O9<|P0Kp}=26uOtpbNK!gaju*Ah-s1cPBuCySux)!yUGKd!KX9 z_wW9>@T_Oen(3bIsj9B7dh4AKMR^HSBmyJ|2nbXuNik&z2-pq?2xt-nDBw&*`^q5% z1PZ^UsHmcps3@7DBgov+#tZ^NG9*?5UQ?y_Rho`6d9@I&vIyw&C+BSc5el_JLrN-0 z5kwXYv4i~lmAPL~oX%=uKWiW=)w_l=^JiQtepFU6zOu=0mV(r~jy)c{9!!0Ha@rEK z7)oupMuXU+X)ty`D})p(%@@KVBgl)WOo4>!lJ~MxeBTv_ z5ftp0NfG0v-N@_@&7w>GF{=C%HhKj~R~YQ=PY3DblIUj59Hwe=y$?5f?6ot1$6#)vX^hmvwl< z7lv%ShU>8QOKYKmCDi={mkg?07{d_iZLuX8x+$a)7-YiKta9NG!T9YH3K;~NwE0J| z=w=C+GKU|p9}uG;HqaxHW3G{e$T!+hszQsPCF8GO^+}k1$-~(GZ2t}VIOj|$(+Cu@ zf@tdhd@>Xe3%@?-r#AI`d`o^s*!fx3KROD{!?X}x3+qdWa2K;N!ssim_D6eCJ}7-? zLzkcY4rLi0UR5-F)Xo?rm^_Gj{_`sS)X}7wUt#nV1;rA>s*j!gbcE%&_%N;m_7w3g zR^Zwp-2!|x>jf_k;IwL4?X=Vr1V298!Y%Wo<`Dl1`K;{x77{0!RHAUUpaHe0$MMnI z2d5$zbvv1V;XKRt%58SLlk-7B#)icegEuI(gV9i;LcG08-cU}H&{Z}tNMjoNc8}of z6@4TP;U2>CDrl#-k2UdFb$??0LTglmj5fL@3zAoXPmf}thrzIN1y8trBiKEHGLcc9 zkrAS&l__+3nlSL8$&Z=QXI**;YNel)h=}hlJnR$raYUs4G2LgqII?rvpAlla-$Pj{ z<8!AO9+a0%erIYf`d2^ha=&v2$fI_6m`{exuf)E%e?xiGxx==~6aUH5@J#}|v@o|O z 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

ulT#JYeD$Aqe&am6dhNow&Qq(=$^&BQr*;_cGF1 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 Mh>c<~_?VVtPXfh{Kdq6ZXtOkM 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 diff --git a/doc.go b/doc.go index 2ad024d..d4e071e 100644 --- a/doc.go +++ b/doc.go @@ -3,78 +3,77 @@ Package tview implements rich widgets for terminal based user interfaces. The widgets provided with this package are useful for data exploration and data entry. -# Widgets +Widgets The package implements the following widgets: - - [TextView]: A scrollable window that display multi-colored text. Text may - also be highlighted. - - [TextArea]: An editable multi-line text area. - - [Table]: A scrollable display of tabular data. Table cells, rows, or columns + - TextView: A scrollable window that display multi-colored text. Text may also + be highlighted. + - Table: A scrollable display of tabular data. Table cells, rows, or columns may also be highlighted. - - [TreeView]: A scrollable display for hierarchical data. Tree nodes can be + - TreeView: A scrollable display for hierarchical data. Tree nodes can be highlighted, collapsed, expanded, and more. - - [List]: A navigable text list with optional keyboard shortcuts. - - [InputField]: One-line input fields to enter text. - - [DropDown]: Drop-down selection fields. - - [Checkbox]: Selectable checkbox for boolean values. - - [Button]: Buttons which get activated when the user selects them. + - List: A navigable text list with optional keyboard shortcuts. + - InputField: One-line input fields to enter text. + - DropDown: Drop-down selection fields. + - Checkbox: Selectable checkbox for boolean values. + - Button: Buttons which get activated when the user selects them. - Form: Forms composed of input fields, drop down selections, checkboxes, and buttons. - - [Modal]: A centered window with a text message and one or more buttons. - - [Grid]: A grid based layout manager. - - [Flex]: A Flexbox based layout manager. - - [Pages]: A page based layout manager. + - Modal: A centered window with a text message and one or more buttons. + - Grid: A grid based layout manager. + - Flex: A Flexbox based layout manager. + - Pages: A page based layout manager. The package also provides Application which is used to poll the event queue and draw widgets on screen. -# Hello World +Hello World The following is a very basic example showing a box with the title "Hello, world!": - package main + package main - import ( - "github.com/rivo/tview" - ) + import ( + "github.com/rivo/tview" + ) - func main() { - box := tview.NewBox().SetBorder(true).SetTitle("Hello, world!") - if err := tview.NewApplication().SetRoot(box, true).Run(); err != nil { - panic(err) - } - } + func main() { + box := tview.NewBox().SetBorder(true).SetTitle("Hello, world!") + if err := tview.NewApplication().SetRoot(box, true).Run(); err != nil { + panic(err) + } + } First, we create a box primitive with a border and a title. Then we create an application, set the box as its root primitive, and run the event loop. The -application exits when the application's [Application.Stop] function is called -or when Ctrl-C is pressed. +application exits when the application's Stop() function is called or when +Ctrl-C is pressed. If we have a primitive which consumes key presses, we call the application's -[Application.SetFocus] function to redirect all key presses to that primitive. -Most primitives then offer ways to install handlers that allow you to react to -any actions performed on them. +SetFocus() function to redirect all key presses to that primitive. Most +primitives then offer ways to install handlers that allow you to react to any +actions performed on them. -# More Demos +More Demos You will find more demos in the "demos" subdirectory. It also contains a presentation (written using tview) which gives an overview of the different widgets and how they can be used. -# Colors +Colors -Throughout this package, colors are specified using the [tcell.Color] type. -Functions such as [tcell.GetColor], [tcell.NewHexColor], and [tcell.NewRGBColor] +Throughout this package, colors are specified using the tcell.Color type. +Functions such as tcell.GetColor(), tcell.NewHexColor(), and tcell.NewRGBColor() can be used to create colors from W3C color names or RGB values. Almost all strings which are displayed can contain color tags. Color tags are W3C color names or six hexadecimal digits following a hash tag, wrapped in square brackets. Examples: - This is a [red]warning[white]! - The sky is [#8080ff]blue[#ffffff]. + This is a [red]warning[white]! + The sky is [#8080ff]blue[#ffffff]. A color tag changes the color of the characters following that color tag. This applies to almost everything from box titles, list text, form item labels, to @@ -85,7 +84,7 @@ Color tags may contain not just the foreground (text) color but also the background color and additional flags. In fact, the full definition of a color tag is as follows: - [::] + [::] Each of the three fields can be left blank and trailing fields can be omitted. (Empty square brackets "[]", however, are not considered color tags.) Colors @@ -95,26 +94,26 @@ means "reset to default". You can specify the following flags (some flags may not be supported by your terminal): - l: blink - b: bold - i: italic - d: dim - r: reverse (switch foreground and background color) - u: underline - s: strike-through + l: blink + b: bold + i: italic + d: dim + r: reverse (switch foreground and background color) + u: underline + s: strike-through Examples: - [yellow]Yellow text - [yellow:red]Yellow text on red background - [:red]Red background, text color unchanged - [yellow::u]Yellow text underlined - [::bl]Bold, blinking text - [::-]Colors unchanged, flags reset - [-]Reset foreground color - [-:-:-]Reset everything - [:]No effect - []Not a valid color tag, will print square brackets as they are + [yellow]Yellow text + [yellow:red]Yellow text on red background + [:red]Red background, text color unchanged + [yellow::u]Yellow text underlined + [::bl]Bold, blinking text + [::-]Colors unchanged, flags reset + [-]Reset foreground color + [-:-:-]Reset everything + [:]No effect + []Not a valid color tag, will print square brackets as they are In the rare event that you want to display a string such as "[red]" or "[#00ff1a]" without applying its effect, you need to put an opening square @@ -122,26 +121,26 @@ bracket before the closing square bracket. Note that the text inside the brackets will be matched less strictly than region or colors tags. I.e. any character that may be used in color or region tags will be recognized. Examples: - [red[] will be output as [red] - ["123"[] will be output as ["123"] - [#6aff00[[] will be output as [#6aff00[] - [a#"[[[] will be output as [a#"[[] - [] will be output as [] (see color tags above) - [[] will be output as [[] (not an escaped tag) + [red[] will be output as [red] + ["123"[] will be output as ["123"] + [#6aff00[[] will be output as [#6aff00[] + [a#"[[[] will be output as [a#"[[] + [] will be output as [] (see color tags above) + [[] will be output as [[] (not an escaped tag) You can use the Escape() function to insert brackets automatically where needed. -# Styles +Styles When primitives are instantiated, they are initialized with colors taken from the global Styles variable. You may change this variable to adapt the look and feel of the primitives to your preferred style. -# Unicode Support +Unicode Support This package supports unicode characters including wide characters. -# Concurrency +Concurrency Many functions in this package are not thread-safe. For many applications, this may not be an issue: If your code makes changes in response to key events, it @@ -149,32 +148,34 @@ will execute in the main goroutine and thus will not cause any race conditions. If you access your primitives from other goroutines, however, you will need to synchronize execution. The easiest way to do this is to call -[Application.QueueUpdate] or [Application.QueueUpdateDraw] (see the function +Application.QueueUpdate() or Application.QueueUpdateDraw() (see the function documentation for details): - go func() { - app.QueueUpdateDraw(func() { - table.SetCellSimple(0, 0, "Foo bar") - }) - }() + go func() { + app.QueueUpdateDraw(func() { + table.SetCellSimple(0, 0, "Foo bar") + }) + }() -One exception to this is the io.Writer interface implemented by [TextView]. You -can safely write to a [TextView] from any goroutine. See the [TextView] +One exception to this is the io.Writer interface implemented by TextView. You +can safely write to a TextView from any goroutine. See the TextView documentation for details. -You can also call [Application.Draw] from any goroutine without having to wrap -it in [Application.QueueUpdate]. And, as mentioned above, key event callbacks -are executed in the main goroutine and thus should not use -[Application.QueueUpdate] as that may lead to deadlocks. +You can also call Application.Draw() from any goroutine without having to wrap +it in QueueUpdate(). And, as mentioned above, key event callbacks are executed +in the main goroutine and thus should not use QueueUpdate() as that may lead to +deadlocks. -# Type Hierarchy +Type Hierarchy -All widgets listed above contain the [Box] type. All of [Box]'s functions are +All widgets listed above contain the Box type. All of Box's functions are therefore available for all widgets, too. -All widgets also implement the [Primitive] interface. +All widgets also implement the Primitive interface. The tview package is based on https://github.com/gdamore/tcell. It uses types and constants from that package (e.g. colors and keyboard values). + +This package does not process mouse input (yet). */ package tview diff --git a/form.go b/form.go index 8445994..4734ebe 100644 --- a/form.go +++ b/form.go @@ -645,9 +645,9 @@ func (f *Form) MouseHandler() func(action MouseAction, event *tcell.EventMouse, } } - // A mouse down anywhere else will return the focus to the last selected + // A mouse click anywhere else will return the focus to the last selected // element. - if action == MouseLeftDown && f.InRect(event.Position()) { + if action == MouseLeftClick && f.InRect(event.Position()) { consumed = true } diff --git a/frame.go b/frame.go index 0ddbb1a..eb6c5ee 100644 --- a/frame.go +++ b/frame.go @@ -27,9 +27,6 @@ type Frame struct { // Border spacing. top, bottom, header, footer, left, right int - - // Keep a reference in case we need it when we change the primitive. - setFocus func(p Primitive) } // NewFrame returns a new frame around the given primitive. The primitive's @@ -52,25 +49,6 @@ func NewFrame(primitive Primitive) *Frame { return f } -// SetPrimitive replaces the contained primitive with the given one. To remove -// a primitive, set it to nil. -func (f *Frame) SetPrimitive(p Primitive) *Frame { - var hasFocus bool - if f.primitive != nil { - hasFocus = f.primitive.HasFocus() - } - f.primitive = p - if hasFocus && f.setFocus != nil { - f.setFocus(p) // Restore focus. - } - return f -} - -// GetPrimitive returns the primitive contained in this frame. -func (f *Frame) GetPrimitive() Primitive { - return f.primitive -} - // AddText adds text to the frame. Set "header" to true if the text is to appear // in the header, above the contained primitive. Set it to false for it to // appear in the footer, below the contained primitive. "align" must be one of @@ -167,7 +145,6 @@ func (f *Frame) Draw(screen tcell.Screen) { // Focus is called when this primitive receives focus. func (f *Frame) Focus(delegate func(p Primitive)) { - f.setFocus = delegate if f.primitive != nil { delegate(f.primitive) } else { @@ -192,19 +169,10 @@ func (f *Frame) MouseHandler() func(action MouseAction, event *tcell.EventMouse, // Pass mouse events on to contained primitive. if f.primitive != nil { - consumed, capture = f.primitive.MouseHandler()(action, event, setFocus) - if consumed { - return true, capture - } + return f.primitive.MouseHandler()(action, event, setFocus) } - // Clicking on the frame parts. - if action == MouseLeftDown { - setFocus(f) - consumed = true - } - - return + return false, nil }) } @@ -214,9 +182,11 @@ func (f *Frame) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi if f.primitive == nil { return } - if handler := f.primitive.InputHandler(); handler != nil { - handler(event, setFocus) - return + if f.primitive.HasFocus() { + if handler := f.primitive.InputHandler(); handler != nil { + handler(event, setFocus) + return + } } }) } diff --git a/go.mod b/go.mod index 4d43904..c145c35 100644 --- a/go.mod +++ b/go.mod @@ -13,5 +13,5 @@ require ( github.com/gdamore/encoding v1.0.0 // indirect golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 // indirect golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/text v0.3.6 // indirect ) diff --git a/go.sum b/go.sum index cbaff94..b74f407 100644 --- a/go.sum +++ b/go.sum @@ -16,7 +16,6 @@ golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/grid.go b/grid.go index a72d08e..4af40f1 100644 --- a/grid.go +++ b/grid.go @@ -35,7 +35,7 @@ type Grid struct { items []*gridItem // The definition of the rows and columns of the grid. See - // [TextView.SetRows] / [TextView.SetColumns] for details. + // SetRows()/SetColumns() for details. rows, columns []int // The minimum sizes for rows and columns. @@ -65,7 +65,7 @@ type Grid struct { // clear a Grid's background before any items are drawn, reset its Box to one // with the desired color: // -// grid.Box = NewBox() +// grid.Box = NewBox() func NewGrid() *Grid { g := &Grid{ bordersColor: Styles.GraphicsColor, @@ -93,14 +93,14 @@ func NewGrid() *Grid { // following call will result in columns with widths of 30, 10, 15, 15, and 30 // cells: // -// grid.SetColumns(30, 10, -1, -1, -2) +// grid.SetColumns(30, 10, -1, -1, -2) // // If a primitive were then placed in the 6th and 7th column, the resulting // widths would be: 30, 10, 10, 10, 20, 10, and 10 cells. // // If you then called SetMinSize() as follows: // -// grid.SetMinSize(15, 20) +// grid.SetMinSize(15, 20) // // The resulting widths would be: 30, 15, 15, 15, 20, 15, and 15 cells, a total // of 125 cells, 25 cells wider than the available grid width. @@ -110,8 +110,8 @@ func (g *Grid) SetColumns(columns ...int) *Grid { } // SetRows defines how the rows of the grid are distributed. These values behave -// the same as the column values provided with [TextView.SetColumns], see there -// for a definition and examples. +// the same as the column values provided with SetColumns(), see there for a +// definition and examples. // // The provided values correspond to row heights, the first value defining // the height of the topmost row. @@ -120,9 +120,8 @@ func (g *Grid) SetRows(rows ...int) *Grid { return g } -// SetSize is a shortcut for [TextView.SetRows] and [TextView.SetColumns] where -// all row and column values are set to the given size values. See -// [TextView.SetColumns] for details on sizes. +// SetSize is a shortcut for SetRows() and SetColumns() where all row and column +// values are set to the given size values. See SetColumns() for details on sizes. func (g *Grid) SetSize(numRows, numColumns, rowSize, columnSize int) *Grid { g.rows = make([]int, numRows) for index := range g.rows { @@ -175,7 +174,7 @@ func (g *Grid) SetBordersColor(color tcell.Color) *Grid { // the given row and column and will span "rowSpan" rows and "colSpan" columns. // For example, for a primitive to occupy rows 2, 3, and 4 and columns 5 and 6: // -// grid.AddItem(p, 2, 5, 3, 2, 0, 0, true) +// grid.AddItem(p, 2, 5, 3, 2, 0, 0, true) // // If rowSpan or colSpan is 0, the primitive will not be drawn. // @@ -186,9 +185,9 @@ func (g *Grid) SetBordersColor(color tcell.Color) *Grid { // primitive apply, the one that has at least one highest minimum value will be // used, or the primitive added last if those values are the same. Example: // -// grid.AddItem(p, 0, 0, 0, 0, 0, 0, true). // Hide in small grids. -// AddItem(p, 0, 0, 1, 2, 100, 0, true). // One-column layout for medium grids. -// AddItem(p, 1, 1, 3, 2, 300, 0, true) // Multi-column layout for large grids. +// grid.AddItem(p, 0, 0, 0, 0, 0, 0, true). // Hide in small grids. +// AddItem(p, 0, 0, 1, 2, 100, 0, true). // One-column layout for medium grids. +// AddItem(p, 1, 1, 3, 2, 300, 0, true) // Multi-column layout for large grids. // // To use the same grid layout for all sizes, simply set minGridWidth and // minGridHeight to 0. diff --git a/inputfield.go b/inputfield.go index 5201665..f0ea22f 100644 --- a/inputfield.go +++ b/inputfield.go @@ -418,7 +418,7 @@ func (i *InputField) Draw(screen tcell.Screen) { // We have enough space for the full text. printWithStyle(screen, Escape(text), x, y, 0, fieldWidth, AlignLeft, i.fieldStyle, true) i.offset = 0 - iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool { + iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { if textPos >= i.cursorPos { return true } @@ -440,7 +440,7 @@ func (i *InputField) Draw(screen tcell.Screen) { shiftLeft = subWidth - fieldWidth + 1 } currentOffset := i.offset - iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool { + iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { if textPos >= currentOffset { if shiftLeft > 0 { i.offset = textPos + textWidth @@ -520,7 +520,7 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p }) } moveRight := func() { - iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool { + iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { i.cursorPos += textWidth return true }) @@ -616,7 +616,7 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p i.offset = 0 } case tcell.KeyDelete, tcell.KeyCtrlD: // Delete character after the cursor. - iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool { + iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+textWidth:] return true }) @@ -685,25 +685,21 @@ func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventM } // Process mouse event. - if y == rectY { - if action == MouseLeftDown { - setFocus(i) - consumed = true - } else if action == MouseLeftClick { - // Determine where to place the cursor. - if x >= i.fieldX { - if !iterateString(i.text[i.offset:], func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth, boundaries int) bool { - if x-i.fieldX < screenPos+screenWidth { - i.cursorPos = textPos + i.offset - return true - } - return false - }) { - i.cursorPos = len(i.text) + if action == MouseLeftClick && y == rectY { + // Determine where to place the cursor. + if x >= i.fieldX { + if !iterateString(i.text[i.offset:], func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool { + if x-i.fieldX < screenPos+screenWidth { + i.cursorPos = textPos + i.offset + return true } + return false + }) { + i.cursorPos = len(i.text) } - consumed = true } + setFocus(i) + consumed = true } return diff --git a/list.go b/list.go index 3be92e7..a35e80e 100644 --- a/list.go +++ b/list.go @@ -699,6 +699,7 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, // Process mouse event. switch action { case MouseLeftClick: + setFocus(l) index := l.indexAtPoint(event.Position()) if index != -1 { item := l.items[index] @@ -727,7 +728,6 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if _, _, _, height := l.GetInnerRect(); lines > height { l.itemOffset++ } - setFocus(l) consumed = true } diff --git a/modal.go b/modal.go index 270c0ff..85ffc63 100644 --- a/modal.go +++ b/modal.go @@ -190,7 +190,7 @@ func (m *Modal) MouseHandler() func(action MouseAction, event *tcell.EventMouse, return m.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { // Pass mouse events on to the form. consumed, capture = m.form.MouseHandler()(action, event, setFocus) - if !consumed && action == MouseLeftDown && m.InRect(event.Position()) { + if !consumed && action == MouseLeftClick && m.InRect(event.Position()) { setFocus(m) consumed = true } diff --git a/table.go b/table.go index 8ae805f..7cf4df6 100644 --- a/table.go +++ b/table.go @@ -135,7 +135,7 @@ func (c *TableCell) SetTransparency(transparent bool) *TableCell { // SetAttributes sets the cell's text attributes. You can combine different // attributes using bitmask operations: // -// cell.SetAttributes(tcell.AttrUnderline | tcell.AttrBold) +// cell.SetAttributes(tcell.AttrUnderline | tcell.AttrBold) func (c *TableCell) SetAttributes(attr tcell.AttrMask) *TableCell { c.Attributes = attr return c @@ -388,13 +388,13 @@ func (t *tableDefaultContent) GetColumnCount() int { // Columns will use as much horizontal space as they need. You can constrain // their size with the MaxWidth parameter of the TableCell type. // -// # Fixed Columns +// Fixed Columns // // You can define fixed rows and rolumns via SetFixed(). They will always stay // in their place, even when the table is scrolled. Fixed rows are always the // top rows. Fixed columns are always the leftmost columns. // -// # Selections +// Selections // // You can call SetSelectable() to set columns and/or rows to "selectable". If // the flag is set only for columns, entire columns can be selected by the user. @@ -402,7 +402,7 @@ func (t *tableDefaultContent) GetColumnCount() int { // set, individual cells can be selected. The "selected" handler set via // SetSelectedFunc() is invoked when the user presses Enter on a selection. // -// # Navigation +// Navigation // // If the table extends beyond the available space, it can be navigated with // key bindings similar to Vim: @@ -551,7 +551,7 @@ func (t *Table) SetBordersColor(color tcell.Color) *Table { // // To reset a previous setting to its default, make the following call: // -// table.SetSelectedStyle(tcell.Style{}) +// table.SetSelectedStyle(tcell.Style{}) func (t *Table) SetSelectedStyle(style tcell.Style) *Table { t.selectedStyle = style return t @@ -1596,9 +1596,6 @@ func (t *Table) MouseHandler() func(action MouseAction, event *tcell.EventMouse, } switch action { - case MouseLeftDown: - setFocus(t) - consumed = true case MouseLeftClick: selectEvent := true row, column := t.cellAt(x, y) @@ -1611,6 +1608,7 @@ func (t *Table) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if selectEvent && (t.rowsSelectable || t.columnsSelectable) { t.Select(row, column) } + setFocus(t) consumed = true case MouseScrollUp: t.trackEnd = false diff --git a/textarea.go b/textarea.go deleted file mode 100644 index 0b7bc0a..0000000 --- a/textarea.go +++ /dev/null @@ -1,2195 +0,0 @@ -package tview - -import ( - "fmt" - "strings" - "unicode" - "unicode/utf8" - - "github.com/gdamore/tcell/v2" - "github.com/rivo/uniseg" -) - -const ( - // The minimum capacity of the text area's piece chain slice. - pieceChainMinCap = 10 - - // The minimum capacity of the text area's edit buffer. - editBufferMinCap = 200 - - // The maximum number of bytes making up a grapheme cluster. In theory, this - // could be longer but it would be highly unusual. - maxGraphemeClusterSize = 40 - - // The minimum width of text (if available) to be shown left of the cursor. - minCursorPrefix = 5 - - // The minimum width of text (if available) to be shown right of the cursor. - minCursorSuffix = 3 -) - -// Types of user actions on a text area. -type taAction int - -const ( - taActionOther taAction = iota - taActionTypeSpace // Typing a space character. - taActionTypeNonSpace // Typing a non-space character. - taActionBackspace // Deleting the previous character. - taActionDelete // Deleting the next character. -) - -// NewLine is the string sequence to be inserted when hitting the Enter key in a -// TextArea. The default is "\n" but you may change it to "\r\n" if required. -var NewLine = "\n" - -// textAreaSpan represents a range of text in a text area. The text area widget -// roughly follows the concept of Piece Chains outlined in -// http://www.catch22.net/tuts/neatpad/piece-chains with some modifications. -// This type represents a "span" (or "piece") and thus refers to a subset of the -// text in the editor as part of a doubly-linked list. -// -// In most places where we reference a position in the text, we use a -// three-element int array. The first element is the index of the referenced -// span in the piece chain. The second element is the offset into the span's -// referenced text (relative to the span's start), its value is always >= 0 and -// < span.length. The third element is the state of the text parser at that -// position. -// -// A range of text is represented by a span range which is a starting position -// (3-int array) and an ending position (3-int array). The starting position -// references the first character of the range, the ending position references -// the position after the last character of the range. The end of the text is -// therefore always [3]int{1, 0, 0}, position 0 of the ending sentinel. -// -// Sentinel spans are dummy spans not referring to any text. There are always -// two sentinel spans: the starting span at index 0 of the [TextArea.spans] -// slice and the ending span at index 1. -type textAreaSpan struct { - // Links to the previous and next textAreaSpan objects as indices into the - // [TextArea.spans] slice. The sentinel spans (index 0 and 1) have -1 as - // their previous or next links, respectively. - previous, next int - - // The start index and the length of the text segment this span represents. - // If "length" is negative, the span represents a substring of - // [TextArea.initialText] and the actual length is its absolute value. If it - // is positive, the span represents a substring of [TextArea.editText]. For - // the sentinel spans (index 0 and 1), both values will be 0. Others will - // never have a zero length. - offset, length int -} - -// textAreaUndoItem represents an undoable edit to the text area. It describes -// the two spans wrapping a text change. -type textAreaUndoItem struct { - before, after int // The index of the copied "before" and "after" spans into the "spans" slice. - originalBefore, originalAfter int // The original indices of the "before" and "after" spans. - pos [3]int // The cursor position to be assumed after applying an undo. - length int // The total text length at the time the undo item was created. - 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. -} - -// TextArea implements a simple text editor for multi-line text. Multi-color -// text is not supported. Word-wrapping is enabled by default but can be turned -// off or be changed to character-wrapping. -// -// At this point, a text area cannot be added to a [Form]. This will be added in -// the future. -// -// # Navigation and Editing -// -// A text area is always in editing mode and no other mode exists. The following -// keys can be used to move the cursor (subject to what the user's terminal -// supports and how it is configured): -// -// - Left arrow: Move left. -// - Right arrow: Move right. -// - Down arrow: Move down. -// - Up arrow: Move up. -// - Ctrl-A, Home: Move to the beginning of the current line. -// - Ctrl-E, End: Move to the end of the current line. -// - Ctrl-F, page down: Move down by one page. -// - Ctrl-B, page up: Move up by one page. -// - Alt-Up arrow: Scroll the page up, leaving the cursor in its position. -// - Alt-Down arrow: Scroll the page down, leaving the cursor in its position. -// - Alt-Left arrow: Scroll the page to the left, leaving the cursor in its -// position. Ignored if wrapping is enabled. -// - Alt-Right arrow: Scroll the page to the right, leaving the cursor in its -// position. Ignored if wrapping is enabled. -// - Alt-B, Ctrl-Left arrow: Jump to the beginning of the current or previous -// word. -// - Alt-F, Ctrl-Right arrow: Jump to the end of the current or next word. -// -// Words are defined according to [Unicode Standard Annex #29]. We skip any -// words that contain only spaces or punctuation. -// -// Entering a character will insert it at the current cursor location. -// Subsequent characters are shifted accordingly. If the cursor is outside the -// visible area, any changes to the text will move it into the visible area. The -// following keys can also be used to modify the text: -// -// - Enter: Insert a newline character (see [NewLine]). -// - Tab: Insert a tab character (\t). It will be rendered like [TabSize] -// spaces. (This may eventually be changed to behave like regular tabs.) -// - Ctrl-H, Backspace: Delete one character to the left of the cursor. -// - Ctrl-D, Delete: Delete the character under the cursor (or the first -// character on the next line if the cursor is at the end of a line). -// - Alt-Backspace: Delete the word to the left of the cursor. -// - Ctrl-K: Delete everything under and to the right of the cursor until the -// next newline character. -// - Ctrl-W: Delete from the start of the current word to the left of the -// cursor. -// - Ctrl-U: Delete the current line, i.e. everything after the last newline -// character before the cursor up until the next newline character. This may -// span multiple lines if wrapping is enabled. -// -// Text can be selected by moving the cursor while holding the Shift key, to the -// extent that this is supported by the user's terminal. The Ctrl-L key can be -// used to select the entire text. (Ctrl-A already binds to the "Home" key.) -// -// When text is selected: -// -// - Entering a character will replace the selected text with the new -// character. -// - Backspace, delete, Ctrl-H, Ctrl-D: Delete the selected text. -// - Ctrl-Q: Copy the selected text into the clipboard, unselect the text. -// - Ctrl-X: Copy the selected text into the clipboard and delete it. -// - Ctrl-V: Replace the selected text with the clipboard text. If no text is -// selected, the clipboard text will be inserted at the cursor location. -// -// The Ctrl-Q key was chosen for the "copy" function because the Ctrl-C key is -// the default key to stop the application. If your application frees up the -// global Ctrl-C key and you want to bind it to the "copy to clipboard" -// function, you may use [Box.SetInputCapture] to override the Ctrl-Q key to -// implement copying to the clipboard. Note that using your terminal's / -// operating system's key bindings for copy+paste functionality may not have the -// expected effect as tview will not be able to handle these keys. Pasting text -// using your operating system's or terminal's own methods may be very slow as -// each character will be pasted individually. -// -// The default clipboard is an internal text buffer, i.e. the operating system's -// clipboard is not used. If you want to implement your own clipboard (or make -// use of your operating system's clipboard), you can use -// [TextArea.SetClipboard] which provides all the functionality needed to -// implement your own clipboard. -// -// The text area also supports Undo: -// -// - Ctrl-Z: Undo the last change. -// - Ctrl-Y: Redo the last Undo change. -// -// Undo does not affect the clipboard. -// -// If the mouse is enabled, the following actions are available: -// -// - Left click: Move the cursor to the clicked position or to the end of the -// line if past the last character. -// - Left double-click: Select the word under the cursor. -// - Left click while holding the Shift key: Select text. -// - Scroll wheel: Scroll the text. -// -// [Unicode Standard Annex #29]: https://unicode.org/reports/tr29/ -type TextArea struct { - *Box - - // The text to be shown in the text area when it is empty. - placeholder string - - // Styles: - - // The style of the text. Background colors different from the Box's - // background color may lead to unwanted artefacts. - textStyle tcell.Style - - // The style of the selected text. - selectedStyle tcell.Style - - // The style of the placeholder text. - placeholderStyle tcell.Style - - // Text manipulation related fields: - - // The text area's text prior to any editing. It is referenced by spans with - // a negative length. - initialText string - - // Any text that's been added by the user at some point. We only ever append - // to this buffer. It is referenced by spans with a positive length. - editText strings.Builder - - // The total length of all text in the text area. - length int - - // The maximum number of bytes allowed in the text area. If 0, there is no - // limit. - maxLength int - - // The piece chain. The first two spans are sentinel spans which don't - // reference anything and always remain in the same place. Spans are never - // deleted from this slice. - spans []textAreaSpan - - // Display, navigation, and cursor related fields: - - // If set to true, lines that are longer than the available width are - // wrapped onto the next line. If set to false, any characters beyond the - // available width are discarded. - wrap bool - - // If set to true and if wrap is also true, lines are split at spaces or - // after punctuation characters. - wordWrap bool - - // The index of the first line shown in the text area. - rowOffset int - - // The number of cells to be skipped on each line (not used in wrap mode). - columnOffset int - - // The inner height and width of the text area the last time it was drawn. - lastHeight, lastWidth int - - // The width of the currently known widest line, as determined by - // [TextArea.extendLines]. - widestLine int - - // Text positions and states of the start of lines. Each element is a span - // position (see [textAreaSpan]). Not all lines of the text may be contained - // at any time, extend as needed with the [TextArea.extendLines] function. - lineStarts [][3]int - - // The cursor always points to the next position where a new character would - // be placed. The selection start is the same as cursor as long as there is - // no selection. When there is one, the selection is between selectionStart - // and cursor. - cursor, selectionStart struct { - // The row and column in screen space but relative to the start of the - // text which may be outside the text area's box. The column value may - // be larger than where the cursor actually is if the line the cursor - // is on is shorter. The actualColumn is the position as it is seen on - // screen. These three values may not be determined yet, in which case - // the row is negative. - row, column, actualColumn int - - // The textAreaSpan position with state for the actual next character. - pos [3]int - } - - // Set to true when the mouse is dragging to select text. - dragging bool - - // Clipboard related fields: - - // The internal clipboard. - clipboard string - - // The function to call when the user copies/cuts a text selection to the - // clipboard. - copyToClipboard func(string) - - // The function to call when the user pastes text from the clipboard. - pasteFromClipboard func() string - - // Undo/redo related fields: - - // The last action performed by the user. - lastAction taAction - - // The undo stack's items. Each item is a copy of the span before the - // modified span range and a copy of the span after the modified span range. - // To undo an action, the two referenced spans are put back into their - // original place. Undos and redos decrease or increase the nextUndo value. - // Thus, the next undo action is not always the last item. - undoStack []textAreaUndoItem - - // The current undo/redo position on the undo stack. If no undo or redo has - // been performed yet, this is the same as len(undoStack). - nextUndo int - - // Event handlers: - - // An optional function which is called when the input has changed. - changed func() - - // An optional function which is called when the position of the cursor or - // the selection has changed. - moved func() -} - -// NewTextArea returns a new text area. Use [TextArea.SetText] to set the -// initial text. -func NewTextArea() *TextArea { - t := &TextArea{ - Box: NewBox(), - wrap: true, - wordWrap: true, - placeholderStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.TertiaryTextColor), - textStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.PrimaryTextColor), - selectedStyle: tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.PrimitiveBackgroundColor), - spans: make([]textAreaSpan, 2, pieceChainMinCap), // We reserve some space to avoid reallocations right when editing starts. - lastAction: taActionOther, - } - t.editText.Grow(editBufferMinCap) - t.spans[0] = textAreaSpan{previous: -1, next: 1} - t.spans[1] = textAreaSpan{previous: 0, next: -1} - t.cursor.pos = [3]int{1, 0, -1} - t.selectionStart = t.cursor - t.SetClipboard(nil, nil) - - return t -} - -// SetText sets the text of the text area. All existing text is deleted and -// replaced with the new text. Any edits are discarded, no undos are available. -// This function is typically only used to initialize the text area with a text -// after it has been created. To clear the text area's text (again, no undos), -// provide an empty string. -// -// If cursorAtTheEnd is false, the cursor is placed at the start of the text. If -// it is true, it is placed at the end of the text. For very long texts, placing -// the cursor at the end can be an expensive operation because the entire text -// needs to be parsed and laid out. -func (t *TextArea) SetText(text string, cursorAtTheEnd bool) *TextArea { - t.spans = t.spans[:2] - t.initialText = text - t.editText.Reset() - t.lineStarts = nil - t.length = len(text) - t.rowOffset = 0 - t.columnOffset = 0 - t.reset() - t.cursor.row, t.cursor.actualColumn, t.cursor.column = 0, 0, 0 - t.cursor.pos = [3]int{1, 0, -1} - t.undoStack = t.undoStack[:0] - - if len(text) > 0 { - t.spans = append(t.spans, textAreaSpan{ - previous: 0, - next: 1, - offset: 0, - length: -len(text), - }) - t.spans[0].next = 2 - t.spans[1].previous = 2 - if cursorAtTheEnd { - t.cursor.row = -1 - if t.lastWidth > 0 { - t.findCursor(true, 0) - } - } else { - t.cursor.pos = [3]int{2, 0, -1} - } - } else { - t.spans[0].next = 1 - t.spans[1].previous = 0 - } - t.selectionStart = t.cursor - - if t.changed != nil { - t.changed() - } - - if t.lastWidth > 0 && t.moved != nil { - t.moved() - } - - return t -} - -// GetText returns the entire text of the text area. Note that this will newly -// allocate the entire text. -func (t *TextArea) GetText() string { - if t.length == 0 { - return "" - } - - var text strings.Builder - text.Grow(t.length) - spanIndex := t.spans[0].next - for spanIndex != 1 { - span := &t.spans[spanIndex] - if span.length < 0 { - text.WriteString(t.initialText[span.offset : span.offset-span.length]) - } else { - text.WriteString(t.editText.String()[span.offset : span.offset+span.length]) - } - spanIndex = t.spans[spanIndex].next - } - - return text.String() -} - -// HasSelection returns whether the selected text is non-empty. -func (t *TextArea) HasSelection() bool { - return t.selectionStart != t.cursor -} - -// GetSelection returns the currently selected text and its start and end -// positions within the entire text as a half-open interval. If the returned -// text is an empty string, the start and end positions are the same and can be -// interpreted as the cursor position. -// -// Calling this function will result in string allocations as well as a search -// for text positions. This is expensive if the text has been edited extensively -// already. Use [TextArea.HasSelection] first if you are only interested in -// selected text. -func (t *TextArea) GetSelection() (text string, start int, end int) { - from, to := t.selectionStart.pos, t.cursor.pos - if t.cursor.row < t.selectionStart.row || (t.cursor.row == t.selectionStart.row && t.cursor.actualColumn < t.selectionStart.actualColumn) { - from, to = to, from - } - - if from[0] == 1 { - start = t.length - } - if to[0] == 1 { - end = t.length - } - - var ( - index int - selection strings.Builder - inside bool - ) - for span := t.spans[0].next; span != 1; span = t.spans[span].next { - var spanText string - length := t.spans[span].length - if length < 0 { - length = -length - spanText = t.initialText - } else { - spanText = t.editText.String() - } - spanText = spanText[t.spans[span].offset : t.spans[span].offset+length] - - if from[0] == span && to[0] == span { - if from != to { - selection.WriteString(spanText[from[1]:to[1]]) - } - start = index + from[1] - end = index + to[1] - break - } else if from[0] == span { - if from != to { - selection.WriteString(spanText[from[1]:]) - } - start = index + from[1] - inside = true - } else if to[0] == span { - if from != to { - selection.WriteString(spanText[:to[1]]) - } - end = index + to[1] - break - } else if inside && from != to { - selection.WriteString(spanText) - } - - index += length - } - - if selection.Len() != 0 { - text = selection.String() - } - return -} - -// GetCursor returns the current cursor position where the first character of -// the entire text is in row 0, column 0. If the user has selected text, the -// "from" values will refer to the beginning of the selection and the "to" -// values to the end of the selection (exclusive). They are the same if there -// is no selection. -func (t *TextArea) GetCursor() (fromRow, fromColumn, toRow, toColumn int) { - fromRow, fromColumn = t.selectionStart.row, t.selectionStart.actualColumn - toRow, toColumn = t.cursor.row, t.cursor.actualColumn - if toRow < fromRow || (toRow == fromRow && toColumn < fromColumn) { - fromRow, fromColumn, toRow, toColumn = toRow, toColumn, fromRow, fromColumn - } - 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. - fromRow++ - fromColumn = 0 - } - if t.length > 0 && t.wrap && toColumn >= t.lastWidth { - toRow++ - toColumn = 0 - } - return -} - -// GetTextLength returns the string length of the text in the text area. -func (t *TextArea) GetTextLength() int { - return t.length -} - -// Replace replaces a section of the text with new text. The start and end -// positions refer to index positions within the entire text string (as a -// half-open interval). They may be the same, in which case text is inserted at -// the given position. If the text is an empty string, text between start and -// end is deleted. Index positions will be shifted to line up with character -// boundaries. -// -// Previous selections are cleared. The cursor will be located at the end of the -// replaced text. Scroll offsets will not be changed. -// -// The effects of this function can be undone (and redone) by the user. -func (t *TextArea) Replace(start, end int, text string) *TextArea { - t.Select(start, end) - row := t.selectionStart.row - t.cursor.pos = t.replace(t.selectionStart.pos, t.cursor.pos, text, false) - t.cursor.row = -1 - t.truncateLines(row - 1) - t.findCursor(false, row) - t.selectionStart = t.cursor - if t.changed != nil { - t.changed() - } - if t.moved != nil { - t.moved() - } - return t -} - -// Select selects a section of the text. The start and end positions refer to -// index positions within the entire text string (as a half-open interval). They -// may be the same, in which case the cursor is placed at the given position. -// Any previous selection is removed. Scroll offsets will be preserved. -// -// Index positions will be shifted to line up with character boundaries. -func (t *TextArea) Select(start, end int) *TextArea { - oldFrom, oldTo := t.selectionStart, t.cursor - defer func() { - if (oldFrom != t.selectionStart || oldTo != t.cursor) && t.moved != nil { - t.moved() - } - }() - - // Clamp input values. - if start < 0 { - start = 0 - } - if start > t.length { - start = t.length - } - if end < 0 { - end = 0 - } - if end > t.length { - end = t.length - } - if end < start { - start, end = end, start - } - - // Find the cursor positions. - var row, index int - t.cursor.row, t.cursor.pos = -1, [3]int{1, 0, -1} - t.selectionStart = t.cursor -RowLoop: - for { - if row >= len(t.lineStarts) { - t.extendLines(t.lastWidth, row) - if row >= len(t.lineStarts) { - break - } - } - - // Check the spans of this row. - pos := t.lineStarts[row] - var ( - next [3]int - lineIndex int - ) - if row+1 < len(t.lineStarts) { - next = t.lineStarts[row+1] - } else { - next = [3]int{1, 0, -1} - } - for { - if pos[0] == next[0] { - if start >= index+lineIndex && start < index+lineIndex+next[1]-pos[1] || - end >= index+lineIndex && end < index+lineIndex+next[1]-pos[1] { - break - } - index += lineIndex + next[1] - pos[1] - row++ - continue RowLoop // Move on to the next row. - } else { - length := t.spans[pos[0]].length - if length < 0 { - length = -length - } - if start >= index+lineIndex && start < index+lineIndex+length-pos[1] || - end >= index+lineIndex && end < index+lineIndex+length-pos[1] { - break - } - lineIndex += length - pos[1] - pos[0], pos[1] = t.spans[pos[0]].next, 0 - } - } - - // One of the indices is in this row. Step through it. - pos = t.lineStarts[row] - endPos := pos - var ( - cluster, text string - column, width int - ) - for pos != next { - if t.selectionStart.row < 0 && start <= index { - t.selectionStart.row, t.selectionStart.column, t.selectionStart.actualColumn = row, column, column - t.selectionStart.pos = pos - } - if t.cursor.row < 0 && end <= index { - t.cursor.row, t.cursor.column, t.cursor.actualColumn = row, column, column - t.cursor.pos = pos - break RowLoop - } - cluster, text, _, width, pos, endPos = t.step(text, pos, endPos) - index += len(cluster) - column += width - } - } - - if t.cursor.row < 0 { - t.findCursor(false, 0) // This only happens if we couldn't find the locations above. - t.selectionStart = t.cursor - } - - return t -} - -// SetWrap sets the flag that, if true, leads to lines that are longer than the -// available width being wrapped onto the next line. If false, any characters -// beyond the available width are not displayed. -func (t *TextArea) SetWrap(wrap bool) *TextArea { - if t.wrap != wrap { - t.wrap = wrap - t.reset() - } - return t -} - -// SetWordWrap sets the flag that causes lines that are longer than the -// available width to be wrapped onto the next line at spaces or after -// punctuation marks (according to [Unicode Standard Annex #14]). This flag is -// ignored if the flag set with [TextArea.SetWordWrap] is false. -// -// [Unicode Standard Annex #14]: https://www.unicode.org/reports/tr14/ -func (t *TextArea) SetWordWrap(wrapOnWords bool) *TextArea { - if t.wordWrap != wrapOnWords { - t.wordWrap = wrapOnWords - t.reset() - } - return t -} - -// SetPlaceholder sets the text to be displayed when the text area is empty. -func (t *TextArea) SetPlaceholder(placeholder string) *TextArea { - t.placeholder = placeholder - return t -} - -// SetMaxLength sets the maximum number of bytes allowed in the text area. If 0, -// there is no limit. If the text area currently contains more bytes than this, -// it may violate this constraint. -func (t *TextArea) SetMaxLength(maxLength int) *TextArea { - t.maxLength = maxLength - return t -} - -// SetTextStyle sets the style of the text. Background colors different from the -// Box's background color may lead to unwanted artefacts. -func (t *TextArea) SetTextStyle(style tcell.Style) *TextArea { - t.textStyle = style - return t -} - -// SetSelectedStyle sets the style of the selected text. -func (t *TextArea) SetSelectedStyle(style tcell.Style) *TextArea { - t.selectedStyle = style - return t -} - -// SetPlaceholderStyle sets the style of the placeholder text. -func (t *TextArea) SetPlaceholderStyle(style tcell.Style) *TextArea { - t.placeholderStyle = style - return t -} - -// GetOffset returns the text's offset, that is, the number of rows and columns -// skipped during drawing at the top or on the left, respectively. Note that the -// column offset is ignored if wrapping is enabled. -func (t *TextArea) GetOffset() (row, column int) { - return t.rowOffset, t.columnOffset -} - -// SetOffset sets the text's offset, that is, the number of rows and columns -// skipped during drawing at the top or on the left, respectively. If wrapping -// is enabled, the column offset is ignored. These values may get adjusted -// automatically to ensure that some text is always visible. -func (t *TextArea) SetOffset(row, column int) *TextArea { - t.rowOffset, t.columnOffset = row, column - return t -} - -// SetClipboard allows you to implement your own clipboard by providing a -// function that is called when the user wishes to store text in the clipboard -// (copyToClipboard) and a function that is called when the user wishes to -// retrieve text from the clipboard (pasteFromClipboard). -// -// Providing nil values will cause the default clipboard implementation to be -// used. -func (t *TextArea) SetClipboard(copyToClipboard func(string), pasteFromClipboard func() string) *TextArea { - t.copyToClipboard = copyToClipboard - if t.copyToClipboard == nil { - t.copyToClipboard = func(text string) { - t.clipboard = text - } - } - - t.pasteFromClipboard = pasteFromClipboard - if t.pasteFromClipboard == nil { - t.pasteFromClipboard = func() string { - return t.clipboard - } - } - - return t -} - -// SetChangedFunc sets a handler which is called whenever the text of the text -// area has changed. -func (t *TextArea) SetChangedFunc(handler func()) *TextArea { - t.changed = handler - return t -} - -// SetMovedFunc sets a handler which is called whenever the cursor position or -// the text selection has changed. -func (t *TextArea) SetMovedFunc(handler func()) *TextArea { - t.moved = handler - return t -} - -// replace deletes a range of text and inserts the given text at that position. -// If the resulting text would exceed the maximum length, the function does not -// do anything. The function returns the end position of the deleted/inserted -// range. The provided row is the row of the deleted range start. -// -// The function can hang if "deleteStart" is located after "deleteEnd". -// -// Undo events are always generated unless continuation is true and text is -// either appended to the end of a span or a span is shortened at the beginning -// or the end (and nothing else). -// -// This function does not modify [TextArea.lineStarts]. -func (t *TextArea) replace(deleteStart, deleteEnd [3]int, insert string, continuation bool) [3]int { - // Maybe nothing needs to be done? - if deleteStart == deleteEnd && insert == "" || t.maxLength > 0 && len(insert) > 0 && t.length+len(insert) >= t.maxLength { - return deleteEnd - } - - // Notify at the end. - if t.changed != nil { - defer t.changed() - } - - // Handle a few cases where we don't put anything onto the undo stack for - // increased efficiency. - if continuation { - // Same action as the one before. An undo item was already generated for - // this block of (same) actions. We're also only changing one character. - switch { - case insert == "" && deleteStart[1] != 0 && deleteEnd[1] == 0: - // Simple backspace. Just shorten this span. - length := t.spans[deleteStart[0]].length - if length < 0 { - t.length -= -length - deleteStart[1] - length = -deleteStart[1] - } else { - t.length -= length - deleteStart[1] - length = deleteStart[1] - } - t.spans[deleteStart[0]].length = length - return deleteEnd - case insert == "" && deleteStart[1] == 0 && deleteEnd[1] != 0: - // Simple delete. Just clip the beginning of this span. - t.spans[deleteEnd[0]].offset += deleteEnd[1] - if t.spans[deleteEnd[0]].length < 0 { - t.spans[deleteEnd[0]].length += deleteEnd[1] - } else { - t.spans[deleteEnd[0]].length -= deleteEnd[1] - } - t.length -= deleteEnd[1] - deleteEnd[1] = 0 - return deleteEnd - case insert != "" && deleteStart == deleteEnd && deleteEnd[1] == 0: - previous := t.spans[deleteStart[0]].previous - bufferSpan := t.spans[previous] - if bufferSpan.length > 0 && bufferSpan.offset+bufferSpan.length == t.editText.Len() { - // Typing individual characters. Simply extend the edit buffer. - length, _ := t.editText.WriteString(insert) - t.spans[previous].length += length - t.length += length - return deleteEnd - } - } - } - - // All other cases generate an undo item. - before := t.spans[deleteStart[0]].previous - after := deleteEnd[0] - if deleteEnd[1] > 0 { - after = t.spans[deleteEnd[0]].next - } - t.undoStack = t.undoStack[:t.nextUndo] - t.undoStack = append(t.undoStack, textAreaUndoItem{ - before: len(t.spans), - after: len(t.spans) + 1, - originalBefore: before, - originalAfter: after, - length: t.length, - pos: t.cursor.pos, - continuation: continuation, - }) - t.spans = append(t.spans, t.spans[before]) - t.spans = append(t.spans, t.spans[after]) - t.nextUndo++ - - // Adjust total text length by subtracting everything between "before" and - // "after". Inserted spans will be added back. - for index := deleteStart[0]; index != after; index = t.spans[index].next { - if t.spans[index].length < 0 { - t.length += t.spans[index].length - } else { - t.length -= t.spans[index].length - } - } - t.spans[before].next = after - t.spans[after].previous = before - - // We go from left to right, connecting new spans as needed. We update - // "before" as the span to connect new spans to. - - // If we start deleting in the middle of a span, connect a partial span. - if deleteStart[1] != 0 { - span := textAreaSpan{ - previous: before, - next: after, - offset: t.spans[deleteStart[0]].offset, - length: deleteStart[1], - } - if t.spans[deleteStart[0]].length < 0 { - span.length = -span.length - } - t.length += deleteStart[1] // This was previously subtracted. - t.spans[before].next = len(t.spans) - t.spans[after].previous = len(t.spans) - before = len(t.spans) - for row, lineStart := range t.lineStarts { // Also redirect line starts until the end of this new span. - if lineStart[0] == deleteStart[0] { - if lineStart[1] >= deleteStart[1] { - t.lineStarts = t.lineStarts[:row] // Everything else is unknown at this point. - break - } - t.lineStarts[row][0] = len(t.spans) - } - } - t.spans = append(t.spans, span) - } - - // If we insert text, connect a new span. - if insert != "" { - span := textAreaSpan{ - previous: before, - next: after, - offset: t.editText.Len(), - } - span.length, _ = t.editText.WriteString(insert) - t.length += span.length - t.spans[before].next = len(t.spans) - t.spans[after].previous = len(t.spans) - before = len(t.spans) - t.spans = append(t.spans, span) - } - - // If we stop deleting in the middle of a span, connect a partial span. - if deleteEnd[1] != 0 { - span := textAreaSpan{ - previous: before, - next: after, - offset: t.spans[deleteEnd[0]].offset + deleteEnd[1], - } - length := t.spans[deleteEnd[0]].length - if length < 0 { - span.length = length + deleteEnd[1] - t.length -= span.length // This was previously subtracted. - } else { - span.length = length - deleteEnd[1] - t.length += span.length // This was previously subtracted. - } - t.spans[before].next = len(t.spans) - t.spans[after].previous = len(t.spans) - deleteEnd[0], deleteEnd[1] = len(t.spans), 0 - t.spans = append(t.spans, span) - } - - return deleteEnd -} - -// Draw draws this primitive onto the screen. -func (t *TextArea) Draw(screen tcell.Screen) { - t.Box.DrawForSubclass(screen, t) - - // Prepare - x, y, width, height := t.GetInnerRect() - if width == 0 || height == 0 { - return // We have no space for anything. - } - columnOffset := t.columnOffset - if t.wrap { - columnOffset = 0 - } - - // Show/hide the cursor at the end. - defer func() { - if t.HasFocus() { - row, column := t.cursor.row, t.cursor.actualColumn - 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. - row++ - column = 0 - } - if row >= 0 && - row-t.rowOffset >= 0 && row-t.rowOffset < height && - column-columnOffset >= 0 && column-columnOffset < width { - screen.ShowCursor(x+column-columnOffset, y+row-t.rowOffset) - } else { - screen.HideCursor() - } - } - }() - - // Placeholder. - if t.length == 0 && len(t.placeholder) > 0 { - t.drawPlaceholder(screen, x, y, width, height) - return // We're done already. - } - - // Make sure the visible lines are broken over. - firstDrawing := t.lastWidth == 0 - if t.lastWidth != width && t.lineStarts != nil { - t.reset() - } - t.lastHeight, t.lastWidth = height, width - t.extendLines(width, t.rowOffset+height) - if len(t.lineStarts) <= t.rowOffset { - return // It's scrolled out of view. - } - - // If the cursor position is unknown, find it. This usually only happens - // before the screen is drawn for the first time. - if t.cursor.row < 0 { - t.findCursor(true, 0) - if t.selectionStart.row < 0 { - t.selectionStart = t.cursor - } - if firstDrawing && t.moved != nil { - t.moved() - } - } - - // Print the text. - var cluster, text string - line := t.rowOffset - pos := t.lineStarts[line] - endPos := pos - posX, posY := 0, 0 - for pos[0] != 1 { - var clusterWidth int - cluster, text, _, clusterWidth, pos, endPos = t.step(text, pos, endPos) - - // Prepare drawing. - runes := []rune(cluster) - style := t.selectedStyle - fromRow, fromColumn := t.cursor.row, t.cursor.actualColumn - toRow, toColumn := t.selectionStart.row, t.selectionStart.actualColumn - if fromRow > toRow || fromRow == toRow && fromColumn > toColumn { - fromRow, fromColumn, toRow, toColumn = toRow, toColumn, fromRow, fromColumn - } - if toRow < line || - toRow == line && toColumn <= posX || - fromRow > line || - fromRow == line && fromColumn > posX { - style = t.textStyle - } - - // Draw character. - if posX+clusterWidth-columnOffset <= width && posX-columnOffset >= 0 && clusterWidth > 0 { - screen.SetContent(x+posX-columnOffset, y+posY, runes[0], runes[1:], style) - } - - // Advance. - posX += clusterWidth - if line+1 < len(t.lineStarts) && t.lineStarts[line+1] == pos { - // We must break over. - posY++ - if posY >= height { - break // Done. - } - posX = 0 - line++ - } - } -} - -// drawPlaceholder draws the placeholder text into the given rectangle. It does -// not do anything if the text area already contains text or if there is no -// placeholder text. -func (t *TextArea) drawPlaceholder(screen tcell.Screen, x, y, width, height int) { - posX, posY := x, y - lastLineBreak, lastGraphemeBreak := x, x // Screen positions of the last possible line/grapheme break. - iterateString(t.placeholder, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool { - if posX+screenWidth > x+width { - // This character doesn't fit. Break over to the next line. - // Perform word wrapping first by copying the last word over to - // the next line. - clearX := lastLineBreak - if lastLineBreak == x { - clearX = lastGraphemeBreak - } - posY++ - if posY >= y+height { - return true - } - newPosX := x - for clearX < posX { - main, comb, _, _ := screen.GetContent(clearX, posY-1) - screen.SetContent(clearX, posY-1, ' ', nil, tcell.StyleDefault.Background(t.backgroundColor)) - screen.SetContent(newPosX, posY, main, comb, t.placeholderStyle) - clearX++ - newPosX++ - } - lastLineBreak, lastGraphemeBreak, posX = x, x, newPosX - } - - // Draw this character. - screen.SetContent(posX, posY, main, comb, t.placeholderStyle) - posX += screenWidth - switch boundaries & uniseg.MaskLine { - case uniseg.LineMustBreak: - posY++ - if posY >= y+height { - return true - } - posX = x - case uniseg.LineCanBreak: - lastLineBreak = posX - } - lastGraphemeBreak = posX - - return false - }) -} - -// reset resets many of the local variables of the text area because they cannot -// be used anymore and must be recalculated, typically after the text area's -// size has changed. -func (t *TextArea) reset() { - t.truncateLines(0) - if t.wrap { - t.cursor.row = -1 - t.selectionStart.row = -1 - } - t.widestLine = 0 -} - -// extendLines traverses the current text and extends [TextArea.lineStarts] such -// that it describes at least maxLines+1 lines (or less if the text is shorter). -// Text is laid out for the given width while respecting the wrapping settings. -// It is assumed that if [TextArea.lineStarts] already has entries, they obey -// the same rules. -// -// If width is 0, nothing happens. -func (t *TextArea) extendLines(width, maxLines int) { - if width <= 0 { - return - } - - // Start with the first span. - if len(t.lineStarts) == 0 { - if len(t.spans) > 2 { - t.lineStarts = append(t.lineStarts, [3]int{t.spans[0].next, 0, -1}) - } else { - return // No text. - } - } - - // Determine starting positions and starting spans. - pos := t.lineStarts[len(t.lineStarts)-1] // The starting position is the last known line. - endPos := pos - var ( - cluster, text string - lineWidth, clusterWidth, boundaries int - lastGraphemeBreak, lastLineBreak [3]int - widthSinceLineBreak int - ) - for pos[0] != 1 { - // Get the next grapheme cluster. - cluster, text, boundaries, clusterWidth, pos, endPos = t.step(text, pos, endPos) - lineWidth += clusterWidth - widthSinceLineBreak += clusterWidth - - // Any line breaks? - if !t.wrap || lineWidth <= width { - if boundaries&uniseg.MaskLine == uniseg.LineMustBreak && (len(text) > 0 || uniseg.HasTrailingLineBreakInString(cluster)) { - // We must break over. - t.lineStarts = append(t.lineStarts, pos) - if lineWidth > t.widestLine { - t.widestLine = lineWidth - } - lineWidth = 0 - lastGraphemeBreak = [3]int{} - lastLineBreak = [3]int{} - widthSinceLineBreak = 0 - if len(t.lineStarts) > maxLines { - break // We have enough lines, we can stop. - } - continue - } - } else { // t.wrap && lineWidth > width - if !t.wordWrap || lastLineBreak == [3]int{} { - if lastGraphemeBreak != [3]int{} { // We have at least one character on each line. - // Break after last grapheme. - t.lineStarts = append(t.lineStarts, lastGraphemeBreak) - if lineWidth > t.widestLine { - t.widestLine = lineWidth - } - lineWidth = clusterWidth - lastLineBreak = [3]int{} - } - } else { // t.wordWrap && lastLineBreak != [3]int{} - // Break after last line break opportunity. - t.lineStarts = append(t.lineStarts, lastLineBreak) - if lineWidth > t.widestLine { - t.widestLine = lineWidth - } - lineWidth = widthSinceLineBreak - lastLineBreak = [3]int{} - } - } - - // Analyze break opportunities. - if boundaries&uniseg.MaskLine == uniseg.LineCanBreak { - lastLineBreak = pos - widthSinceLineBreak = 0 - } - lastGraphemeBreak = pos - - // Can we stop? - if len(t.lineStarts) > maxLines { - break - } - } -} - -// truncateLines truncates the trailing lines of the [TextArea.lineStarts] -// slice such that len(lineStarts) <= fromLine. If fromLine is negative, a value -// of 0 is assumed. If it is greater than the length of lineStarts, nothing -// happens. -func (t *TextArea) truncateLines(fromLine int) { - if fromLine < 0 { - fromLine = 0 - } - if fromLine < len(t.lineStarts) { - t.lineStarts = t.lineStarts[:fromLine] - } -} - -// findCursor determines the cursor position if its "row" value is < 0 -// (=unknown) but only its span position ("pos" value) is known. If the cursor -// position is already known (row >= 0), it can also be used to modify row and -// column offsets such that the cursor is visible during the next call to -// [TextArea.Draw], by setting "clamp" to true. -// -// To determine the cursor position, "startRow" helps reduce processing time by -// indicating the lowest row in which searching should start. Set this to 0 if -// you don't have any information where the cursor might be (but know that this -// is expensive for long texts). -func (t *TextArea) findCursor(clamp bool, startRow int) { - if !clamp && t.cursor.row >= 0 { - return // Nothing to do. - } - - // Clamp to viewport. - if clamp && t.cursor.row >= 0 { - cursorRow := t.cursor.row - if t.wrap && t.cursor.actualColumn >= t.lastWidth { - cursorRow++ // A row can push the cursor just outside the viewport. It will wrap onto the next line. - } - if cursorRow < t.rowOffset { - // We're above the viewport. - t.rowOffset = cursorRow - } else if cursorRow >= t.rowOffset+t.lastHeight { - // We're below the viewport. - t.rowOffset = cursorRow - t.lastHeight + 1 - if t.rowOffset >= len(t.lineStarts) { - t.extendLines(t.lastWidth, t.rowOffset) - if t.rowOffset >= len(t.lineStarts) { - t.rowOffset = len(t.lineStarts) - 1 - if t.rowOffset < 0 { - t.rowOffset = 0 - } - } - } - } - if !t.wrap { - if t.cursor.actualColumn < t.columnOffset+minCursorPrefix { - // We're left of the viewport. - t.columnOffset = t.cursor.actualColumn - minCursorPrefix - if t.columnOffset < 0 { - t.columnOffset = 0 - } - } else if t.cursor.actualColumn >= t.columnOffset+t.lastWidth-minCursorSuffix { - // We're right of the viewport. - t.columnOffset = t.cursor.actualColumn - t.lastWidth + minCursorSuffix - if t.columnOffset >= t.widestLine { - t.columnOffset = t.widestLine - 1 - if t.columnOffset < 0 { - t.columnOffset = 0 - } - } - } - } - return - } - - // The screen position of the cursor is unknown. Find it. This can be - // expensive. First, find the row. - row := startRow - if row < 0 { - row = 0 - } -RowLoop: - for { - // Examine the current row. - if row+1 >= len(t.lineStarts) { - t.extendLines(t.lastWidth, row+1) - } - if row >= len(t.lineStarts) { - t.cursor.row, t.cursor.actualColumn, t.cursor.pos = row, 0, [3]int{1, 0, -1} - break // It's the end of the text. - } - - // Check this row's spans to see if the cursor is in this row. - pos := t.lineStarts[row] - for pos[0] != 1 { - if row+1 >= len(t.lineStarts) { - break // It's the last row so the cursor must be in this row. - } - if t.cursor.pos[0] == pos[0] { - // The cursor is in this span. - if t.lineStarts[row+1][0] == pos[0] { - // The next row starts with the same span. - if t.cursor.pos[1] >= t.lineStarts[row+1][1] { - // The cursor is not in this row. - row++ - continue RowLoop - } else { - // The cursor is in this row. - break - } - } else { - // The next row starts with a different span. The cursor - // must be in this row. - break - } - } else { - // The cursor is in a different span. - if t.lineStarts[row+1][0] == pos[0] { - // The next row starts with the same span. This row is - // irrelevant. - row++ - continue RowLoop - } else { - // The next row starts with a different span. Move towards it. - pos = [3]int{t.spans[pos[0]].next, 0, -1} - } - } - } - - // Try to find the screen position in this row. - pos = t.lineStarts[row] - endPos := pos - column := 0 - var text string - for { - if pos[0] == 1 || t.cursor.pos[0] == pos[0] && t.cursor.pos[1] == pos[1] { - // We found the position. We're done. - t.cursor.row, t.cursor.actualColumn, t.cursor.pos = row, column, pos - break RowLoop - } - var clusterWidth int - _, text, _, clusterWidth, pos, endPos = t.step(text, pos, endPos) - if row+1 < len(t.lineStarts) && t.lineStarts[row+1] == pos { - // We reached the end of the line. Go to the next one. - row++ - continue RowLoop - } - column += clusterWidth - } - } - - if clamp && t.cursor.row >= 0 { - // We know the position now. Adapt offsets. - t.findCursor(true, startRow) - } -} - -// step is similar to [github.com/rivo/uniseg.StepString] but it iterates over -// the piece chain, starting with "pos", a span position plus state (which may -// be -1 for the start of the text). The returned "boundaries" value is same -// value returned by [github.com/rivo/uniseg.StepString], "width" is the screen -// width of the grapheme. The "pos" and "endPos" positions refer to the start -// and the end of the "text" string, respectively. For the first call, text may -// be empty and pos/endPos may be the same. For consecutive calls, provide -// "rest" as the text and "newPos" and "newEndPos" as the new positions/states. -// An empty "rest" string indicates the end of the text. The "endPos" state is -// irrelevant. -func (t *TextArea) step(text string, pos, endPos [3]int) (cluster, rest string, boundaries, width int, newPos, newEndPos [3]int) { - if pos[0] == 1 { - return // We're already past the end. - } - - // We want to make sure we have a text at least the size of a grapheme - // cluster. - span := t.spans[pos[0]] - if len(text) < maxGraphemeClusterSize && - (span.length < 0 && -span.length-pos[1] >= maxGraphemeClusterSize || - span.length > 0 && t.spans[pos[0]].length-pos[1] >= maxGraphemeClusterSize) { - // We can use a substring of one span. - if span.length < 0 { - text = t.initialText[span.offset+pos[1] : span.offset-span.length] - } else { - text = t.editText.String()[span.offset+pos[1] : span.offset+span.length] - } - endPos = [3]int{span.next, 0, -1} - } else { - // We have to compose the text from multiple spans. - for len(text) < maxGraphemeClusterSize && endPos[0] != 1 { - endSpan := t.spans[endPos[0]] - var moreText string - if endSpan.length < 0 { - moreText = t.initialText[endSpan.offset+endPos[1] : endSpan.offset-endSpan.length] - } else { - moreText = t.editText.String()[endSpan.offset+endPos[1] : endSpan.offset+endSpan.length] - } - if len(moreText) > maxGraphemeClusterSize { - moreText = moreText[:maxGraphemeClusterSize] - } - text += moreText - endPos[1] += len(moreText) - if endPos[1] >= endSpan.length { - endPos[0], endPos[1] = endSpan.next, 0 - } - } - } - - // Run the grapheme cluster iterator. - cluster, text, boundaries, pos[2] = uniseg.StepString(text, pos[2]) - pos[1] += len(cluster) - for pos[0] != 1 && (span.length < 0 && pos[1] >= -span.length || span.length >= 0 && pos[1] >= span.length) { - pos[0] = span.next - if span.length < 0 { - pos[1] += span.length - } else { - pos[1] -= span.length - } - span = t.spans[pos[0]] - } - - if cluster == "\t" { - width = TabSize - } else { - width = stringWidth(cluster) - } - - return cluster, text, boundaries, width, pos, endPos -} - -// moveCursor sets the cursor's screen position and span position for the given -// row and column which are screen space coordinates relative to the top-left -// corner of the text area's full text (visible or not). The column value may be -// negative, in which case, the cursor will be placed at the end of the line. -// The cursor's actual position will be aligned with a grapheme cluster -// boundary. The next call to [TextArea.Draw] will attempt to keep the cursor in -// the viewport. -func (t *TextArea) moveCursor(row, column int) { - // Are we within the range of rows? - if len(t.lineStarts) <= row { - // No. Extent the line buffer. - t.extendLines(t.lastWidth, row) - } - if len(t.lineStarts) == 0 { - return // No lines. Nothing to do. - } - if row < 0 { - // We're at the start of the text. - row = 0 - column = 0 - } else if row >= len(t.lineStarts) { - // We're already past the end. - row = len(t.lineStarts) - 1 - column = -1 - } - - // Iterate through this row until we find the position. - t.cursor.row, t.cursor.actualColumn = row, 0 - if t.wrap { - t.cursor.actualColumn = 0 - } - pos := t.lineStarts[row] - endPos := pos - var text string - for pos[0] != 1 { - var clusterWidth int - oldPos := pos // We may have to revert to this position. - _, text, _, clusterWidth, pos, endPos = t.step(text, pos, endPos) - if len(t.lineStarts) > row+1 && pos == t.lineStarts[row+1] || // We've reached the end of the line. - column >= 0 && t.cursor.actualColumn+clusterWidth > column { // We're past the requested column. - pos = oldPos - break - } - t.cursor.actualColumn += clusterWidth - } - - if column < 0 { - t.cursor.column = t.cursor.actualColumn - } else { - t.cursor.column = column - } - t.cursor.pos = pos - t.findCursor(true, row) -} - -// moveWordRight moves the cursor to the end of the current or next word. If -// after is set to true, the cursor will be placed after the word. If false, the -// cursor will be placed on the last character of the word. If clamp is set to -// true, the cursor will be visible during the next call to [TextArea.Draw]. -func (t *TextArea) moveWordRight(after, clamp bool) { - // Because we rely on clampToCursor to calculate the new screen position, - // this is an expensive operation for large texts. - pos := t.cursor.pos - endPos := pos - var ( - cluster, text string - inWord bool - ) - for pos[0] != 0 { - var boundaries int - oldPos := pos - cluster, text, boundaries, _, pos, endPos = t.step(text, pos, endPos) - if oldPos == t.cursor.pos { - continue // Skip the first character. - } - firstRune, _ := utf8.DecodeRuneInString(cluster) - if !unicode.IsSpace(firstRune) && !unicode.IsPunct(firstRune) { - inWord = true - } - if inWord && boundaries&uniseg.MaskWord != 0 { - if !after { - pos = oldPos - } - break - } - } - startRow := t.cursor.row - t.cursor.row, t.cursor.column, t.cursor.actualColumn = -1, 0, 0 - t.cursor.pos = pos - t.findCursor(clamp, startRow) -} - -// moveWordLeft moves the cursor to the beginning of the current or previous -// word. If clamp is true, the cursor will be visible during the next call to -// [TextArea.Draw]. -func (t *TextArea) moveWordLeft(clamp bool) { - // We go back row by row, trying to find the last word boundary before the - // cursor. - row := t.cursor.row - if row+1 < len(t.lineStarts) { - t.extendLines(t.lastWidth, row+1) - } - if row >= len(t.lineStarts) { - row = len(t.lineStarts) - 1 - } - for row >= 0 { - pos := t.lineStarts[row] - endPos := pos - var lastWordBoundary [3]int - var ( - cluster, text string - inWord bool - boundaries int - ) - for pos[0] != 1 && pos != t.cursor.pos { - oldBoundaries := boundaries - oldPos := pos - cluster, text, boundaries, _, pos, endPos = t.step(text, pos, endPos) - firstRune, _ := utf8.DecodeRuneInString(cluster) - wordRune := !unicode.IsSpace(firstRune) && !unicode.IsPunct(firstRune) - if oldBoundaries&uniseg.MaskWord != 0 { - if pos != t.cursor.pos && !inWord && wordRune { - // A boundary transitioning from a space/punctuation word to - // a letter word. - lastWordBoundary = oldPos - } - inWord = false - } - if wordRune { - inWord = true - } - } - if lastWordBoundary[0] != 0 { - // We found something. - t.cursor.pos = lastWordBoundary - break - } - row-- - } - if row < 0 { - // We didn't find anything. We're at the start of the text. - t.cursor.pos = [3]int{t.spans[0].next, 0, -1} - row = 0 - } - t.cursor.row, t.cursor.column, t.cursor.actualColumn = -1, 0, 0 - t.findCursor(clamp, row) -} - -// deleteLine deletes all characters between the last newline before the cursor -// and the next newline after the cursor (inclusive). -func (t *TextArea) deleteLine() { - // We go back row by row, trying to find the last mandatory line break - // before the cursor. - startRow := t.cursor.row - if t.cursor.actualColumn == 0 && t.cursor.pos[0] == 1 { - startRow-- // If we're at the very end, delete the row before. - } - if startRow+1 < len(t.lineStarts) { - t.extendLines(t.lastWidth, startRow+1) - } - if len(t.lineStarts) == 0 { - return // Nothing to delete. - } - if startRow >= len(t.lineStarts) { - startRow = len(t.lineStarts) - 1 - } - for startRow >= 0 { - // What's the last rune before the start of the line? - pos := t.lineStarts[startRow] - span := t.spans[pos[0]] - var text string - if pos[1] > 0 { - // Extract text from this span. - if span.length < 0 { - text = t.initialText - } else { - text = t.editText.String() - } - text = text[:span.offset+pos[1]] - } else { - // Extract text from the previous span. - if span.previous != 0 { - span = t.spans[span.previous] - if span.length < 0 { - text = t.initialText[:span.offset-span.length] - } else { - text = t.editText.String()[:span.offset+span.length] - } - } - } - if uniseg.HasTrailingLineBreakInString(text) { - // The row before this one ends with a mandatory line break. This is - // the first line we will delete. - break - } - startRow-- - } - if startRow < 0 { - // We didn't find anything. It'll be the first line. - startRow = 0 - } - - // Find the next line break after the cursor. - pos := t.cursor.pos - endPos := pos - var cluster, text string - for pos[0] != 1 { - cluster, text, _, _, pos, endPos = t.step(text, pos, endPos) - if uniseg.HasTrailingLineBreakInString(cluster) { - break - } - } - - // Delete the text. - t.cursor.pos = t.replace(t.lineStarts[startRow], pos, "", false) - t.cursor.row = -1 - t.truncateLines(startRow) - t.findCursor(true, startRow) -} - -// getSelection returns the current selection as span locations where the first -// returned location is always before or the same as the second returned -// location. This assumes that the cursor and selection positions are known. The -// third return value is the starting row of the selection. -func (t *TextArea) getSelection() ([3]int, [3]int, int) { - from := t.selectionStart.pos - to := t.cursor.pos - row := t.selectionStart.row - if t.cursor.row < t.selectionStart.row || - (t.cursor.row == t.selectionStart.row && t.cursor.actualColumn < t.selectionStart.actualColumn) { - from, to = to, from - row = t.cursor.row - } - return from, to, row -} - -// getSelectedText returns the text of the current selection. -func (t *TextArea) getSelectedText() string { - var text strings.Builder - - from, to, _ := t.getSelection() - for from[0] != to[0] { - span := t.spans[from[0]] - if span.length < 0 { - text.WriteString(t.initialText[span.offset+from[1] : span.offset-span.length]) - } else { - text.WriteString(t.editText.String()[span.offset+from[1] : span.offset+span.length]) - } - from[0], from[1] = span.next, 0 - } - if from[0] != 1 && from[1] < to[1] { - span := t.spans[from[0]] - if span.length < 0 { - text.WriteString(t.initialText[span.offset+from[1] : span.offset+to[1]]) - } else { - text.WriteString(t.editText.String()[span.offset+from[1] : span.offset+to[1]]) - } - } - - return text.String() -} - -// InputHandler returns the handler for this primitive. -func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { - return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { - // All actions except a few specific ones are "other" actions. - newLastAction := taActionOther - defer func() { - t.lastAction = newLastAction - }() - - // Trigger a "moved" event if requested. - if t.moved != nil { - selectionStart, cursor := t.selectionStart, t.cursor - defer func() { - if selectionStart != t.selectionStart || cursor != t.cursor { - t.moved() - } - }() - } - - // Process the different key events. - switch key := event.Key(); key { - case tcell.KeyLeft: // Move one grapheme cluster to the left. - if event.Modifiers()&tcell.ModAlt == 0 { - // Regular movement. - if event.Modifiers()&tcell.ModShift == 0 && t.selectionStart.pos != t.cursor.pos { - // Move to the start of the selection. - if t.selectionStart.row < t.cursor.row || (t.selectionStart.row == t.cursor.row && t.selectionStart.actualColumn < t.cursor.actualColumn) { - t.cursor = t.selectionStart - } - t.findCursor(true, t.cursor.row) - } else if event.Modifiers()&tcell.ModMeta != 0 || event.Modifiers()&tcell.ModCtrl != 0 { - // This captures Ctrl-Left on some systems. - t.moveWordLeft(event.Modifiers()&tcell.ModShift != 0) - } else if t.cursor.actualColumn == 0 { - // Move to the end of the previous row. - if t.cursor.row > 0 { - t.moveCursor(t.cursor.row-1, -1) - } - } else { - // Move one grapheme cluster to the left. - t.moveCursor(t.cursor.row, t.cursor.actualColumn-1) - } - if event.Modifiers()&tcell.ModShift == 0 { - t.selectionStart = t.cursor - } - } else if !t.wrap { // This doesn't work on all terminals. - // Just scroll. - t.columnOffset-- - if t.columnOffset < 0 { - t.columnOffset = 0 - } - } - case tcell.KeyRight: // Move one grapheme cluster to the right. - if event.Modifiers()&tcell.ModAlt == 0 { - // Regular movement. - if event.Modifiers()&tcell.ModShift == 0 && t.selectionStart.pos != t.cursor.pos { - // Move to the end of the selection. - if t.selectionStart.row > t.cursor.row || (t.selectionStart.row == t.cursor.row && t.selectionStart.actualColumn > t.cursor.actualColumn) { - t.cursor = t.selectionStart - } - t.findCursor(true, t.cursor.row) - } else if t.cursor.pos[0] != 1 { - if event.Modifiers()&tcell.ModMeta != 0 || event.Modifiers()&tcell.ModCtrl != 0 { - // This captures Ctrl-Right on some systems. - t.moveWordRight(event.Modifiers()&tcell.ModShift != 0, true) - } else { - // Move one grapheme cluster to the right. - var clusterWidth int - _, _, _, clusterWidth, t.cursor.pos, _ = t.step("", t.cursor.pos, t.cursor.pos) - if len(t.lineStarts) <= t.cursor.row+1 { - t.extendLines(t.lastWidth, t.cursor.row+1) - } - if t.cursor.row+1 < len(t.lineStarts) && t.lineStarts[t.cursor.row+1] == t.cursor.pos { - // We've reached the end of the line. - t.cursor.row++ - t.cursor.actualColumn = 0 - t.cursor.column = 0 - t.findCursor(true, t.cursor.row) - } else { - // Move one character to the right. - t.moveCursor(t.cursor.row, t.cursor.actualColumn+clusterWidth) - } - } - } - if event.Modifiers()&tcell.ModShift == 0 { - t.selectionStart = t.cursor - } - } else if !t.wrap { // This doesn't work on all terminals. - // Just scroll. - t.columnOffset++ - if t.columnOffset >= t.widestLine { - t.columnOffset = t.widestLine - 1 - if t.columnOffset < 0 { - t.columnOffset = 0 - } - } - } - case tcell.KeyDown: // Move one row down. - if event.Modifiers()&tcell.ModAlt == 0 { - // Regular movement. - t.moveCursor(t.cursor.row+1, t.cursor.column) - if event.Modifiers()&tcell.ModShift == 0 { - t.selectionStart = t.cursor - } - } else { - // Just scroll. - t.rowOffset++ - if t.rowOffset >= len(t.lineStarts) { - t.extendLines(t.lastWidth, t.rowOffset) - if t.rowOffset >= len(t.lineStarts) { - t.rowOffset = len(t.lineStarts) - 1 - if t.rowOffset < 0 { - t.rowOffset = 0 - } - } - } - } - case tcell.KeyUp: // Move one row up. - if event.Modifiers()&tcell.ModAlt == 0 { - // Regular movement. - t.moveCursor(t.cursor.row-1, t.cursor.column) - if event.Modifiers()&tcell.ModShift == 0 { - t.selectionStart = t.cursor - } - } else { - // Just scroll. - t.rowOffset-- - if t.rowOffset < 0 { - t.rowOffset = 0 - } - } - case tcell.KeyHome, tcell.KeyCtrlA: // Move to the start of the line. - t.moveCursor(t.cursor.row, 0) - if event.Modifiers()&tcell.ModShift == 0 { - t.selectionStart = t.cursor - } - case tcell.KeyEnd, tcell.KeyCtrlE: // Move to the end of the line. - t.moveCursor(t.cursor.row, -1) - if event.Modifiers()&tcell.ModShift == 0 { - t.selectionStart = t.cursor - } - case tcell.KeyPgDn, tcell.KeyCtrlF: // Move one page down. - t.moveCursor(t.cursor.row+t.lastHeight, t.cursor.column) - if event.Modifiers()&tcell.ModShift == 0 { - t.selectionStart = t.cursor - } - case tcell.KeyPgUp, tcell.KeyCtrlB: // Move one page up. - t.moveCursor(t.cursor.row-t.lastHeight, t.cursor.column) - if event.Modifiers()&tcell.ModShift == 0 { - t.selectionStart = t.cursor - } - case tcell.KeyEnter: // Insert a newline. - from, to, row := t.getSelection() - t.cursor.pos = t.replace(from, to, NewLine, t.lastAction == taActionTypeSpace) - t.cursor.row = -1 - t.truncateLines(row - 1) - t.findCursor(true, row) - t.selectionStart = t.cursor - newLastAction = taActionTypeSpace - case tcell.KeyTab: // Insert a tab character. It will be rendered as TabSize spaces. - from, to, row := t.getSelection() - t.cursor.pos = t.replace(from, to, "\t", t.lastAction == taActionTypeSpace) - t.cursor.row = -1 - t.truncateLines(row - 1) - t.findCursor(true, row) - t.selectionStart = t.cursor - newLastAction = taActionTypeSpace - case tcell.KeyRune: - if event.Modifiers()&tcell.ModAlt > 0 { - // We accept some Alt- key combinations. - switch event.Rune() { - case 'f': - if event.Modifiers()&tcell.ModShift == 0 { - t.moveWordRight(false, true) - t.selectionStart = t.cursor - } else { - t.moveWordRight(true, true) - } - case 'b': - t.moveWordLeft(true) - if event.Modifiers()&tcell.ModShift == 0 { - t.selectionStart = t.cursor - } - } - } else { - // Other keys are simply accepted as regular characters. - r := event.Rune() - from, to, row := t.getSelection() - newLastAction = taActionTypeNonSpace - if unicode.IsSpace(r) { - newLastAction = taActionTypeSpace - } - t.cursor.pos = t.replace(from, to, string(r), newLastAction == t.lastAction || t.lastAction == taActionTypeNonSpace && newLastAction == taActionTypeSpace) - t.cursor.row = -1 - t.truncateLines(row - 1) - t.findCursor(true, row) - t.selectionStart = t.cursor - } - case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete backwards. tcell.KeyBackspace is the same as tcell.CtrlH. - from, to, row := t.getSelection() - if from != to { - // Simply delete the current selection. - t.cursor.pos = t.replace(from, to, "", false) - t.cursor.row = -1 - t.truncateLines(row - 1) - t.findCursor(true, row) - t.selectionStart = t.cursor - break - } - - beforeCursor := t.cursor - if event.Modifiers()&tcell.ModAlt == 0 { - // Move the cursor back by one grapheme cluster. - if t.cursor.actualColumn == 0 { - // Move to the end of the previous row. - if t.cursor.row > 0 { - t.moveCursor(t.cursor.row-1, -1) - } - } else { - // Move one grapheme cluster to the left. - t.moveCursor(t.cursor.row, t.cursor.actualColumn-1) - } - newLastAction = taActionBackspace - } else { - // Move the cursor back by one word. - t.moveWordLeft(false) - } - - // Remove that last grapheme cluster. - if t.cursor.pos != beforeCursor.pos { - t.cursor, beforeCursor = beforeCursor, t.cursor // So we put the right position on the stack. - t.cursor.pos = t.replace(beforeCursor.pos, t.cursor.pos, "", t.lastAction == taActionBackspace) // Delete the character. - t.cursor.row = -1 - t.truncateLines(beforeCursor.row - 1) - t.findCursor(true, beforeCursor.row-1) - } - t.selectionStart = t.cursor - case tcell.KeyDelete, tcell.KeyCtrlD: // Delete forward. - from, to, row := t.getSelection() - if from != to { - // Simply delete the current selection. - t.cursor.pos = t.replace(from, to, "", false) - t.cursor.row = -1 - t.truncateLines(row - 1) - t.findCursor(true, row) - t.selectionStart = t.cursor - break - } - - if t.cursor.pos[0] != 1 { - _, _, _, _, endPos, _ := t.step("", t.cursor.pos, t.cursor.pos) - t.cursor.pos = t.replace(t.cursor.pos, endPos, "", t.lastAction == taActionDelete) // Delete the character. - t.cursor.pos[2] = endPos[2] - t.truncateLines(t.cursor.row - 1) - t.findCursor(true, t.cursor.row) - newLastAction = taActionDelete - } - t.selectionStart = t.cursor - case tcell.KeyCtrlK: // Delete everything under and to the right of the cursor until before the next newline character. - pos := t.cursor.pos - endPos := pos - var cluster, text string - for pos[0] != 1 { - var boundaries int - oldPos := pos - cluster, text, boundaries, _, pos, endPos = t.step(text, pos, endPos) - if boundaries&uniseg.MaskLine == uniseg.LineMustBreak { - if uniseg.HasTrailingLineBreakInString(cluster) { - pos = oldPos - } - break - } - } - t.cursor.pos = t.replace(t.cursor.pos, pos, "", false) - row := t.cursor.row - t.cursor.row = -1 - t.truncateLines(row - 1) - t.findCursor(true, row) - t.selectionStart = t.cursor - case tcell.KeyCtrlW: // Delete from the start of the current word to the left of the cursor. - pos := t.cursor.pos - t.moveWordLeft(true) - t.cursor.pos = t.replace(t.cursor.pos, pos, "", false) - row := t.cursor.row - 1 - t.cursor.row = -1 - t.truncateLines(row) - t.findCursor(true, row) - t.selectionStart = t.cursor - case tcell.KeyCtrlU: // Delete the current line. - t.deleteLine() - t.selectionStart = t.cursor - case tcell.KeyCtrlL: // Select everything. - t.selectionStart.row, t.selectionStart.column, t.selectionStart.actualColumn = 0, 0, 0 - t.selectionStart.pos = [3]int{t.spans[0].next, 0, -1} - row := t.cursor.row - t.cursor.row = -1 - t.cursor.pos = [3]int{1, 0, -1} - t.findCursor(false, row) - case tcell.KeyCtrlQ: // Copy to clipboard. - if t.cursor != t.selectionStart { - t.copyToClipboard(t.getSelectedText()) - t.selectionStart = t.cursor - } - case tcell.KeyCtrlX: // Cut to clipboard. - if t.cursor != t.selectionStart { - t.copyToClipboard(t.getSelectedText()) - from, to, row := t.getSelection() - t.cursor.pos = t.replace(from, to, "", false) - t.cursor.row = -1 - t.truncateLines(row - 1) - t.findCursor(true, row) - t.selectionStart = t.cursor - } - case tcell.KeyCtrlV: // Paste from clipboard. - from, to, row := t.getSelection() - t.cursor.pos = t.replace(from, to, t.pasteFromClipboard(), false) - t.cursor.row = -1 - t.truncateLines(row - 1) - t.findCursor(true, row) - t.selectionStart = t.cursor - case tcell.KeyCtrlZ: // Undo. - if t.nextUndo <= 0 { - break - } - for t.nextUndo > 0 { - t.nextUndo-- - undo := t.undoStack[t.nextUndo] - t.spans[undo.originalBefore], t.spans[undo.before] = t.spans[undo.before], t.spans[undo.originalBefore] - t.spans[undo.originalAfter], t.spans[undo.after] = t.spans[undo.after], t.spans[undo.originalAfter] - t.cursor.pos, t.undoStack[t.nextUndo].pos = undo.pos, t.cursor.pos - t.length, t.undoStack[t.nextUndo].length = undo.length, t.length - if !undo.continuation { - break - } - } - t.cursor.row = -1 - t.truncateLines(0) // This is why Undo is expensive for large texts. (t.lineStarts can get largely unusable after an undo.) - t.findCursor(true, 0) - t.selectionStart = t.cursor - if t.changed != nil { - defer t.changed() - } - case tcell.KeyCtrlY: // Redo. - if t.nextUndo >= len(t.undoStack) { - break - } - for t.nextUndo < len(t.undoStack) { - undo := t.undoStack[t.nextUndo] - t.spans[undo.originalBefore], t.spans[undo.before] = t.spans[undo.before], t.spans[undo.originalBefore] - t.spans[undo.originalAfter], t.spans[undo.after] = t.spans[undo.after], t.spans[undo.originalAfter] - t.cursor.pos, t.undoStack[t.nextUndo].pos = undo.pos, t.cursor.pos - t.length, t.undoStack[t.nextUndo].length = undo.length, t.length - t.nextUndo++ - if t.nextUndo < len(t.undoStack) && !t.undoStack[t.nextUndo].continuation { - break - } - } - t.cursor.row = -1 - t.truncateLines(0) // This is why Redo is expensive for large texts. (t.lineStarts can get largely unusable after an undo.) - t.findCursor(true, 0) - t.selectionStart = t.cursor - if t.changed != nil { - defer t.changed() - } - } - }) -} - -// THIS FUNCTION WILL BE REMOVED ONCE WE DEEM THE TEXT AREA STABLE! DO NOT USE! -func (t *TextArea) Dump() string { - var buf strings.Builder - - // Dump spans. - buf.WriteString("Spans:\n\n") - index := 0 - for index >= 0 { - span := t.spans[index] - var text string - if span.length < 0 { - text = t.initialText[span.offset : span.offset-span.length] - } else { - text = t.editText.String()[span.offset : span.offset+span.length] - } - if len(text) > 9 { - text = fmt.Sprintf("%s...%s", text[:3], text[len(text)-3:]) - } - fmt.Fprintf(&buf, `[blue]%d:[white]{[yellow]%d[white] %q [red]%d[white]} `, index, span.previous, text, span.next) - index = span.next - } - - // Dump undo stack. - buf.WriteString("\n\nUndo stack:\n\n") - for undoIndex, undo := range t.undoStack { - if t.nextUndo == undoIndex { - buf.WriteString("^^^^^^\n") - } - fmt.Fprintf(&buf, `[yellow]%d[white]>[blue]%d[yellow] `, undo.before, undo.originalBefore) - index := undo.before - for { - if index == undo.originalAfter { - index = undo.after - } - span := t.spans[index] - var text string - if span.length < 0 { - text = t.initialText[span.offset : span.offset-span.length] - } else { - text = t.editText.String()[span.offset : span.offset+span.length] - } - if len(text) > 9 { - text = fmt.Sprintf("%s...%s", text[:3], text[len(text)-3:]) - } - fmt.Fprintf(&buf, `[blue]%d:[white]{[yellow]%d[white] %q [red]%d[white]} `, index, span.previous, text, span.next) - if index == undo.after { - break - } - index = span.next - } - fmt.Fprintf(&buf, "[yellow]%d[white]>[blue]%d[yellow]", undo.after, undo.originalAfter) - fmt.Fprintf(&buf, " %d\n", undo.pos) - } - - return buf.String() -} - -// MouseHandler returns the mouse handler for this primitive. -func (t *TextArea) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - x, y := event.Position() - rectX, rectY, _, _ := t.GetInnerRect() - if !t.InRect(x, y) { - return false, nil - } - - // Trigger a "moved" event at the end if requested. - if t.moved != nil { - selectionStart, cursor := t.selectionStart, t.cursor - defer func() { - if selectionStart != t.selectionStart || cursor != t.cursor { - t.moved() - } - }() - } - - // Turn mouse coordinates into text coordinates. - column := x - rectX - row := y - rectY - if !t.wrap { - column += t.columnOffset - } - row += t.rowOffset - - // Process mouse actions. - switch action { - case MouseLeftDown: - t.moveCursor(row, column) - if event.Modifiers()&tcell.ModShift == 0 { - t.selectionStart = t.cursor - } - setFocus(t) - consumed = true - capture = t - t.dragging = true - case MouseMove: - if !t.dragging { - break - } - t.moveCursor(row, column) - consumed = true - case MouseLeftUp: - t.moveCursor(row, column) - consumed = true - capture = nil - t.dragging = false - case MouseLeftDoubleClick: // Select word. - // Left down/up was already triggered so we are at the correct - // position. - t.moveWordLeft(false) - t.selectionStart = t.cursor - t.moveWordRight(true, false) - consumed = true - case MouseScrollUp: - if t.rowOffset > 0 { - t.rowOffset-- - } - consumed = true - case MouseScrollDown: - t.rowOffset++ - if t.rowOffset >= len(t.lineStarts) { - t.rowOffset = len(t.lineStarts) - 1 - if t.rowOffset < 0 { - t.rowOffset = 0 - } - } - consumed = true - case MouseScrollLeft: - if t.columnOffset > 0 { - t.columnOffset-- - } - consumed = true - case MouseScrollRight: - t.columnOffset++ - if t.columnOffset >= t.widestLine { - t.columnOffset = t.widestLine - 1 - if t.columnOffset < 0 { - t.columnOffset = 0 - } - } - consumed = true - } - - return - }) -} diff --git a/textview.go b/textview.go index 7d83bcb..6ea4e73 100644 --- a/textview.go +++ b/textview.go @@ -75,16 +75,12 @@ func (w TextViewWriter) HasFocus() bool { return w.t.hasFocus } -// TextView is a box which displays text. While the text to be displayed can be -// changed or appended to, there is no functionality that allows the user to -// edit text. For that, TextArea should be used. -// -// TextView implements the io.Writer interface so you can stream text to it, -// appending to the existing text. This does not trigger a redraw automatically +// TextView is a box which displays text. It implements the io.Writer interface +// so you can stream text to it. This does not trigger a redraw automatically // but if a handler is installed via SetChangedFunc(), you can cause it to be // redrawn. (See SetChangedFunc() for more details.) // -// # Navigation +// Navigation // // If the text view is scrollable (the default), text is kept in a buffer which // may be larger than the screen and can be navigated similarly to Vim: @@ -103,27 +99,27 @@ func (w TextViewWriter) HasFocus() bool { // // Use SetInputCapture() to override or modify keyboard input. // -// # Colors +// Colors // // If dynamic colors are enabled via SetDynamicColors(), text color can be // changed dynamically by embedding color strings in square brackets. This works // the same way as anywhere else. Please see the package documentation for more // information. // -// # Regions and Highlights +// Regions and Highlights // // If regions are enabled via SetRegions(), you can define text regions within // the text and assign region IDs to them. Text regions start with region tags. // Region tags are square brackets that contain a region ID in double quotes, // for example: // -// We define a ["rg"]region[""] here. +// We define a ["rg"]region[""] here. // // A text region ends with the next region tag. Tags with no region ID ([""]) // don't start new regions. They can therefore be used to mark the end of a // region. Region IDs must satisfy the following regular expression: // -// [a-zA-Z0-9_,;: \-\.]+ +// [a-zA-Z0-9_,;: \-\.]+ // // Regions can be highlighted by calling the Highlight() function with one or // more region IDs. This can be used to display search results, for example. @@ -131,7 +127,7 @@ func (w TextViewWriter) HasFocus() bool { // The ScrollToHighlight() function can be used to jump to the currently // highlighted region once when the text view is drawn the next time. // -// # Large Texts +// Large Texts // // This widget is not designed for very large texts as word wrapping, color and // region tag handling, and proper Unicode handling will result in a significant @@ -183,8 +179,7 @@ type TextView struct { // If set to true, the text view will always remain at the end of the content. trackEnd bool - // The number of characters to be skipped on each line (not used in wrap - // mode). + // The number of characters to be skipped on each line (not in wrap mode). columnOffset int // The maximum number of lines kept in the line index, effectively the @@ -756,13 +751,13 @@ func (t *TextView) write(p []byte) (n int, err error) { // BatchWriter is called, and will be released when the returned writer is // closed. Example: // -// tv := tview.NewTextView() -// w := tv.BatchWriter() -// defer w.Close() -// w.Clear() -// fmt.Fprintln(w, "To sit in solemn silence") -// fmt.Fprintln(w, "on a dull, dark, dock") -// fmt.Println(tv.GetText(false)) +// tv := tview.NewTextView() +// w := tv.BatchWriter() +// defer w.Close() +// w.Clear() +// fmt.Fprintln(w, "To sit in solemn silence") +// fmt.Fprintln(w, "on a dull, dark, dock") +// fmt.Println(tv.GetText(false)) // // Note that using the batch writer requires you to manage any issues that may // arise from concurrency yourself. See package description for details on @@ -1143,7 +1138,7 @@ func (t *TextView) Draw(screen tcell.Screen) { // Print the line. if y+line-t.lineOffset >= 0 { var colorPos, regionPos, escapePos, tagOffset, skipped int - iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool { + iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { // Process tags. for { if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] { @@ -1317,9 +1312,6 @@ func (t *TextView) MouseHandler() func(action MouseAction, event *tcell.EventMou } switch action { - case MouseLeftDown: - setFocus(t) - consumed = true case MouseLeftClick: if t.regions { // Find a region to highlight. @@ -1334,6 +1326,7 @@ func (t *TextView) MouseHandler() func(action MouseAction, event *tcell.EventMou break } } + setFocus(t) consumed = true case MouseScrollUp: t.trackEnd = false diff --git a/treeview.go b/treeview.go index 0bbb106..fb9a2ab 100644 --- a/treeview.go +++ b/treeview.go @@ -367,8 +367,8 @@ func (t *TreeView) SetTopLevel(topLevel int) *TreeView { // // For example, to display a hierarchical list with bullet points: // -// treeView.SetGraphics(false). -// SetPrefixes([]string{"* ", "- ", "x "}) +// treeView.SetGraphics(false). +// SetPrefixes([]string{"* ", "- ", "x "}) func (t *TreeView) SetPrefixes(prefixes []string) *TreeView { t.prefixes = prefixes return t @@ -792,10 +792,8 @@ func (t *TreeView) MouseHandler() func(action MouseAction, event *tcell.EventMou } switch action { - case MouseLeftDown: - setFocus(t) - consumed = true case MouseLeftClick: + setFocus(t) _, rectY, _, _ := t.GetInnerRect() y += t.offsetY - rectY if y >= 0 && y < len(t.nodes) { diff --git a/util.go b/util.go index 45d00d9..1bcbf03 100644 --- a/util.go +++ b/util.go @@ -262,7 +262,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth, foregroundColor, backgroundColor, attributes string ) originalStyle := style - iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool { + iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { // Update color/escape tag offset and style. if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] { foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) @@ -305,7 +305,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth, for rightIndex-1 > leftIndex && strippedWidth-skipWidth-choppedLeft-choppedRight > maxWidth { if skipWidth > 0 || choppedLeft < choppedRight { // Iterate on the left by one character. - iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool { + iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { if skipWidth > 0 { skipWidth -= screenWidth strippedWidth -= screenWidth @@ -369,7 +369,7 @@ func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth, drawn, drawnWidth, colorPos, escapePos, tagOffset, from, to int foregroundColor, backgroundColor, attributes string ) - iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth, boundaries int) bool { + iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool { // Skip character if necessary. if skipWidth > 0 { skipWidth -= screenWidth @@ -496,7 +496,7 @@ func WordWrap(text string, width int) (lines []string) { } return substr } - iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool { + iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { // Handle tags. for { if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] { @@ -582,18 +582,16 @@ func Escape(text string) string { // Unicode code points of the character (the first rune and any combining runes // which may be nil if there aren't any), the starting position (in bytes) // within the original string, its length in bytes, the screen position of the -// character, the screen width of it, and a boundaries value which includes -// word/sentence boundary or line break information (see the -// github.com/rivo/uniseg package, Step() function, for more information). The -// iteration stops if the callback returns true. This function returns true if -// the iteration was stopped before the last character. -func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool) bool { - var screenPos, textPos, boundaries int +// character, and the screen width of it. The iteration stops if the callback +// returns true. This function returns true if the iteration was stopped before +// the last character. +func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool { + var screenPos, textPos int state := -1 for len(text) > 0 { var cluster string - cluster, text, boundaries, state = uniseg.StepString(text, state) + cluster, text, _, state = uniseg.FirstGraphemeClusterInString(text, state) var width int runes := make([]rune, 0, len(cluster)) @@ -610,7 +608,7 @@ func iterateString(text string, callback func(main rune, comb []rune, textPos, t comb = runes[1:] } - if callback(runes[0], comb, textPos, len(cluster), screenPos, width, boundaries) { + if callback(runes[0], comb, textPos, len(cluster), screenPos, width) { return true } @@ -638,7 +636,7 @@ func iterateStringReverse(text string, callback func(main rune, comb []rune, tex // Create the grapheme clusters. var clusters []cluster - iterateString(text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth, boundaries int) bool { + iterateString(text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool { clusters = append(clusters, cluster{ main: main, comb: comb,