...

Source file src/github.com/rivo/tview/form.go

Documentation: github.com/rivo/tview

     1  package tview
     2  
     3  import (
     4  	"image"
     5  
     6  	"github.com/gdamore/tcell/v2"
     7  )
     8  
     9  var (
    10  	// DefaultFormFieldWidth is the default field screen width of form elements
    11  	// whose field width is flexible (0). This is used in the Form class for
    12  	// horizontal layouts.
    13  	DefaultFormFieldWidth = 10
    14  
    15  	// DefaultFormFieldHeight is the default field height of multi-line form
    16  	// elements whose field height is flexible (0).
    17  	DefaultFormFieldHeight = 5
    18  )
    19  
    20  // FormItem is the interface all form items must implement to be able to be
    21  // included in a form.
    22  type FormItem interface {
    23  	Primitive
    24  
    25  	// GetLabel returns the item's label text.
    26  	GetLabel() string
    27  
    28  	// SetFormAttributes sets a number of item attributes at once.
    29  	SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem
    30  
    31  	// GetFieldWidth returns the width of the form item's field (the area which
    32  	// is manipulated by the user) in number of screen cells. A value of 0
    33  	// indicates the the field width is flexible and may use as much space as
    34  	// required.
    35  	GetFieldWidth() int
    36  
    37  	// GetFieldHeight returns the height of the form item's field (the area which
    38  	// is manipulated by the user). This value must be greater than 0.
    39  	GetFieldHeight() int
    40  
    41  	// SetFinishedFunc sets the handler function for when the user finished
    42  	// entering data into the item. The handler may receive events for the
    43  	// Enter key (we're done), the Escape key (cancel input), the Tab key (move
    44  	// to next field), the Backtab key (move to previous field), or a negative
    45  	// value, indicating that the action for the last known key should be
    46  	// repeated.
    47  	SetFinishedFunc(handler func(key tcell.Key)) FormItem
    48  
    49  	// SetDisabled sets whether or not the item is disabled / read-only. A form
    50  	// must have at least one item that is not disabled.
    51  	SetDisabled(disabled bool) FormItem
    52  }
    53  
    54  // Form allows you to combine multiple one-line form elements into a vertical
    55  // or horizontal layout. Form elements include types such as InputField or
    56  // Checkbox. These elements can be optionally followed by one or more buttons
    57  // for which you can define form-wide actions (e.g. Save, Clear, Cancel).
    58  //
    59  // See https://github.com/rivo/tview/wiki/Form for an example.
    60  type Form struct {
    61  	*Box
    62  
    63  	// The items of the form (one row per item).
    64  	items []FormItem
    65  
    66  	// The buttons of the form.
    67  	buttons []*Button
    68  
    69  	// If set to true, instead of position items and buttons from top to bottom,
    70  	// they are positioned from left to right.
    71  	horizontal bool
    72  
    73  	// The alignment of the buttons.
    74  	buttonsAlign int
    75  
    76  	// The number of empty cells between items.
    77  	itemPadding int
    78  
    79  	// The index of the item or button which has focus. (Items are counted first,
    80  	// buttons are counted last.) This is only used when the form itself receives
    81  	// focus so that the last element that had focus keeps it.
    82  	focusedElement int
    83  
    84  	// The label color.
    85  	labelColor tcell.Color
    86  
    87  	// The background color of the input area.
    88  	fieldBackgroundColor tcell.Color
    89  
    90  	// The text color of the input area.
    91  	fieldTextColor tcell.Color
    92  
    93  	// The style of the buttons when they are not focused.
    94  	buttonStyle tcell.Style
    95  
    96  	// The style of the buttons when they are focused.
    97  	buttonActivatedStyle tcell.Style
    98  
    99  	// The style of the buttons when they are disabled.
   100  	buttonDisabledStyle tcell.Style
   101  
   102  	// The last (valid) key that wsa sent to a "finished" handler or -1 if no
   103  	// such key is known yet.
   104  	lastFinishedKey tcell.Key
   105  
   106  	// An optional function which is called when the user hits Escape.
   107  	cancel func()
   108  }
   109  
   110  // NewForm returns a new form.
   111  func NewForm() *Form {
   112  	box := NewBox().SetBorderPadding(1, 1, 1, 1)
   113  
   114  	f := &Form{
   115  		Box:                  box,
   116  		itemPadding:          1,
   117  		labelColor:           Styles.SecondaryTextColor,
   118  		fieldBackgroundColor: Styles.ContrastBackgroundColor,
   119  		fieldTextColor:       Styles.PrimaryTextColor,
   120  		buttonStyle:          tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.PrimaryTextColor),
   121  		buttonActivatedStyle: tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.ContrastBackgroundColor),
   122  		lastFinishedKey:      tcell.KeyTab, // To skip over inactive elements at the beginning of the form.
   123  	}
   124  
   125  	return f
   126  }
   127  
   128  // SetItemPadding sets the number of empty rows between form items for vertical
   129  // layouts and the number of empty cells between form items for horizontal
   130  // layouts.
   131  func (f *Form) SetItemPadding(padding int) *Form {
   132  	f.itemPadding = padding
   133  	return f
   134  }
   135  
   136  // SetHorizontal sets the direction the form elements are laid out. If set to
   137  // true, instead of positioning them from top to bottom (the default), they are
   138  // positioned from left to right, moving into the next row if there is not
   139  // enough space.
   140  func (f *Form) SetHorizontal(horizontal bool) *Form {
   141  	f.horizontal = horizontal
   142  	return f
   143  }
   144  
   145  // SetLabelColor sets the color of the labels.
   146  func (f *Form) SetLabelColor(color tcell.Color) *Form {
   147  	f.labelColor = color
   148  	return f
   149  }
   150  
   151  // SetFieldBackgroundColor sets the background color of the input areas.
   152  func (f *Form) SetFieldBackgroundColor(color tcell.Color) *Form {
   153  	f.fieldBackgroundColor = color
   154  	return f
   155  }
   156  
   157  // SetFieldTextColor sets the text color of the input areas.
   158  func (f *Form) SetFieldTextColor(color tcell.Color) *Form {
   159  	f.fieldTextColor = color
   160  	return f
   161  }
   162  
   163  // SetButtonsAlign sets how the buttons align horizontally, one of AlignLeft
   164  // (the default), AlignCenter, and AlignRight. This is only
   165  func (f *Form) SetButtonsAlign(align int) *Form {
   166  	f.buttonsAlign = align
   167  	return f
   168  }
   169  
   170  // SetButtonBackgroundColor sets the background color of the buttons. This is
   171  // also the text color of the buttons when they are focused.
   172  func (f *Form) SetButtonBackgroundColor(color tcell.Color) *Form {
   173  	f.buttonStyle = f.buttonStyle.Background(color)
   174  	f.buttonActivatedStyle = f.buttonActivatedStyle.Foreground(color)
   175  	return f
   176  }
   177  
   178  // SetButtonTextColor sets the color of the button texts. This is also the
   179  // background of the buttons when they are focused.
   180  func (f *Form) SetButtonTextColor(color tcell.Color) *Form {
   181  	f.buttonStyle = f.buttonStyle.Foreground(color)
   182  	f.buttonActivatedStyle = f.buttonActivatedStyle.Background(color)
   183  	return f
   184  }
   185  
   186  // SetButtonStyle sets the style of the buttons when they are not focused.
   187  func (f *Form) SetButtonStyle(style tcell.Style) *Form {
   188  	f.buttonStyle = style
   189  	return f
   190  }
   191  
   192  // SetButtonActivatedStyle sets the style of the buttons when they are focused.
   193  func (f *Form) SetButtonActivatedStyle(style tcell.Style) *Form {
   194  	f.buttonActivatedStyle = style
   195  	return f
   196  }
   197  
   198  // SetFocus shifts the focus to the form element with the given index, counting
   199  // non-button items first and buttons last. Note that this index is only used
   200  // when the form itself receives focus.
   201  func (f *Form) SetFocus(index int) *Form {
   202  	if index < 0 {
   203  		f.focusedElement = 0
   204  	} else if index >= len(f.items)+len(f.buttons) {
   205  		f.focusedElement = len(f.items) + len(f.buttons)
   206  	} else {
   207  		f.focusedElement = index
   208  	}
   209  	return f
   210  }
   211  
   212  // AddTextArea adds a text area to the form. It has a label, an optional initial
   213  // text, a size (width and height) referring to the actual input area (a
   214  // fieldWidth of 0 extends it as far right as possible, a fieldHeight of 0 will
   215  // cause it to be [DefaultFormFieldHeight]), and a maximum number of bytes of
   216  // text allowed (0 means no limit).
   217  //
   218  // The optional callback function is invoked when the content of the text area
   219  // has changed. Note that especially for larger texts, this is an expensive
   220  // operation due to technical constraints of the [TextArea] primitive (every key
   221  // stroke leads to a new reallocation of the entire text).
   222  func (f *Form) AddTextArea(label, text string, fieldWidth, fieldHeight, maxLength int, changed func(text string)) *Form {
   223  	if fieldHeight == 0 {
   224  		fieldHeight = DefaultFormFieldHeight
   225  	}
   226  	textArea := NewTextArea().
   227  		SetLabel(label).
   228  		SetSize(fieldHeight, fieldWidth).
   229  		SetMaxLength(maxLength)
   230  	if text != "" {
   231  		textArea.SetText(text, true)
   232  	}
   233  	if changed != nil {
   234  		textArea.SetChangedFunc(func() {
   235  			changed(textArea.GetText())
   236  		})
   237  	}
   238  	f.items = append(f.items, textArea)
   239  	return f
   240  }
   241  
   242  // AddTextView adds a text view to the form. It has a label and text, a size
   243  // (width and height) referring to the actual text element (a fieldWidth of 0
   244  // extends it as far right as possible, a fieldHeight of 0 will cause it to be
   245  // [DefaultFormFieldHeight]), a flag to turn on/off dynamic colors, and a flag
   246  // to turn on/off scrolling. If scrolling is turned off, the text view will not
   247  // receive focus.
   248  func (f *Form) AddTextView(label, text string, fieldWidth, fieldHeight int, dynamicColors, scrollable bool) *Form {
   249  	if fieldHeight == 0 {
   250  		fieldHeight = DefaultFormFieldHeight
   251  	}
   252  	textArea := NewTextView().
   253  		SetLabel(label).
   254  		SetSize(fieldHeight, fieldWidth).
   255  		SetDynamicColors(dynamicColors).
   256  		SetScrollable(scrollable).
   257  		SetText(text)
   258  	f.items = append(f.items, textArea)
   259  	return f
   260  }
   261  
   262  // AddInputField adds an input field to the form. It has a label, an optional
   263  // initial value, a field width (a value of 0 extends it as far as possible),
   264  // an optional accept function to validate the item's value (set to nil to
   265  // accept any text), and an (optional) callback function which is invoked when
   266  // the input field's text has changed.
   267  func (f *Form) AddInputField(label, value string, fieldWidth int, accept func(textToCheck string, lastChar rune) bool, changed func(text string)) *Form {
   268  	f.items = append(f.items, NewInputField().
   269  		SetLabel(label).
   270  		SetText(value).
   271  		SetFieldWidth(fieldWidth).
   272  		SetAcceptanceFunc(accept).
   273  		SetChangedFunc(changed))
   274  	return f
   275  }
   276  
   277  // AddPasswordField adds a password field to the form. This is similar to an
   278  // input field except that the user's input not shown. Instead, a "mask"
   279  // character is displayed. The password field has a label, an optional initial
   280  // value, a field width (a value of 0 extends it as far as possible), and an
   281  // (optional) callback function which is invoked when the input field's text has
   282  // changed.
   283  func (f *Form) AddPasswordField(label, value string, fieldWidth int, mask rune, changed func(text string)) *Form {
   284  	if mask == 0 {
   285  		mask = '*'
   286  	}
   287  	f.items = append(f.items, NewInputField().
   288  		SetLabel(label).
   289  		SetText(value).
   290  		SetFieldWidth(fieldWidth).
   291  		SetMaskCharacter(mask).
   292  		SetChangedFunc(changed))
   293  	return f
   294  }
   295  
   296  // AddDropDown adds a drop-down element to the form. It has a label, options,
   297  // and an (optional) callback function which is invoked when an option was
   298  // selected. The initial option may be a negative value to indicate that no
   299  // option is currently selected.
   300  func (f *Form) AddDropDown(label string, options []string, initialOption int, selected func(option string, optionIndex int)) *Form {
   301  	f.items = append(f.items, NewDropDown().
   302  		SetLabel(label).
   303  		SetOptions(options, selected).
   304  		SetCurrentOption(initialOption))
   305  	return f
   306  }
   307  
   308  // AddCheckbox adds a checkbox to the form. It has a label, an initial state,
   309  // and an (optional) callback function which is invoked when the state of the
   310  // checkbox was changed by the user.
   311  func (f *Form) AddCheckbox(label string, checked bool, changed func(checked bool)) *Form {
   312  	f.items = append(f.items, NewCheckbox().
   313  		SetLabel(label).
   314  		SetChecked(checked).
   315  		SetChangedFunc(changed))
   316  	return f
   317  }
   318  
   319  // AddImage adds an image to the form. It has a label and the image will fit in
   320  // the specified width and height (its aspect ratio is preserved). See
   321  // [Image.SetColors] for a description of the "colors" parameter. Images are not
   322  // interactive and are skipped over in a form. The "width" value may be 0
   323  // (adjust dynamically) but "height" should generally be a positive value.
   324  func (f *Form) AddImage(label string, image image.Image, width, height, colors int) *Form {
   325  	f.items = append(f.items, NewImage().
   326  		SetLabel(label).
   327  		SetImage(image).
   328  		SetSize(height, width).
   329  		SetAlign(AlignTop, AlignLeft).
   330  		SetColors(colors))
   331  	return f
   332  }
   333  
   334  // AddButton adds a new button to the form. The "selected" function is called
   335  // when the user selects this button. It may be nil.
   336  func (f *Form) AddButton(label string, selected func()) *Form {
   337  	f.buttons = append(f.buttons, NewButton(label).SetSelectedFunc(selected))
   338  	return f
   339  }
   340  
   341  // GetButton returns the button at the specified 0-based index. Note that
   342  // buttons have been specially prepared for this form and modifying some of
   343  // their attributes may have unintended side effects.
   344  func (f *Form) GetButton(index int) *Button {
   345  	return f.buttons[index]
   346  }
   347  
   348  // RemoveButton removes the button at the specified position, starting with 0
   349  // for the button that was added first.
   350  func (f *Form) RemoveButton(index int) *Form {
   351  	f.buttons = append(f.buttons[:index], f.buttons[index+1:]...)
   352  	return f
   353  }
   354  
   355  // GetButtonCount returns the number of buttons in this form.
   356  func (f *Form) GetButtonCount() int {
   357  	return len(f.buttons)
   358  }
   359  
   360  // GetButtonIndex returns the index of the button with the given label, starting
   361  // with 0 for the button that was added first. If no such label was found, -1
   362  // is returned.
   363  func (f *Form) GetButtonIndex(label string) int {
   364  	for index, button := range f.buttons {
   365  		if button.GetLabel() == label {
   366  			return index
   367  		}
   368  	}
   369  	return -1
   370  }
   371  
   372  // Clear removes all input elements from the form, including the buttons if
   373  // specified.
   374  func (f *Form) Clear(includeButtons bool) *Form {
   375  	f.items = nil
   376  	if includeButtons {
   377  		f.ClearButtons()
   378  	}
   379  	f.focusedElement = 0
   380  	return f
   381  }
   382  
   383  // ClearButtons removes all buttons from the form.
   384  func (f *Form) ClearButtons() *Form {
   385  	f.buttons = nil
   386  	return f
   387  }
   388  
   389  // AddFormItem adds a new item to the form. This can be used to add your own
   390  // objects to the form. Note, however, that the Form class will override some
   391  // of its attributes to make it work in the form context. Specifically, these
   392  // are:
   393  //
   394  //   - The label width
   395  //   - The label color
   396  //   - The background color
   397  //   - The field text color
   398  //   - The field background color
   399  func (f *Form) AddFormItem(item FormItem) *Form {
   400  	f.items = append(f.items, item)
   401  	return f
   402  }
   403  
   404  // GetFormItemCount returns the number of items in the form (not including the
   405  // buttons).
   406  func (f *Form) GetFormItemCount() int {
   407  	return len(f.items)
   408  }
   409  
   410  // GetFormItem returns the form item at the given position, starting with index
   411  // 0. Elements are referenced in the order they were added. Buttons are not
   412  // included.
   413  func (f *Form) GetFormItem(index int) FormItem {
   414  	return f.items[index]
   415  }
   416  
   417  // RemoveFormItem removes the form element at the given position, starting with
   418  // index 0. Elements are referenced in the order they were added. Buttons are
   419  // not included.
   420  func (f *Form) RemoveFormItem(index int) *Form {
   421  	f.items = append(f.items[:index], f.items[index+1:]...)
   422  	return f
   423  }
   424  
   425  // GetFormItemByLabel returns the first form element with the given label. If
   426  // no such element is found, nil is returned. Buttons are not searched and will
   427  // therefore not be returned.
   428  func (f *Form) GetFormItemByLabel(label string) FormItem {
   429  	for _, item := range f.items {
   430  		if item.GetLabel() == label {
   431  			return item
   432  		}
   433  	}
   434  	return nil
   435  }
   436  
   437  // GetFormItemIndex returns the index of the first form element with the given
   438  // label. If no such element is found, -1 is returned. Buttons are not searched
   439  // and will therefore not be returned.
   440  func (f *Form) GetFormItemIndex(label string) int {
   441  	for index, item := range f.items {
   442  		if item.GetLabel() == label {
   443  			return index
   444  		}
   445  	}
   446  	return -1
   447  }
   448  
   449  // GetFocusedItemIndex returns the indices of the form element or button which
   450  // currently has focus. If they don't, -1 is returned resepectively.
   451  func (f *Form) GetFocusedItemIndex() (formItem, button int) {
   452  	index := f.focusIndex()
   453  	if index < 0 {
   454  		return -1, -1
   455  	}
   456  	if index < len(f.items) {
   457  		return index, -1
   458  	}
   459  	return -1, index - len(f.items)
   460  }
   461  
   462  // SetCancelFunc sets a handler which is called when the user hits the Escape
   463  // key.
   464  func (f *Form) SetCancelFunc(callback func()) *Form {
   465  	f.cancel = callback
   466  	return f
   467  }
   468  
   469  // Draw draws this primitive onto the screen.
   470  func (f *Form) Draw(screen tcell.Screen) {
   471  	f.Box.DrawForSubclass(screen, f)
   472  
   473  	// Determine the actual item that has focus.
   474  	if index := f.focusIndex(); index >= 0 {
   475  		f.focusedElement = index
   476  	}
   477  
   478  	// Determine the dimensions.
   479  	x, y, width, height := f.GetInnerRect()
   480  	topLimit := y
   481  	bottomLimit := y + height
   482  	rightLimit := x + width
   483  	startX := x
   484  
   485  	// Find the longest label.
   486  	var maxLabelWidth int
   487  	for _, item := range f.items {
   488  		labelWidth := TaggedStringWidth(item.GetLabel())
   489  		if labelWidth > maxLabelWidth {
   490  			maxLabelWidth = labelWidth
   491  		}
   492  	}
   493  	maxLabelWidth++ // Add one space.
   494  
   495  	// Calculate positions of form items.
   496  	type position struct{ x, y, width, height int }
   497  	positions := make([]position, len(f.items)+len(f.buttons))
   498  	var (
   499  		focusedPosition position
   500  		lineHeight      = 1
   501  	)
   502  	for index, item := range f.items {
   503  		// Calculate the space needed.
   504  		labelWidth := TaggedStringWidth(item.GetLabel())
   505  		var itemWidth int
   506  		if f.horizontal {
   507  			fieldWidth := item.GetFieldWidth()
   508  			if fieldWidth <= 0 {
   509  				fieldWidth = DefaultFormFieldWidth
   510  			}
   511  			labelWidth++
   512  			itemWidth = labelWidth + fieldWidth
   513  		} else {
   514  			// We want all fields to align vertically.
   515  			labelWidth = maxLabelWidth
   516  			itemWidth = width
   517  		}
   518  		itemHeight := item.GetFieldHeight()
   519  		if itemHeight <= 0 {
   520  			itemHeight = DefaultFormFieldHeight
   521  		}
   522  
   523  		// Advance to next line if there is no space.
   524  		if f.horizontal && x+labelWidth+1 >= rightLimit {
   525  			x = startX
   526  			y += lineHeight + 1
   527  			lineHeight = itemHeight
   528  		}
   529  
   530  		// Update line height.
   531  		if itemHeight > lineHeight {
   532  			lineHeight = itemHeight
   533  		}
   534  
   535  		// Adjust the item's attributes.
   536  		if x+itemWidth >= rightLimit {
   537  			itemWidth = rightLimit - x
   538  		}
   539  		item.SetFormAttributes(
   540  			labelWidth,
   541  			f.labelColor,
   542  			f.backgroundColor,
   543  			f.fieldTextColor,
   544  			f.fieldBackgroundColor,
   545  		)
   546  
   547  		// Save position.
   548  		positions[index].x = x
   549  		positions[index].y = y
   550  		positions[index].width = itemWidth
   551  		positions[index].height = itemHeight
   552  		if item.HasFocus() {
   553  			focusedPosition = positions[index]
   554  		}
   555  
   556  		// Advance to next item.
   557  		if f.horizontal {
   558  			x += itemWidth + f.itemPadding
   559  		} else {
   560  			y += itemHeight + f.itemPadding
   561  		}
   562  	}
   563  
   564  	// How wide are the buttons?
   565  	buttonWidths := make([]int, len(f.buttons))
   566  	buttonsWidth := 0
   567  	for index, button := range f.buttons {
   568  		w := TaggedStringWidth(button.GetLabel()) + 4
   569  		buttonWidths[index] = w
   570  		buttonsWidth += w + 1
   571  	}
   572  	buttonsWidth--
   573  
   574  	// Where do we place them?
   575  	if !f.horizontal && x+buttonsWidth < rightLimit {
   576  		if f.buttonsAlign == AlignRight {
   577  			x = rightLimit - buttonsWidth
   578  		} else if f.buttonsAlign == AlignCenter {
   579  			x = (x + rightLimit - buttonsWidth) / 2
   580  		}
   581  
   582  		// In vertical layouts, buttons always appear after an empty line.
   583  		if f.itemPadding == 0 {
   584  			y++
   585  		}
   586  	}
   587  
   588  	// Calculate positions of buttons.
   589  	for index, button := range f.buttons {
   590  		space := rightLimit - x
   591  		buttonWidth := buttonWidths[index]
   592  		if f.horizontal {
   593  			if space < buttonWidth-4 {
   594  				x = startX
   595  				y += lineHeight + 1
   596  				space = width
   597  				lineHeight = 1
   598  			}
   599  		} else {
   600  			if space < 1 {
   601  				break // No space for this button anymore.
   602  			}
   603  		}
   604  		if buttonWidth > space {
   605  			buttonWidth = space
   606  		}
   607  		button.SetStyle(f.buttonStyle).
   608  			SetActivatedStyle(f.buttonActivatedStyle)
   609  
   610  		buttonIndex := index + len(f.items)
   611  		positions[buttonIndex].x = x
   612  		positions[buttonIndex].y = y
   613  		positions[buttonIndex].width = buttonWidth
   614  		positions[buttonIndex].height = 1
   615  
   616  		if button.HasFocus() {
   617  			focusedPosition = positions[buttonIndex]
   618  		}
   619  
   620  		x += buttonWidth + 1
   621  	}
   622  
   623  	// Determine vertical offset based on the position of the focused item.
   624  	var offset int
   625  	if focusedPosition.y+focusedPosition.height > bottomLimit {
   626  		offset = focusedPosition.y + focusedPosition.height - bottomLimit
   627  		if focusedPosition.y-offset < topLimit {
   628  			offset = focusedPosition.y - topLimit
   629  		}
   630  	}
   631  
   632  	// Draw items.
   633  	for index, item := range f.items {
   634  		// Set position.
   635  		y := positions[index].y - offset
   636  		height := positions[index].height
   637  		item.SetRect(positions[index].x, y, positions[index].width, height)
   638  
   639  		// Is this item visible?
   640  		if y+height <= topLimit || y >= bottomLimit {
   641  			continue
   642  		}
   643  
   644  		// Draw items with focus last (in case of overlaps).
   645  		if item.HasFocus() {
   646  			defer item.Draw(screen)
   647  		} else {
   648  			item.Draw(screen)
   649  		}
   650  	}
   651  
   652  	// Draw buttons.
   653  	for index, button := range f.buttons {
   654  		// Set position.
   655  		buttonIndex := index + len(f.items)
   656  		y := positions[buttonIndex].y - offset
   657  		height := positions[buttonIndex].height
   658  		button.SetRect(positions[buttonIndex].x, y, positions[buttonIndex].width, height)
   659  
   660  		// Is this button visible?
   661  		if y+height <= topLimit || y >= bottomLimit {
   662  			continue
   663  		}
   664  
   665  		// Draw button.
   666  		button.Draw(screen)
   667  	}
   668  }
   669  
   670  // Focus is called by the application when the primitive receives focus.
   671  func (f *Form) Focus(delegate func(p Primitive)) {
   672  	// Hand on the focus to one of our child elements.
   673  	if f.focusedElement < 0 || f.focusedElement >= len(f.items)+len(f.buttons) {
   674  		f.focusedElement = 0
   675  	}
   676  	var handler func(key tcell.Key)
   677  	handler = func(key tcell.Key) {
   678  		if key >= 0 {
   679  			f.lastFinishedKey = key
   680  		}
   681  		switch key {
   682  		case tcell.KeyTab, tcell.KeyEnter:
   683  			f.focusedElement++
   684  			f.Focus(delegate)
   685  		case tcell.KeyBacktab:
   686  			f.focusedElement--
   687  			if f.focusedElement < 0 {
   688  				f.focusedElement = len(f.items) + len(f.buttons) - 1
   689  			}
   690  			f.Focus(delegate)
   691  		case tcell.KeyEscape:
   692  			if f.cancel != nil {
   693  				f.cancel()
   694  			} else {
   695  				f.focusedElement = 0
   696  				f.Focus(delegate)
   697  			}
   698  		default:
   699  			if key < 0 && f.lastFinishedKey >= 0 {
   700  				// Repeat the last action.
   701  				handler(f.lastFinishedKey)
   702  			}
   703  		}
   704  	}
   705  
   706  	// Track whether a form item has focus.
   707  	var itemFocused bool
   708  	f.hasFocus = false
   709  
   710  	// Set the handler and focus for all items and buttons.
   711  	for index, button := range f.buttons {
   712  		button.SetExitFunc(handler)
   713  		if f.focusedElement == index+len(f.items) {
   714  			if button.IsDisabled() {
   715  				f.focusedElement++
   716  				if f.focusedElement >= len(f.items)+len(f.buttons) {
   717  					f.focusedElement = 0
   718  				}
   719  				continue
   720  			}
   721  
   722  			itemFocused = true
   723  			func(b *Button) { // Wrapping might not be necessary anymore in future Go versions.
   724  				defer delegate(b)
   725  			}(button)
   726  		}
   727  	}
   728  	for index, item := range f.items {
   729  		item.SetFinishedFunc(handler)
   730  		if f.focusedElement == index {
   731  			itemFocused = true
   732  			func(i FormItem) { // Wrapping might not be necessary anymore in future Go versions.
   733  				defer delegate(i)
   734  			}(item)
   735  		}
   736  	}
   737  
   738  	// If no item was focused, focus the form itself.
   739  	if !itemFocused {
   740  		f.Box.Focus(delegate)
   741  	}
   742  }
   743  
   744  // HasFocus returns whether or not this primitive has focus.
   745  func (f *Form) HasFocus() bool {
   746  	if f.focusIndex() >= 0 {
   747  		return true
   748  	}
   749  	return f.Box.HasFocus()
   750  }
   751  
   752  // focusIndex returns the index of the currently focused item, counting form
   753  // items first, then buttons. A negative value indicates that no containeed item
   754  // has focus.
   755  func (f *Form) focusIndex() int {
   756  	for index, item := range f.items {
   757  		if item.HasFocus() {
   758  			return index
   759  		}
   760  	}
   761  	for index, button := range f.buttons {
   762  		if button.HasFocus() {
   763  			return len(f.items) + index
   764  		}
   765  	}
   766  	return -1
   767  }
   768  
   769  // MouseHandler returns the mouse handler for this primitive.
   770  func (f *Form) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   771  	return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   772  		// At the end, update f.focusedElement and prepare current item/button.
   773  		defer func() {
   774  			if consumed {
   775  				index := f.focusIndex()
   776  				if index >= 0 {
   777  					f.focusedElement = index
   778  				}
   779  			}
   780  		}()
   781  
   782  		// Determine items to pass mouse events to.
   783  		for _, item := range f.items {
   784  			// Exclude TextView items from mouse-down events as they are
   785  			// read-only items and thus should not be focused.
   786  			if _, ok := item.(*TextView); ok && action == MouseLeftDown {
   787  				continue
   788  			}
   789  
   790  			consumed, capture = item.MouseHandler()(action, event, setFocus)
   791  			if consumed {
   792  				return
   793  			}
   794  		}
   795  		for _, button := range f.buttons {
   796  			consumed, capture = button.MouseHandler()(action, event, setFocus)
   797  			if consumed {
   798  				return
   799  			}
   800  		}
   801  
   802  		// A mouse down anywhere else will return the focus to the last selected
   803  		// element.
   804  		if action == MouseLeftDown && f.InRect(event.Position()) {
   805  			f.Focus(setFocus)
   806  			consumed = true
   807  		}
   808  
   809  		return
   810  	})
   811  }
   812  
   813  // InputHandler returns the handler for this primitive.
   814  func (f *Form) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
   815  	return f.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
   816  		for _, item := range f.items {
   817  			if item != nil && item.HasFocus() {
   818  				if handler := item.InputHandler(); handler != nil {
   819  					handler(event, setFocus)
   820  					return
   821  				}
   822  			}
   823  		}
   824  
   825  		for _, button := range f.buttons {
   826  			if button.HasFocus() {
   827  				if handler := button.InputHandler(); handler != nil {
   828  					handler(event, setFocus)
   829  					return
   830  				}
   831  			}
   832  		}
   833  	})
   834  }
   835  

View as plain text