Text area can now be added to forms. See #594

pull/294/merge
Oliver 2 years ago
parent 0b2ae10823
commit ed3ea789e9

@ -336,10 +336,8 @@ func (b *Box) DrawForSubclass(screen tcell.Screen, p Primitive) {
return
}
def := tcell.StyleDefault
// Fill background.
background := def.Background(b.backgroundColor)
background := tcell.StyleDefault.Background(b.backgroundColor)
if !b.dontClear {
for y := b.y; y < b.y+b.height; y++ {
for x := b.x; x < b.x+b.width; x++ {

@ -130,6 +130,11 @@ func (c *Checkbox) GetFieldWidth() int {
return 1
}
// GetFieldHeight returns this primitive's field height.
func (c *Checkbox) GetFieldHeight() int {
return 1
}
// SetChangedFunc sets a handler which is called when the checked state of this
// checkbox was changed by the user. The handler function receives the new
// state.
@ -170,13 +175,13 @@ func (c *Checkbox) Draw(screen tcell.Screen) {
// Draw label.
if c.labelWidth > 0 {
labelWidth := c.labelWidth
if labelWidth > rightLimit-x {
labelWidth = rightLimit - x
if labelWidth > width {
labelWidth = width
}
Print(screen, c.label, x, y, labelWidth, AlignLeft, c.labelColor)
x += labelWidth
} else {
_, drawnWidth := Print(screen, c.label, x, y, rightLimit-x, AlignLeft, c.labelColor)
_, drawnWidth := Print(screen, c.label, x, y, width, AlignLeft, c.labelColor)
x += drawnWidth
}

@ -11,6 +11,7 @@ func main() {
AddDropDown("Title", []string{"Mr.", "Ms.", "Mrs.", "Dr.", "Prof."}, 0, nil).
AddInputField("First name", "", 20, nil, nil).
AddInputField("Last name", "", 20, nil, nil).
AddTextArea("Address", "", 40, 0, 0, nil).
AddCheckbox("Age 18+", false, nil).
AddPasswordField("Password", "", 10, '*', nil).
AddButton("Save", nil).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 52 KiB

@ -77,6 +77,7 @@ Type to enter text.
Move while holding Shift or drag the mouse.
Double-click to select a word.
[yellow]Ctrl-L[white] to select entire text.
[green]Clipboard

@ -244,6 +244,11 @@ func (d *DropDown) GetFieldWidth() int {
return fieldWidth
}
// GetFieldHeight returns this primitive's field height.
func (d *DropDown) GetFieldHeight() int {
return 1
}
// AddOption adds a new selectable option to this drop-down. The "selected"
// callback is called when this option was selected. It may be nil.
func (d *DropDown) AddOption(text string, selected func()) *DropDown {

@ -4,10 +4,16 @@ import (
"github.com/gdamore/tcell/v2"
)
// DefaultFormFieldWidth is the default field screen width of form elements
// whose field width is flexible (0). This is used in the Form class for
// horizontal layouts.
var DefaultFormFieldWidth = 10
var (
// DefaultFormFieldWidth is the default field screen width of form elements
// whose field width is flexible (0). This is used in the Form class for
// horizontal layouts.
DefaultFormFieldWidth = 10
// DefaultFormFieldHeight is the default field height of multi-line form
// elements whose field height is flexible (0).
DefaultFormFieldHeight = 5
)
// FormItem is the interface all form items must implement to be able to be
// included in a form.
@ -26,6 +32,10 @@ type FormItem interface {
// required.
GetFieldWidth() int
// GetFieldHeight returns the height of the form item's field (the area which
// is manipulated by the user). This value must be greater than 0.
GetFieldHeight() int
// 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
@ -55,7 +65,7 @@ type Form struct {
// The alignment of the buttons.
buttonsAlign int
// The number of empty rows between items.
// The number of empty cells between items.
itemPadding int
// The index of the item or button which has focus. (Items are counted first,
@ -167,6 +177,36 @@ func (f *Form) SetFocus(index int) *Form {
return f
}
// AddTextArea adds a text area to the form. It has a label, an optional initial
// text, a size (width and height) referring to the actual input area (a
// fieldWidth of 0 extends it as far right as possible, a fieldHeight of 0 will
// cause it to be [DefaultFormFieldHeight]), and a maximum number of bytes of
// text allowed (0 means no limit).
//
// The optional callback function is invoked when the content of the text area
// has changed. Note that especially for larger texts, this is an expensive
// operation due to technical constraints of the [TextArea] primitive (every key
// stroke leads to a new reallocation of the entire text).
func (f *Form) AddTextArea(label, text string, fieldWidth, fieldHeight, maxLength int, changed func(text string)) *Form {
if fieldHeight == 0 {
fieldHeight = DefaultFormFieldHeight
}
textArea := NewTextArea().
SetLabel(label).
SetSize(fieldHeight, fieldWidth).
SetMaxLength(maxLength)
if text != "" {
textArea.SetText(text, true)
}
if changed != nil {
textArea.SetChangedFunc(func() {
changed(textArea.GetText())
})
}
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
@ -386,15 +426,19 @@ func (f *Form) Draw(screen tcell.Screen) {
maxLabelWidth++ // Add one space.
// Calculate positions of form items.
positions := make([]struct{ x, y, width, height int }, len(f.items)+len(f.buttons))
var focusedPosition struct{ x, y, width, height int }
type position struct{ x, y, width, height int }
positions := make([]position, len(f.items)+len(f.buttons))
var (
focusedPosition position
lineHeight = 1
)
for index, item := range f.items {
// Calculate the space needed.
labelWidth := TaggedStringWidth(item.GetLabel())
var itemWidth int
if f.horizontal {
fieldWidth := item.GetFieldWidth()
if fieldWidth == 0 {
if fieldWidth <= 0 {
fieldWidth = DefaultFormFieldWidth
}
labelWidth++
@ -404,11 +448,21 @@ func (f *Form) Draw(screen tcell.Screen) {
labelWidth = maxLabelWidth
itemWidth = width
}
itemHeight := item.GetFieldHeight()
if itemHeight <= 0 {
itemHeight = DefaultFormFieldHeight
}
// Advance to next line if there is no space.
if f.horizontal && x+labelWidth+1 >= rightLimit {
x = startX
y += 2
y += lineHeight + 1
lineHeight = itemHeight
}
// Update line height.
if itemHeight > lineHeight {
lineHeight = itemHeight
}
// Adjust the item's attributes.
@ -427,7 +481,7 @@ func (f *Form) Draw(screen tcell.Screen) {
positions[index].x = x
positions[index].y = y
positions[index].width = itemWidth
positions[index].height = 1
positions[index].height = itemHeight
if item.HasFocus() {
focusedPosition = positions[index]
}
@ -436,7 +490,7 @@ func (f *Form) Draw(screen tcell.Screen) {
if f.horizontal {
x += itemWidth + f.itemPadding
} else {
y += 1 + f.itemPadding
y += itemHeight + f.itemPadding
}
}
@ -471,8 +525,9 @@ func (f *Form) Draw(screen tcell.Screen) {
if f.horizontal {
if space < buttonWidth-4 {
x = startX
y += 2
y += lineHeight + 1
space = width
lineHeight = 1
}
} else {
if space < 1 {

@ -249,6 +249,11 @@ func (i *InputField) GetFieldWidth() int {
return i.fieldWidth
}
// GetFieldHeight returns this primitive's field height.
func (i *InputField) GetFieldHeight() int {
return 1
}
// SetMaskCharacter sets a character that masks user input on a screen. A value
// of 0 disables masking.
func (i *InputField) SetMaskCharacter(mask rune) *InputField {
@ -372,13 +377,13 @@ func (i *InputField) Draw(screen tcell.Screen) {
_, labelBg, _ := i.labelStyle.Decompose()
if i.labelWidth > 0 {
labelWidth := i.labelWidth
if labelWidth > rightLimit-x {
labelWidth = rightLimit - x
if labelWidth > width {
labelWidth = width
}
printWithStyle(screen, i.label, x, y, 0, labelWidth, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
x += labelWidth
} else {
_, drawnWidth, _, _ := printWithStyle(screen, i.label, x, y, 0, rightLimit-x, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
_, drawnWidth, _, _ := printWithStyle(screen, i.label, x, y, 0, width, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
x += drawnWidth
}

@ -192,11 +192,24 @@ type textAreaUndoItem struct {
type TextArea struct {
*Box
// The size of the text area. If set to 0, the text area will use the entire
// available space.
width, height int
// The text to be shown in the text area when it is empty.
placeholder string
// The label text shown, usually when part of a form.
label string
// The width of the text area's label.
labelWidth int
// Styles:
// The label style.
labelStyle tcell.Style
// The style of the text. Background colors different from the Box's
// background color may lead to unwanted artefacts.
textStyle tcell.Style
@ -314,6 +327,10 @@ type TextArea struct {
// An optional function which is called when the position of the cursor or
// the selection has changed.
moved func()
// A callback function set by the Form class and called when the user leaves
// this form item.
finished func(tcell.Key)
}
// NewTextArea returns a new text area. Use [TextArea.SetText] to set the
@ -324,6 +341,7 @@ func NewTextArea() *TextArea {
wrap: true,
wordWrap: true,
placeholderStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.TertiaryTextColor),
labelStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor),
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.
@ -693,6 +711,44 @@ func (t *TextArea) SetPlaceholder(placeholder string) *TextArea {
return t
}
// SetLabel sets the text to be displayed before the text area.
func (t *TextArea) SetLabel(label string) *TextArea {
t.label = label
return t
}
// GetLabel returns the text to be displayed before the text area.
func (t *TextArea) 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 *TextArea) SetLabelWidth(width int) *TextArea {
t.labelWidth = width
return t
}
// SetSize sets the screen size of the input element of the text area. The input
// 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 *TextArea) SetSize(rows, columns int) *TextArea {
t.width = columns
t.height = rows
return t
}
// GetFieldWidth returns this primitive's field width.
func (t *TextArea) GetFieldWidth() int {
return t.width
}
// GetFieldHeight returns this primitive's field height.
func (t *TextArea) GetFieldHeight() int {
return t.height
}
// SetMaxLength sets the maximum number of bytes allowed in the text area. A
// value of 0 means there is no limit. If the text area currently contains more
// bytes than this, it may violate this constraint.
@ -701,6 +757,17 @@ func (t *TextArea) SetMaxLength(maxLength int) *TextArea {
return t
}
// SetLabelStyle sets the style of the label.
func (t *TextArea) SetLabelStyle(style tcell.Style) *TextArea {
t.labelStyle = style
return t
}
// GetLabelStyle returns the style of the label.
func (t *TextArea) GetLabelStyle() tcell.Style {
return t.labelStyle
}
// 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 {
@ -775,6 +842,21 @@ func (t *TextArea) SetMovedFunc(handler func()) *TextArea {
return t
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (t *TextArea) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
t.finished = handler
return t
}
// SetFormAttributes sets attributes shared by all form items.
func (t *TextArea) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
t.labelWidth = labelWidth
t.backgroundColor = bgColor
t.labelStyle = t.labelStyle.Foreground(labelColor)
t.textStyle = tcell.StyleDefault.Foreground(fieldTextColor).Background(fieldBgColor)
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
@ -947,7 +1029,7 @@ func (t *TextArea) Draw(screen tcell.Screen) {
// Prepare
x, y, width, height := t.GetInnerRect()
if width == 0 || height == 0 {
if width <= 0 || height <= 0 {
return // We have no space for anything.
}
columnOffset := t.columnOffset
@ -955,6 +1037,43 @@ func (t *TextArea) Draw(screen tcell.Screen) {
columnOffset = 0
}
// 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 input 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 input 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)
}
}
}
// Show/hide the cursor at the end.
defer func() {
if t.HasFocus() {
@ -1856,6 +1975,12 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
t.selectionStart = t.cursor
newLastAction = taActionTypeSpace
case tcell.KeyTab: // Insert a tab character. It will be rendered as TabSize spaces.
// But forwarding takes precedence.
if t.finished != nil {
t.finished(key)
return
}
from, to, row := t.getSelection()
t.cursor.pos = t.replace(from, to, "\t", t.lastAction == taActionTypeSpace)
t.cursor.row = -1
@ -1863,6 +1988,11 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
t.findCursor(true, row)
t.selectionStart = t.cursor
newLastAction = taActionTypeSpace
case tcell.KeyBacktab, tcell.KeyEscape: // Only used in forms.
if t.finished != nil {
t.finished(key)
return
}
case tcell.KeyRune:
if event.Modifiers()&tcell.ModAlt > 0 {
// We accept some Alt- key combinations.
@ -2084,7 +2214,11 @@ func (t *TextArea) MouseHandler() func(action MouseAction, event *tcell.EventMou
}
// Turn mouse coordinates into text coordinates.
column := x - rectX
labelWidth := t.labelWidth
if labelWidth == 0 && t.label != "" {
labelWidth = TaggedStringWidth(t.label)
}
column := x - rectX - labelWidth
row := y - rectY
if !t.wrap {
column += t.columnOffset

Loading…
Cancel
Save