diff --git a/demos/form/main.go b/demos/form/main.go index b76ac4b..317eb59 100644 --- a/demos/form/main.go +++ b/demos/form/main.go @@ -12,6 +12,7 @@ func main() { AddInputField("First name", "", 20, nil, nil). AddInputField("Last name", "", 20, nil, nil). AddTextArea("Address", "", 40, 0, 0, nil). + AddTextView("Notes", "This is just a demo.\nYou can enter whatever you wish.", 40, 2, true, false). AddCheckbox("Age 18+", false, nil). AddPasswordField("Password", "", 10, '*', nil). AddButton("Save", nil). diff --git a/form.go b/form.go index 46d814f..82d23a2 100644 --- a/form.go +++ b/form.go @@ -38,8 +38,10 @@ type FormItem interface { // SetFinishedFunc sets the handler function for when the user finished // entering data into the item. The handler may receive events for the - // Enter key (we're done), the Escape key (cancel input), the Tab key (move to - // next field), and the Backtab key (move to previous field). + // Enter key (we're done), the Escape key (cancel input), the Tab key (move + // to next field), the Backtab key (move to previous field), or a negative + // value, indicating that the action for the last known key should be + // repeated. SetFinishedFunc(handler func(key tcell.Key)) FormItem } @@ -88,6 +90,10 @@ type Form struct { // The color of the button text. buttonTextColor tcell.Color + // The last (valid) key that wsa sent to a "finished" handler or -1 if no + // such key is known yet. + lastFinishedKey tcell.Key + // An optional function which is called when the user hits Escape. cancel func() } @@ -104,6 +110,7 @@ func NewForm() *Form { fieldTextColor: Styles.PrimaryTextColor, buttonBackgroundColor: Styles.ContrastBackgroundColor, buttonTextColor: Styles.PrimaryTextColor, + lastFinishedKey: -1, } return f @@ -207,6 +214,26 @@ func (f *Form) AddTextArea(label, text string, fieldWidth, fieldHeight, maxLengt return f } +// AddTextView adds a text view to the form. It has a label and text, a size +// (width and height) referring to the actual text element (a fieldWidth of 0 +// extends it as far right as possible, a fieldHeight of 0 will cause it to be +// [DefaultFormFieldHeight]), a flag to turn on/off dynamic colors, and a flag +// to turn on/off scrolling. If scrolling is turned off, the text view will not +// receive focus. +func (f *Form) AddTextView(label, text string, fieldWidth, fieldHeight int, dynamicColors, scrollable bool) *Form { + if fieldHeight == 0 { + fieldHeight = DefaultFormFieldHeight + } + textArea := NewTextView(). + SetLabel(label). + SetSize(fieldHeight, fieldWidth). + SetDynamicColors(dynamicColors). + SetScrollable(scrollable). + SetText(text) + f.items = append(f.items, textArea) + return f +} + // AddInputField adds an input field to the form. It has a label, an optional // initial value, a field width (a value of 0 extends it as far as possible), // an optional accept function to validate the item's value (set to nil to @@ -614,7 +641,11 @@ func (f *Form) Focus(delegate func(p Primitive)) { if f.focusedElement < 0 || f.focusedElement >= len(f.items)+len(f.buttons) { f.focusedElement = 0 } - handler := func(key tcell.Key) { + var handler func(key tcell.Key) + handler = func(key tcell.Key) { + if key >= 0 { + f.lastFinishedKey = key + } switch key { case tcell.KeyTab, tcell.KeyEnter: f.focusedElement++ @@ -632,6 +663,11 @@ func (f *Form) Focus(delegate func(p Primitive)) { f.focusedElement = 0 f.Focus(delegate) } + default: + if key < 0 && f.lastFinishedKey >= 0 { + // Repeat the last action. + handler(f.lastFinishedKey) + } } } diff --git a/textview.go b/textview.go index 5b48843..9f00e90 100644 --- a/textview.go +++ b/textview.go @@ -142,6 +142,10 @@ type TextView struct { sync.Mutex *Box + // The size of the text area. If set to 0, the text view will use the entire + // available space. + width, height int + // The text buffer. buffer []string @@ -152,6 +156,15 @@ type TextView struct { // to be re-indexed. index []*textViewIndex + // The label text shown, usually when part of a form. + label string + + // The width of the text area's label. + labelWidth int + + // The label style. + labelStyle tcell.Style + // The text alignment, one of AlignLeft, AlignCenter, or AlignRight. align int @@ -206,8 +219,9 @@ type TextView struct { // after punctuation characters. wordWrap bool - // The (starting) color of the text. - textColor tcell.Color + // The (starting) style of the text. This also defines the background color + // of the main text element. + textStyle tcell.Style // If set to true, the text color can be changed dynamically by piping color // strings in square brackets to the text view. @@ -235,23 +249,66 @@ type TextView struct { // An optional function which is called when one or more regions were // highlighted. highlighted func(added, removed, remaining []string) + + // A callback function set by the Form class and called when the user leaves + // this form item. + finished func(tcell.Key) } // NewTextView returns a new text view. func NewTextView() *TextView { return &TextView{ Box: NewBox(), + labelStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor), highlights: make(map[string]struct{}), lineOffset: -1, scrollable: true, align: AlignLeft, wrap: true, - textColor: Styles.PrimaryTextColor, + textStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.PrimaryTextColor), regions: false, dynamicColors: false, } } +// SetLabel sets the text to be displayed before the text view. +func (t *TextView) SetLabel(label string) *TextView { + t.label = label + return t +} + +// GetLabel returns the text to be displayed before the text view. +func (t *TextView) GetLabel() string { + return t.label +} + +// SetLabelWidth sets the screen width of the label. A value of 0 will cause the +// primitive to use the width of the label string. +func (t *TextView) SetLabelWidth(width int) *TextView { + t.labelWidth = width + return t +} + +// SetSize sets the screen size of the main text element of the text view. This +// element is always located next to the label which is always located in the +// top left corner. If any of the values are 0 or larger than the available +// space, the available space will be used. +func (t *TextView) SetSize(rows, columns int) *TextView { + t.width = columns + t.height = rows + return t +} + +// GetFieldWidth returns this primitive's field width. +func (t *TextView) GetFieldWidth() int { + return t.width +} + +// GetFieldHeight returns this primitive's field height. +func (t *TextView) GetFieldHeight() int { + return t.height +} + // SetScrollable sets the flag that decides whether or not the text view is // scrollable. If true, text is kept in a buffer and can be navigated. If false, // the last line will always be visible. @@ -315,7 +372,16 @@ func (t *TextView) SetTextAlign(align int) *TextView { // dynamically by sending color strings in square brackets to the text view if // dynamic colors are enabled). func (t *TextView) SetTextColor(color tcell.Color) *TextView { - t.textColor = color + t.textStyle = t.textStyle.Foreground(color) + return t +} + +// SetTextStyle sets the initial style of the text (which can be changed +// dynamically by sending color strings in square brackets to the text view if +// dynamic colors are enabled). This style's background color also determines +// the background color of the main text element (even if empty). +func (t *TextView) SetTextStyle(style tcell.Style) *TextView { + t.textStyle = style return t } @@ -427,6 +493,22 @@ func (t *TextView) SetHighlightedFunc(handler func(added, removed, remaining []s return t } +// SetFinishedFunc sets a callback invoked when the user leaves this form item. +func (t *TextView) SetFinishedFunc(handler func(key tcell.Key)) FormItem { + t.finished = handler + return t +} + +// SetFormAttributes sets attributes shared by all form items. +func (t *TextView) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { + t.labelWidth = labelWidth + t.backgroundColor = bgColor + t.labelStyle = t.labelStyle.Foreground(labelColor) + // We ignore the field background color because this is a read-only element. + t.textStyle = tcell.StyleDefault.Foreground(fieldTextColor).Background(bgColor) + return t +} + // ScrollTo scrolls to the specified row and column (both starting with 0). func (t *TextView) ScrollTo(row, column int) *TextView { if !t.scrollable { @@ -668,6 +750,13 @@ func (t *TextView) Focus(delegate func(p Primitive)) { t.Lock() defer t.Unlock() t.Box.Focus(delegate) + + // But if we're part of a form and not scrollable, there's nothing the user + // can do here so we're finished. + if t.finished != nil && !t.scrollable { + t.finished(-1) + return + } } // HasFocus returns whether or not this primitive has focus. @@ -1014,12 +1103,48 @@ func (t *TextView) Draw(screen tcell.Screen) { t.Box.DrawForSubclass(screen, t) t.Lock() defer t.Unlock() - totalWidth, totalHeight := screen.Size() // Get the available size. x, y, width, height := t.GetInnerRect() t.pageSize = height + // Draw label. + _, labelBg, _ := t.labelStyle.Decompose() + if t.labelWidth > 0 { + labelWidth := t.labelWidth + if labelWidth > width { + labelWidth = width + } + printWithStyle(screen, t.label, x, y, 0, labelWidth, AlignLeft, t.labelStyle, labelBg == tcell.ColorDefault) + x += labelWidth + width -= labelWidth + } else { + _, drawnWidth, _, _ := printWithStyle(screen, t.label, x, y, 0, width, AlignLeft, t.labelStyle, labelBg == tcell.ColorDefault) + x += drawnWidth + width -= drawnWidth + } + + // What's the space for the text element? + if t.width > 0 && t.width < width { + width = t.width + } + if t.height > 0 && t.height < height { + height = t.height + } + if width <= 0 { + return // No space left for the text area. + } + + // Draw the text element if necessary. + _, bg, _ := t.textStyle.Decompose() + if bg != t.GetBackgroundColor() { + for row := 0; row < height; row++ { + for column := 0; column < width; column++ { + screen.SetContent(x+column, y+row, ' ', nil, t.textStyle) + } + } + } + // If the width has changed, we need to reindex. if width != t.lastWidth && t.wrap { t.index = nil @@ -1101,10 +1226,9 @@ func (t *TextView) Draw(screen tcell.Screen) { } // Draw the buffer. - defaultStyle := tcell.StyleDefault.Foreground(t.textColor).Background(t.backgroundColor) for line := t.lineOffset; line < len(t.index); line++ { // Are we done? - if line-t.lineOffset >= height || y+line-t.lineOffset >= totalHeight { + if line-t.lineOffset >= height { break } @@ -1193,7 +1317,7 @@ func (t *TextView) Draw(screen tcell.Screen) { } // Mix the existing style with the new style. - style := overlayStyle(defaultStyle, foregroundColor, backgroundColor, attributes) + style := overlayStyle(t.textStyle, foregroundColor, backgroundColor, attributes) // Do we highlight this character? var highlighted bool @@ -1224,7 +1348,7 @@ func (t *TextView) Draw(screen tcell.Screen) { } // Stop at the right border. - if posX+screenWidth > width || x+posX >= totalWidth { + if posX+screenWidth > width { return true } @@ -1266,6 +1390,9 @@ func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr if t.done != nil { t.done(key) } + if t.finished != nil { + t.finished(key) + } return }