1
0
mirror of https://github.com/rivo/tview.git synced 2025-05-01 22:18:30 +08:00

Text views can also become part of forms.

This commit is contained in:
Oliver 2022-12-11 00:46:02 +01:00
parent db36428c92
commit 3f246bda86
3 changed files with 176 additions and 12 deletions

View File

@ -12,6 +12,7 @@ func main() {
AddInputField("First name", "", 20, nil, nil). AddInputField("First name", "", 20, nil, nil).
AddInputField("Last name", "", 20, nil, nil). AddInputField("Last name", "", 20, nil, nil).
AddTextArea("Address", "", 40, 0, 0, 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). AddCheckbox("Age 18+", false, nil).
AddPasswordField("Password", "", 10, '*', nil). AddPasswordField("Password", "", 10, '*', nil).
AddButton("Save", nil). AddButton("Save", nil).

42
form.go
View File

@ -38,8 +38,10 @@ type FormItem interface {
// SetFinishedFunc sets the handler function for when the user finished // SetFinishedFunc sets the handler function for when the user finished
// entering data into the item. The handler may receive events for the // 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 // Enter key (we're done), the Escape key (cancel input), the Tab key (move
// next field), and the Backtab key (move to previous field). // 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 SetFinishedFunc(handler func(key tcell.Key)) FormItem
} }
@ -88,6 +90,10 @@ type Form struct {
// The color of the button text. // The color of the button text.
buttonTextColor tcell.Color 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. // An optional function which is called when the user hits Escape.
cancel func() cancel func()
} }
@ -104,6 +110,7 @@ func NewForm() *Form {
fieldTextColor: Styles.PrimaryTextColor, fieldTextColor: Styles.PrimaryTextColor,
buttonBackgroundColor: Styles.ContrastBackgroundColor, buttonBackgroundColor: Styles.ContrastBackgroundColor,
buttonTextColor: Styles.PrimaryTextColor, buttonTextColor: Styles.PrimaryTextColor,
lastFinishedKey: -1,
} }
return f return f
@ -207,6 +214,26 @@ func (f *Form) AddTextArea(label, text string, fieldWidth, fieldHeight, maxLengt
return f 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 // 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), // 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 // 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) { if f.focusedElement < 0 || f.focusedElement >= len(f.items)+len(f.buttons) {
f.focusedElement = 0 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 { switch key {
case tcell.KeyTab, tcell.KeyEnter: case tcell.KeyTab, tcell.KeyEnter:
f.focusedElement++ f.focusedElement++
@ -632,6 +663,11 @@ func (f *Form) Focus(delegate func(p Primitive)) {
f.focusedElement = 0 f.focusedElement = 0
f.Focus(delegate) f.Focus(delegate)
} }
default:
if key < 0 && f.lastFinishedKey >= 0 {
// Repeat the last action.
handler(f.lastFinishedKey)
}
} }
} }

View File

@ -142,6 +142,10 @@ type TextView struct {
sync.Mutex sync.Mutex
*Box *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. // The text buffer.
buffer []string buffer []string
@ -152,6 +156,15 @@ type TextView struct {
// to be re-indexed. // to be re-indexed.
index []*textViewIndex 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. // The text alignment, one of AlignLeft, AlignCenter, or AlignRight.
align int align int
@ -206,8 +219,9 @@ type TextView struct {
// after punctuation characters. // after punctuation characters.
wordWrap bool wordWrap bool
// The (starting) color of the text. // The (starting) style of the text. This also defines the background color
textColor tcell.Color // of the main text element.
textStyle tcell.Style
// If set to true, the text color can be changed dynamically by piping color // If set to true, the text color can be changed dynamically by piping color
// strings in square brackets to the text view. // 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 // An optional function which is called when one or more regions were
// highlighted. // highlighted.
highlighted func(added, removed, remaining []string) 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. // NewTextView returns a new text view.
func NewTextView() *TextView { func NewTextView() *TextView {
return &TextView{ return &TextView{
Box: NewBox(), Box: NewBox(),
labelStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor),
highlights: make(map[string]struct{}), highlights: make(map[string]struct{}),
lineOffset: -1, lineOffset: -1,
scrollable: true, scrollable: true,
align: AlignLeft, align: AlignLeft,
wrap: true, wrap: true,
textColor: Styles.PrimaryTextColor, textStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.PrimaryTextColor),
regions: false, regions: false,
dynamicColors: 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 // 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, // scrollable. If true, text is kept in a buffer and can be navigated. If false,
// the last line will always be visible. // 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 // dynamically by sending color strings in square brackets to the text view if
// dynamic colors are enabled). // dynamic colors are enabled).
func (t *TextView) SetTextColor(color tcell.Color) *TextView { 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 return t
} }
@ -427,6 +493,22 @@ func (t *TextView) SetHighlightedFunc(handler func(added, removed, remaining []s
return t 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). // ScrollTo scrolls to the specified row and column (both starting with 0).
func (t *TextView) ScrollTo(row, column int) *TextView { func (t *TextView) ScrollTo(row, column int) *TextView {
if !t.scrollable { if !t.scrollable {
@ -668,6 +750,13 @@ func (t *TextView) Focus(delegate func(p Primitive)) {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
t.Box.Focus(delegate) 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. // 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.Box.DrawForSubclass(screen, t)
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
totalWidth, totalHeight := screen.Size()
// Get the available size. // Get the available size.
x, y, width, height := t.GetInnerRect() x, y, width, height := t.GetInnerRect()
t.pageSize = height 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 the width has changed, we need to reindex.
if width != t.lastWidth && t.wrap { if width != t.lastWidth && t.wrap {
t.index = nil t.index = nil
@ -1101,10 +1226,9 @@ func (t *TextView) Draw(screen tcell.Screen) {
} }
// Draw the buffer. // Draw the buffer.
defaultStyle := tcell.StyleDefault.Foreground(t.textColor).Background(t.backgroundColor)
for line := t.lineOffset; line < len(t.index); line++ { for line := t.lineOffset; line < len(t.index); line++ {
// Are we done? // Are we done?
if line-t.lineOffset >= height || y+line-t.lineOffset >= totalHeight { if line-t.lineOffset >= height {
break break
} }
@ -1193,7 +1317,7 @@ func (t *TextView) Draw(screen tcell.Screen) {
} }
// Mix the existing style with the new style. // 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? // Do we highlight this character?
var highlighted bool var highlighted bool
@ -1224,7 +1348,7 @@ func (t *TextView) Draw(screen tcell.Screen) {
} }
// Stop at the right border. // Stop at the right border.
if posX+screenWidth > width || x+posX >= totalWidth { if posX+screenWidth > width {
return true return true
} }
@ -1266,6 +1390,9 @@ func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
if t.done != nil { if t.done != nil {
t.done(key) t.done(key)
} }
if t.finished != nil {
t.finished(key)
}
return return
} }