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