diff --git a/checkbox.go b/checkbox.go index 0feb6da..c191b4d 100644 --- a/checkbox.go +++ b/checkbox.go @@ -96,6 +96,11 @@ func (c *Checkbox) SetFormAttributes(label string, labelColor, bgColor, fieldTex return c } +// GetFieldLength returns this primitive's field length. +func (c *Checkbox) GetFieldLength() 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. diff --git a/dropdown.go b/dropdown.go index c9e9cea..ebdd227 100644 --- a/dropdown.go +++ b/dropdown.go @@ -138,6 +138,21 @@ func (d *DropDown) SetFieldLength(length int) *DropDown { return d } +// GetFieldLength returns this primitive's field length. +func (d *DropDown) GetFieldLength() int { + if d.fieldLength > 0 { + return d.fieldLength + } + fieldLength := 0 + for _, option := range d.options { + length := runewidth.StringWidth(option.Text) + if length > fieldLength { + fieldLength = length + } + } + return fieldLength +} + // 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 { diff --git a/form.go b/form.go index 2b88fe3..08fde3b 100644 --- a/form.go +++ b/form.go @@ -7,6 +7,11 @@ import ( runewidth "github.com/mattn/go-runewidth" ) +// DefaultFormFieldLength is the default field length of form elements whose +// field length is flexible (0). This is used in the Form class for horizontal +// layouts. +var DefaultFormFieldLength = 10 + // FormItem is the interface all form items must implement to be able to be // included in a form. type FormItem interface { @@ -18,6 +23,11 @@ type FormItem interface { // SetFormAttributes sets a number of item attributes at once. SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem + // GetFieldLength returns the length of the form item's field (the area which + // is manipulated by the user). A value of 0 indicates the the field length + // is flexible and may use as much space as required. + GetFieldLength() int + // SetEnteredFunc 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 @@ -37,6 +47,10 @@ type Form struct { // The buttons of the form. buttons []*Button + // If set to true, instead of position items and buttons from top to bottom, + // they are positioned from left to right. + horizontal bool + // The alignment of the buttons. buttonsAlign int @@ -85,12 +99,23 @@ func NewForm() *Form { return f } -// SetItemPadding sets the number of empty rows between form items. +// SetItemPadding sets the number of empty rows between form items for vertical +// layouts and the number of empty cells between form items for horizontal +// layouts. func (f *Form) SetItemPadding(padding int) *Form { f.itemPadding = padding return f } +// SetHorizontal sets the direction the form elements are laid out. If set to +// true, instead of positioning them from top to bottom (the default), they are +// positioned from left to right, moving into the next row if there is not +// enough space. +func (f *Form) SetHorizontal(horizontal bool) *Form { + f.horizontal = horizontal + return f +} + // SetLabelColor sets the color of the labels. func (f *Form) SetLabelColor(color tcell.Color) *Form { f.labelColor = color @@ -110,7 +135,7 @@ func (f *Form) SetFieldTextColor(color tcell.Color) *Form { } // SetButtonsAlign sets how the buttons align horizontally, one of AlignLeft -// (the default), AlignCenter, and AlignRight. +// (the default), AlignCenter, and AlignRight. This is only func (f *Form) SetButtonsAlign(align int) *Form { f.buttonsAlign = align return f @@ -201,6 +226,14 @@ func (f *Form) Clear(includeButtons bool) *Form { return f } +// AddFormItem adds a new item to the form. This can be used to add your own +// objects to the form. Note, however, that the Form class will override some +// of its attributes to make it work in the form context. +func (f *Form) AddFormItem(item FormItem) *Form { + f.items = append(f.items, item) + return f +} + // GetFormItem returns the form element at the given position, starting with // index 0. Elements are referenced in the order they were added. Buttons are // not included. @@ -223,6 +256,7 @@ func (f *Form) Draw(screen tcell.Screen) { x, y, width, height := f.GetInnerRect() bottomLimit := y + height rightLimit := x + width + startX := x // Find the longest label. var labelLength int @@ -237,23 +271,60 @@ func (f *Form) Draw(screen tcell.Screen) { // Set up and draw the input fields. for _, item := range f.items { + // Stop if there is no more space. if y >= bottomLimit { - return // Stop here. + return } + + // Calculate the space needed. label := strings.TrimSpace(item.GetLabel()) + labelWidth := runewidth.StringWidth(label) + var itemWidth int + if f.horizontal { + fieldLength := item.GetFieldLength() + if fieldLength == 0 { + fieldLength = DefaultFormFieldLength + } + label += " " + labelWidth++ + itemWidth = labelWidth + fieldLength + } else { + // We want all fields to align vertically. + label += strings.Repeat(" ", labelLength-labelWidth) + itemWidth = width + } + + // Advance to next line if there is no space. + if f.horizontal && x+labelWidth+1 >= rightLimit { + x = startX + y += 2 + } + + // Adjust the item's attributes. + if x+itemWidth >= rightLimit { + itemWidth = rightLimit - x + } item.SetFormAttributes( - label+strings.Repeat(" ", labelLength-runewidth.StringWidth(label)), + label, f.labelColor, f.backgroundColor, f.fieldTextColor, f.fieldBackgroundColor, - ).SetRect(x, y, width, 1) + ).SetRect(x, y, itemWidth, 1) + + // Draw items with focus last (in case of overlaps). if item.GetFocusable().HasFocus() { defer item.Draw(screen) } else { item.Draw(screen) } - y += 1 + f.itemPadding + + // Advance to next item. + if f.horizontal { + x += itemWidth + f.itemPadding + } else { + y += 1 + f.itemPadding + } } // How wide are the buttons? @@ -262,32 +333,43 @@ func (f *Form) Draw(screen tcell.Screen) { for index, button := range f.buttons { width := runewidth.StringWidth(button.GetLabel()) + 4 buttonWidths[index] = width - buttonsWidth += width + 2 + buttonsWidth += width + 1 } - buttonsWidth -= 2 + buttonsWidth-- // Where do we place them? - if x+buttonsWidth < rightLimit { + if !f.horizontal && x+buttonsWidth < rightLimit { if f.buttonsAlign == AlignRight { x = rightLimit - buttonsWidth } else if f.buttonsAlign == AlignCenter { x = (x + rightLimit - buttonsWidth) / 2 } + + // In vertical layouts, buttons always appear after an empty line. + if f.itemPadding == 0 { + y++ + } } // Draw them. - if f.itemPadding == 0 { - y++ - } - if y >= bottomLimit { - return // Stop here. - } for index, button := range f.buttons { - space := rightLimit - x - if space < 1 { - break // No space for this button anymore. + if y >= bottomLimit { + return // Stop here. } + + space := rightLimit - x buttonWidth := buttonWidths[index] + if f.horizontal { + if space < buttonWidth-4 { + x = startX + y += 2 + space = width + } + } else { + if space < 1 { + break // No space for this button anymore. + } + } if buttonWidth > space { buttonWidth = space } @@ -298,7 +380,7 @@ func (f *Form) Draw(screen tcell.Screen) { SetRect(x, y, buttonWidth, 1) button.Draw(screen) - x += buttonWidth + 2 + x += buttonWidth + 1 } } diff --git a/inputfield.go b/inputfield.go index 717f62c..5ecfdf3 100644 --- a/inputfield.go +++ b/inputfield.go @@ -125,6 +125,11 @@ func (i *InputField) SetFieldLength(length int) *InputField { return i } +// GetFieldLength returns this primitive's field length. +func (i *InputField) GetFieldLength() int { + return i.fieldLength +} + // SetMaskCharacter sets a character that masks user input on a screen. A value // of 0 disables masking. func (i *InputField) SetMaskCharacter(mask rune) *InputField {