1package tview
2
3import (
4	"github.com/gdamore/tcell"
5)
6
7// DefaultFormFieldWidth is the default field screen width of form elements
8// whose field width is flexible (0). This is used in the Form class for
9// horizontal layouts.
10var DefaultFormFieldWidth = 10
11
12// FormItem is the interface all form items must implement to be able to be
13// included in a form.
14type FormItem interface {
15	Primitive
16
17	// GetLabel returns the item's label text.
18	GetLabel() string
19
20	// SetFormAttributes sets a number of item attributes at once.
21	SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem
22
23	// GetFieldWidth returns the width of the form item's field (the area which
24	// is manipulated by the user) in number of screen cells. A value of 0
25	// indicates the the field width is flexible and may use as much space as
26	// required.
27	GetFieldWidth() int
28
29	// SetFinishedFunc sets the handler function for when the user finished
30	// entering data into the item. The handler may receive events for the
31	// Enter key (we're done), the Escape key (cancel input), the Tab key (move to
32	// next field), and the Backtab key (move to previous field).
33	SetFinishedFunc(handler func(key tcell.Key)) FormItem
34}
35
36// Form allows you to combine multiple one-line form elements into a vertical
37// or horizontal layout. Form elements include types such as InputField or
38// Checkbox. These elements can be optionally followed by one or more buttons
39// for which you can define form-wide actions (e.g. Save, Clear, Cancel).
40//
41// See https://github.com/rivo/tview/wiki/Form for an example.
42type Form struct {
43	*Box
44
45	// The items of the form (one row per item).
46	items []FormItem
47
48	// The buttons of the form.
49	buttons []*Button
50
51	// If set to true, instead of position items and buttons from top to bottom,
52	// they are positioned from left to right.
53	horizontal bool
54
55	// The alignment of the buttons.
56	buttonsAlign int
57
58	// The number of empty rows between items.
59	itemPadding int
60
61	// The index of the item or button which has focus. (Items are counted first,
62	// buttons are counted last.)
63	focusedElement int
64
65	// The label color.
66	labelColor tcell.Color
67
68	// The background color of the input area.
69	fieldBackgroundColor tcell.Color
70
71	// The text color of the input area.
72	fieldTextColor tcell.Color
73
74	// The background color of the buttons.
75	buttonBackgroundColor tcell.Color
76
77	// The color of the button text.
78	buttonTextColor tcell.Color
79
80	// An optional function which is called when the user hits Escape.
81	cancel func()
82}
83
84// NewForm returns a new form.
85func NewForm() *Form {
86	box := NewBox().SetBorderPadding(1, 1, 1, 1)
87
88	f := &Form{
89		Box:                   box,
90		itemPadding:           1,
91		labelColor:            Styles.SecondaryTextColor,
92		fieldBackgroundColor:  Styles.ContrastBackgroundColor,
93		fieldTextColor:        Styles.PrimaryTextColor,
94		buttonBackgroundColor: Styles.ContrastBackgroundColor,
95		buttonTextColor:       Styles.PrimaryTextColor,
96	}
97
98	f.focus = f
99
100	return f
101}
102
103// SetItemPadding sets the number of empty rows between form items for vertical
104// layouts and the number of empty cells between form items for horizontal
105// layouts.
106func (f *Form) SetItemPadding(padding int) *Form {
107	f.itemPadding = padding
108	return f
109}
110
111// SetHorizontal sets the direction the form elements are laid out. If set to
112// true, instead of positioning them from top to bottom (the default), they are
113// positioned from left to right, moving into the next row if there is not
114// enough space.
115func (f *Form) SetHorizontal(horizontal bool) *Form {
116	f.horizontal = horizontal
117	return f
118}
119
120// SetLabelColor sets the color of the labels.
121func (f *Form) SetLabelColor(color tcell.Color) *Form {
122	f.labelColor = color
123	return f
124}
125
126// SetFieldBackgroundColor sets the background color of the input areas.
127func (f *Form) SetFieldBackgroundColor(color tcell.Color) *Form {
128	f.fieldBackgroundColor = color
129	return f
130}
131
132// SetFieldTextColor sets the text color of the input areas.
133func (f *Form) SetFieldTextColor(color tcell.Color) *Form {
134	f.fieldTextColor = color
135	return f
136}
137
138// SetButtonsAlign sets how the buttons align horizontally, one of AlignLeft
139// (the default), AlignCenter, and AlignRight. This is only
140func (f *Form) SetButtonsAlign(align int) *Form {
141	f.buttonsAlign = align
142	return f
143}
144
145// SetButtonBackgroundColor sets the background color of the buttons.
146func (f *Form) SetButtonBackgroundColor(color tcell.Color) *Form {
147	f.buttonBackgroundColor = color
148	return f
149}
150
151// SetButtonTextColor sets the color of the button texts.
152func (f *Form) SetButtonTextColor(color tcell.Color) *Form {
153	f.buttonTextColor = color
154	return f
155}
156
157// SetFocus shifts the focus to the form element with the given index, counting
158// non-button items first and buttons last. Note that this index is only used
159// when the form itself receives focus.
160func (f *Form) SetFocus(index int) *Form {
161	if index < 0 {
162		f.focusedElement = 0
163	} else if index >= len(f.items)+len(f.buttons) {
164		f.focusedElement = len(f.items) + len(f.buttons)
165	} else {
166		f.focusedElement = index
167	}
168	return f
169}
170
171// AddInputField adds an input field to the form. It has a label, an optional
172// initial value, a field width (a value of 0 extends it as far as possible),
173// an optional accept function to validate the item's value (set to nil to
174// accept any text), and an (optional) callback function which is invoked when
175// the input field's text has changed.
176func (f *Form) AddInputField(label, value string, fieldWidth int, accept func(textToCheck string, lastChar rune) bool, changed func(text string)) *Form {
177	f.items = append(f.items, NewInputField().
178		SetLabel(label).
179		SetText(value).
180		SetFieldWidth(fieldWidth).
181		SetAcceptanceFunc(accept).
182		SetChangedFunc(changed))
183	return f
184}
185
186// AddPasswordField adds a password field to the form. This is similar to an
187// input field except that the user's input not shown. Instead, a "mask"
188// character is displayed. The password field has a label, an optional initial
189// value, a field width (a value of 0 extends it as far as possible), and an
190// (optional) callback function which is invoked when the input field's text has
191// changed.
192func (f *Form) AddPasswordField(label, value string, fieldWidth int, mask rune, changed func(text string)) *Form {
193	if mask == 0 {
194		mask = '*'
195	}
196	f.items = append(f.items, NewInputField().
197		SetLabel(label).
198		SetText(value).
199		SetFieldWidth(fieldWidth).
200		SetMaskCharacter(mask).
201		SetChangedFunc(changed))
202	return f
203}
204
205// AddDropDown adds a drop-down element to the form. It has a label, options,
206// and an (optional) callback function which is invoked when an option was
207// selected. The initial option may be a negative value to indicate that no
208// option is currently selected.
209func (f *Form) AddDropDown(label string, options []string, initialOption int, selected func(option string, optionIndex int)) *Form {
210	f.items = append(f.items, NewDropDown().
211		SetLabel(label).
212		SetOptions(options, selected).
213		SetCurrentOption(initialOption))
214	return f
215}
216
217// AddCheckbox adds a checkbox to the form. It has a label, an initial state,
218// and an (optional) callback function which is invoked when the state of the
219// checkbox was changed by the user.
220func (f *Form) AddCheckbox(label string, checked bool, changed func(checked bool)) *Form {
221	f.items = append(f.items, NewCheckbox().
222		SetLabel(label).
223		SetChecked(checked).
224		SetChangedFunc(changed))
225	return f
226}
227
228// AddButton adds a new button to the form. The "selected" function is called
229// when the user selects this button. It may be nil.
230func (f *Form) AddButton(label string, selected func()) *Form {
231	f.buttons = append(f.buttons, NewButton(label).SetSelectedFunc(selected))
232	return f
233}
234
235// GetButton returns the button at the specified 0-based index. Note that
236// buttons have been specially prepared for this form and modifying some of
237// their attributes may have unintended side effects.
238func (f *Form) GetButton(index int) *Button {
239	return f.buttons[index]
240}
241
242// RemoveButton removes the button at the specified position, starting with 0
243// for the button that was added first.
244func (f *Form) RemoveButton(index int) *Form {
245	f.buttons = append(f.buttons[:index], f.buttons[index+1:]...)
246	return f
247}
248
249// GetButtonCount returns the number of buttons in this form.
250func (f *Form) GetButtonCount() int {
251	return len(f.buttons)
252}
253
254// GetButtonIndex returns the index of the button with the given label, starting
255// with 0 for the button that was added first. If no such label was found, -1
256// is returned.
257func (f *Form) GetButtonIndex(label string) int {
258	for index, button := range f.buttons {
259		if button.GetLabel() == label {
260			return index
261		}
262	}
263	return -1
264}
265
266// Clear removes all input elements from the form, including the buttons if
267// specified.
268func (f *Form) Clear(includeButtons bool) *Form {
269	f.items = nil
270	if includeButtons {
271		f.ClearButtons()
272	}
273	f.focusedElement = 0
274	return f
275}
276
277// ClearButtons removes all buttons from the form.
278func (f *Form) ClearButtons() *Form {
279	f.buttons = nil
280	return f
281}
282
283// AddFormItem adds a new item to the form. This can be used to add your own
284// objects to the form. Note, however, that the Form class will override some
285// of its attributes to make it work in the form context. Specifically, these
286// are:
287//
288//   - The label width
289//   - The label color
290//   - The background color
291//   - The field text color
292//   - The field background color
293func (f *Form) AddFormItem(item FormItem) *Form {
294	f.items = append(f.items, item)
295	return f
296}
297
298// GetFormItem returns the form element at the given position, starting with
299// index 0. Elements are referenced in the order they were added. Buttons are
300// not included.
301func (f *Form) GetFormItem(index int) FormItem {
302	return f.items[index]
303}
304
305// RemoveFormItem removes the form element at the given position, starting with
306// index 0. Elements are referenced in the order they were added. Buttons are
307// not included.
308func (f *Form) RemoveFormItem(index int) *Form {
309	f.items = append(f.items[:index], f.items[index+1:]...)
310	return f
311}
312
313// GetFormItemByLabel returns the first form element with the given label. If
314// no such element is found, nil is returned. Buttons are not searched and will
315// therefore not be returned.
316func (f *Form) GetFormItemByLabel(label string) FormItem {
317	for _, item := range f.items {
318		if item.GetLabel() == label {
319			return item
320		}
321	}
322	return nil
323}
324
325// GetFormItemIndex returns the index of the first form element with the given
326// label. If no such element is found, -1 is returned. Buttons are not searched
327// and will therefore not be returned.
328func (f *Form) GetFormItemIndex(label string) int {
329	for index, item := range f.items {
330		if item.GetLabel() == label {
331			return index
332		}
333	}
334	return -1
335}
336
337// SetCancelFunc sets a handler which is called when the user hits the Escape
338// key.
339func (f *Form) SetCancelFunc(callback func()) *Form {
340	f.cancel = callback
341	return f
342}
343
344// Draw draws this primitive onto the screen.
345func (f *Form) Draw(screen tcell.Screen) {
346	f.Box.Draw(screen)
347
348	// Determine the dimensions.
349	x, y, width, height := f.GetInnerRect()
350	topLimit := y
351	bottomLimit := y + height
352	rightLimit := x + width
353	startX := x
354
355	// Find the longest label.
356	var maxLabelWidth int
357	for _, item := range f.items {
358		labelWidth := TaggedStringWidth(item.GetLabel())
359		if labelWidth > maxLabelWidth {
360			maxLabelWidth = labelWidth
361		}
362	}
363	maxLabelWidth++ // Add one space.
364
365	// Calculate positions of form items.
366	positions := make([]struct{ x, y, width, height int }, len(f.items)+len(f.buttons))
367	var focusedPosition struct{ x, y, width, height int }
368	for index, item := range f.items {
369		// Calculate the space needed.
370		labelWidth := TaggedStringWidth(item.GetLabel())
371		var itemWidth int
372		if f.horizontal {
373			fieldWidth := item.GetFieldWidth()
374			if fieldWidth == 0 {
375				fieldWidth = DefaultFormFieldWidth
376			}
377			labelWidth++
378			itemWidth = labelWidth + fieldWidth
379		} else {
380			// We want all fields to align vertically.
381			labelWidth = maxLabelWidth
382			itemWidth = width
383		}
384
385		// Advance to next line if there is no space.
386		if f.horizontal && x+labelWidth+1 >= rightLimit {
387			x = startX
388			y += 2
389		}
390
391		// Adjust the item's attributes.
392		if x+itemWidth >= rightLimit {
393			itemWidth = rightLimit - x
394		}
395		item.SetFormAttributes(
396			labelWidth,
397			f.labelColor,
398			f.backgroundColor,
399			f.fieldTextColor,
400			f.fieldBackgroundColor,
401		)
402
403		// Save position.
404		positions[index].x = x
405		positions[index].y = y
406		positions[index].width = itemWidth
407		positions[index].height = 1
408		if item.GetFocusable().HasFocus() {
409			focusedPosition = positions[index]
410		}
411
412		// Advance to next item.
413		if f.horizontal {
414			x += itemWidth + f.itemPadding
415		} else {
416			y += 1 + f.itemPadding
417		}
418	}
419
420	// How wide are the buttons?
421	buttonWidths := make([]int, len(f.buttons))
422	buttonsWidth := 0
423	for index, button := range f.buttons {
424		w := TaggedStringWidth(button.GetLabel()) + 4
425		buttonWidths[index] = w
426		buttonsWidth += w + 1
427	}
428	buttonsWidth--
429
430	// Where do we place them?
431	if !f.horizontal && x+buttonsWidth < rightLimit {
432		if f.buttonsAlign == AlignRight {
433			x = rightLimit - buttonsWidth
434		} else if f.buttonsAlign == AlignCenter {
435			x = (x + rightLimit - buttonsWidth) / 2
436		}
437
438		// In vertical layouts, buttons always appear after an empty line.
439		if f.itemPadding == 0 {
440			y++
441		}
442	}
443
444	// Calculate positions of buttons.
445	for index, button := range f.buttons {
446		space := rightLimit - x
447		buttonWidth := buttonWidths[index]
448		if f.horizontal {
449			if space < buttonWidth-4 {
450				x = startX
451				y += 2
452				space = width
453			}
454		} else {
455			if space < 1 {
456				break // No space for this button anymore.
457			}
458		}
459		if buttonWidth > space {
460			buttonWidth = space
461		}
462		button.SetLabelColor(f.buttonTextColor).
463			SetLabelColorActivated(f.buttonBackgroundColor).
464			SetBackgroundColorActivated(f.buttonTextColor).
465			SetBackgroundColor(f.buttonBackgroundColor)
466
467		buttonIndex := index + len(f.items)
468		positions[buttonIndex].x = x
469		positions[buttonIndex].y = y
470		positions[buttonIndex].width = buttonWidth
471		positions[buttonIndex].height = 1
472
473		if button.HasFocus() {
474			focusedPosition = positions[buttonIndex]
475		}
476
477		x += buttonWidth + 1
478	}
479
480	// Determine vertical offset based on the position of the focused item.
481	var offset int
482	if focusedPosition.y+focusedPosition.height > bottomLimit {
483		offset = focusedPosition.y + focusedPosition.height - bottomLimit
484		if focusedPosition.y-offset < topLimit {
485			offset = focusedPosition.y - topLimit
486		}
487	}
488
489	// Draw items.
490	for index, item := range f.items {
491		// Set position.
492		y := positions[index].y - offset
493		height := positions[index].height
494		item.SetRect(positions[index].x, y, positions[index].width, height)
495
496		// Is this item visible?
497		if y+height <= topLimit || y >= bottomLimit {
498			continue
499		}
500
501		// Draw items with focus last (in case of overlaps).
502		if item.GetFocusable().HasFocus() {
503			defer item.Draw(screen)
504		} else {
505			item.Draw(screen)
506		}
507	}
508
509	// Draw buttons.
510	for index, button := range f.buttons {
511		// Set position.
512		buttonIndex := index + len(f.items)
513		y := positions[buttonIndex].y - offset
514		height := positions[buttonIndex].height
515		button.SetRect(positions[buttonIndex].x, y, positions[buttonIndex].width, height)
516
517		// Is this button visible?
518		if y+height <= topLimit || y >= bottomLimit {
519			continue
520		}
521
522		// Draw button.
523		button.Draw(screen)
524	}
525}
526
527// Focus is called by the application when the primitive receives focus.
528func (f *Form) Focus(delegate func(p Primitive)) {
529	if len(f.items)+len(f.buttons) == 0 {
530		f.hasFocus = true
531		return
532	}
533	f.hasFocus = false
534
535	// Hand on the focus to one of our child elements.
536	if f.focusedElement < 0 || f.focusedElement >= len(f.items)+len(f.buttons) {
537		f.focusedElement = 0
538	}
539	handler := func(key tcell.Key) {
540		switch key {
541		case tcell.KeyTab, tcell.KeyEnter:
542			f.focusedElement++
543			f.Focus(delegate)
544		case tcell.KeyBacktab:
545			f.focusedElement--
546			if f.focusedElement < 0 {
547				f.focusedElement = len(f.items) + len(f.buttons) - 1
548			}
549			f.Focus(delegate)
550		case tcell.KeyEscape:
551			if f.cancel != nil {
552				f.cancel()
553			} else {
554				f.focusedElement = 0
555				f.Focus(delegate)
556			}
557		}
558	}
559
560	if f.focusedElement < len(f.items) {
561		// We're selecting an item.
562		item := f.items[f.focusedElement]
563		item.SetFinishedFunc(handler)
564		delegate(item)
565	} else {
566		// We're selecting a button.
567		button := f.buttons[f.focusedElement-len(f.items)]
568		button.SetBlurFunc(handler)
569		delegate(button)
570	}
571}
572
573// HasFocus returns whether or not this primitive has focus.
574func (f *Form) HasFocus() bool {
575	if f.hasFocus {
576		return true
577	}
578	for _, item := range f.items {
579		if item.GetFocusable().HasFocus() {
580			return true
581		}
582	}
583	for _, button := range f.buttons {
584		if button.focus.HasFocus() {
585			return true
586		}
587	}
588	return false
589}
590