...

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

Documentation: github.com/rivo/tview

     1  package tview
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/gdamore/tcell/v2"
     8  )
     9  
    10  // listItem represents one item in a List.
    11  type listItem struct {
    12  	MainText      string // The main text of the list item.
    13  	SecondaryText string // A secondary text to be shown underneath the main text.
    14  	Shortcut      rune   // The key to select the list item directly, 0 if there is no shortcut.
    15  	Selected      func() // The optional function which is called when the item is selected.
    16  }
    17  
    18  // List displays rows of items, each of which can be selected. List items can be
    19  // shown as a single line or as two lines. They can be selected by pressing
    20  // their assigned shortcut key, navigating to them and pressing Enter, or
    21  // clicking on them with the mouse. The following key binds are available:
    22  //
    23  //   - Down arrow / tab: Move down one item.
    24  //   - Up arrow / backtab: Move up one item.
    25  //   - Home: Move to the first item.
    26  //   - End: Move to the last item.
    27  //   - Page down: Move down one page.
    28  //   - Page up: Move up one page.
    29  //   - Enter / Space: Select the current item.
    30  //   - Right / left: Scroll horizontally. Only if the list is wider than the
    31  //     available space.
    32  //
    33  // See [List.SetChangedFunc] for a way to be notified when the user navigates
    34  // to a list item. See [List.SetSelectedFunc] for a way to be notified when a
    35  // list item was selected.
    36  //
    37  // See https://github.com/rivo/tview/wiki/List for an example.
    38  type List struct {
    39  	*Box
    40  
    41  	// The items of the list.
    42  	items []*listItem
    43  
    44  	// The index of the currently selected item.
    45  	currentItem int
    46  
    47  	// Whether or not to show the secondary item texts.
    48  	showSecondaryText bool
    49  
    50  	// The item main text style.
    51  	mainTextStyle tcell.Style
    52  
    53  	// The item secondary text style.
    54  	secondaryTextStyle tcell.Style
    55  
    56  	// The item shortcut text style.
    57  	shortcutStyle tcell.Style
    58  
    59  	// The style for selected items.
    60  	selectedStyle tcell.Style
    61  
    62  	// If true, the selection is only shown when the list has focus.
    63  	selectedFocusOnly bool
    64  
    65  	// If true, the entire row is highlighted when selected.
    66  	highlightFullLine bool
    67  
    68  	// Whether or not navigating the list will wrap around.
    69  	wrapAround bool
    70  
    71  	// The number of list items skipped at the top before the first item is
    72  	// drawn.
    73  	itemOffset int
    74  
    75  	// The number of cells skipped on the left side of an item text. Shortcuts
    76  	// are not affected.
    77  	horizontalOffset int
    78  
    79  	// Set to true if a currently visible item flows over the right border of
    80  	// the box. This is set by the Draw() function. It determines the behaviour
    81  	// of the right arrow key.
    82  	overflowing bool
    83  
    84  	// An optional function which is called when the user has navigated to a
    85  	// list item.
    86  	changed func(index int, mainText, secondaryText string, shortcut rune)
    87  
    88  	// An optional function which is called when a list item was selected. This
    89  	// function will be called even if the list item defines its own callback.
    90  	selected func(index int, mainText, secondaryText string, shortcut rune)
    91  
    92  	// An optional function which is called when the user presses the Escape key.
    93  	done func()
    94  }
    95  
    96  // NewList returns a new list.
    97  func NewList() *List {
    98  	return &List{
    99  		Box:                NewBox(),
   100  		showSecondaryText:  true,
   101  		wrapAround:         true,
   102  		mainTextStyle:      tcell.StyleDefault.Foreground(Styles.PrimaryTextColor),
   103  		secondaryTextStyle: tcell.StyleDefault.Foreground(Styles.TertiaryTextColor),
   104  		shortcutStyle:      tcell.StyleDefault.Foreground(Styles.SecondaryTextColor),
   105  		selectedStyle:      tcell.StyleDefault.Foreground(Styles.PrimitiveBackgroundColor).Background(Styles.PrimaryTextColor),
   106  	}
   107  }
   108  
   109  // SetCurrentItem sets the currently selected item by its index, starting at 0
   110  // for the first item. If a negative index is provided, items are referred to
   111  // from the back (-1 = last item, -2 = second-to-last item, and so on). Out of
   112  // range indices are clamped to the beginning/end.
   113  //
   114  // Calling this function triggers a "changed" event if the selection changes.
   115  func (l *List) SetCurrentItem(index int) *List {
   116  	if index < 0 {
   117  		index = len(l.items) + index
   118  	}
   119  	if index >= len(l.items) {
   120  		index = len(l.items) - 1
   121  	}
   122  	if index < 0 {
   123  		index = 0
   124  	}
   125  
   126  	if index != l.currentItem && l.changed != nil {
   127  		item := l.items[index]
   128  		l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
   129  	}
   130  
   131  	l.currentItem = index
   132  
   133  	l.adjustOffset()
   134  
   135  	return l
   136  }
   137  
   138  // GetCurrentItem returns the index of the currently selected list item,
   139  // starting at 0 for the first item.
   140  func (l *List) GetCurrentItem() int {
   141  	return l.currentItem
   142  }
   143  
   144  // SetOffset sets the number of items to be skipped (vertically) as well as the
   145  // number of cells skipped horizontally when the list is drawn. Note that one
   146  // item corresponds to two rows when there are secondary texts. Shortcuts are
   147  // always drawn.
   148  //
   149  // These values may change when the list is drawn to ensure the currently
   150  // selected item is visible and item texts move out of view. Users can also
   151  // modify these values by interacting with the list.
   152  func (l *List) SetOffset(items, horizontal int) *List {
   153  	l.itemOffset = items
   154  	l.horizontalOffset = horizontal
   155  	return l
   156  }
   157  
   158  // GetOffset returns the number of items skipped while drawing, as well as the
   159  // number of cells item text is moved to the left. See also SetOffset() for more
   160  // information on these values.
   161  func (l *List) GetOffset() (int, int) {
   162  	return l.itemOffset, l.horizontalOffset
   163  }
   164  
   165  // RemoveItem removes the item with the given index (starting at 0) from the
   166  // list. If a negative index is provided, items are referred to from the back
   167  // (-1 = last item, -2 = second-to-last item, and so on). Out of range indices
   168  // are clamped to the beginning/end, i.e. unless the list is empty, an item is
   169  // always removed.
   170  //
   171  // The currently selected item is shifted accordingly. If it is the one that is
   172  // removed, a "changed" event is fired, unless no items are left.
   173  func (l *List) RemoveItem(index int) *List {
   174  	if len(l.items) == 0 {
   175  		return l
   176  	}
   177  
   178  	// Adjust index.
   179  	if index < 0 {
   180  		index = len(l.items) + index
   181  	}
   182  	if index >= len(l.items) {
   183  		index = len(l.items) - 1
   184  	}
   185  	if index < 0 {
   186  		index = 0
   187  	}
   188  
   189  	// Remove item.
   190  	l.items = append(l.items[:index], l.items[index+1:]...)
   191  
   192  	// If there is nothing left, we're done.
   193  	if len(l.items) == 0 {
   194  		return l
   195  	}
   196  
   197  	// Shift current item.
   198  	previousCurrentItem := l.currentItem
   199  	if l.currentItem > index || l.currentItem == len(l.items) {
   200  		l.currentItem--
   201  	}
   202  
   203  	// Fire "changed" event for removed items.
   204  	if previousCurrentItem == index && l.changed != nil {
   205  		item := l.items[l.currentItem]
   206  		l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
   207  	}
   208  
   209  	return l
   210  }
   211  
   212  // SetMainTextColor sets the color of the items' main text.
   213  func (l *List) SetMainTextColor(color tcell.Color) *List {
   214  	l.mainTextStyle = l.mainTextStyle.Foreground(color)
   215  	return l
   216  }
   217  
   218  // SetMainTextStyle sets the style of the items' main text. Note that the
   219  // background color is ignored in order not to override the background color of
   220  // the list itself.
   221  func (l *List) SetMainTextStyle(style tcell.Style) *List {
   222  	l.mainTextStyle = style
   223  	return l
   224  }
   225  
   226  // SetSecondaryTextColor sets the color of the items' secondary text.
   227  func (l *List) SetSecondaryTextColor(color tcell.Color) *List {
   228  	l.secondaryTextStyle = l.secondaryTextStyle.Foreground(color)
   229  	return l
   230  }
   231  
   232  // SetSecondaryTextStyle sets the style of the items' secondary text. Note that
   233  // the background color is ignored in order not to override the background color
   234  // of the list itself.
   235  func (l *List) SetSecondaryTextStyle(style tcell.Style) *List {
   236  	l.secondaryTextStyle = style
   237  	return l
   238  }
   239  
   240  // SetShortcutColor sets the color of the items' shortcut.
   241  func (l *List) SetShortcutColor(color tcell.Color) *List {
   242  	l.shortcutStyle = l.shortcutStyle.Foreground(color)
   243  	return l
   244  }
   245  
   246  // SetShortcutStyle sets the style of the items' shortcut. Note that the
   247  // background color is ignored in order not to override the background color of
   248  // the list itself.
   249  func (l *List) SetShortcutStyle(style tcell.Style) *List {
   250  	l.shortcutStyle = style
   251  	return l
   252  }
   253  
   254  // SetSelectedTextColor sets the text color of selected items. Note that the
   255  // color of main text characters that are different from the main text color
   256  // (e.g. color tags) is maintained.
   257  func (l *List) SetSelectedTextColor(color tcell.Color) *List {
   258  	l.selectedStyle = l.selectedStyle.Foreground(color)
   259  	return l
   260  }
   261  
   262  // SetSelectedBackgroundColor sets the background color of selected items.
   263  func (l *List) SetSelectedBackgroundColor(color tcell.Color) *List {
   264  	l.selectedStyle = l.selectedStyle.Background(color)
   265  	return l
   266  }
   267  
   268  // SetSelectedStyle sets the style of the selected items. Note that the color of
   269  // main text characters that are different from the main text color (e.g. color
   270  // tags) is maintained.
   271  func (l *List) SetSelectedStyle(style tcell.Style) *List {
   272  	l.selectedStyle = style
   273  	return l
   274  }
   275  
   276  // SetSelectedFocusOnly sets a flag which determines when the currently selected
   277  // list item is highlighted. If set to true, selected items are only highlighted
   278  // when the list has focus. If set to false, they are always highlighted.
   279  func (l *List) SetSelectedFocusOnly(focusOnly bool) *List {
   280  	l.selectedFocusOnly = focusOnly
   281  	return l
   282  }
   283  
   284  // SetHighlightFullLine sets a flag which determines whether the colored
   285  // background of selected items spans the entire width of the view. If set to
   286  // true, the highlight spans the entire view. If set to false, only the text of
   287  // the selected item from beginning to end is highlighted.
   288  func (l *List) SetHighlightFullLine(highlight bool) *List {
   289  	l.highlightFullLine = highlight
   290  	return l
   291  }
   292  
   293  // ShowSecondaryText determines whether or not to show secondary item texts.
   294  func (l *List) ShowSecondaryText(show bool) *List {
   295  	l.showSecondaryText = show
   296  	return l
   297  }
   298  
   299  // SetWrapAround sets the flag that determines whether navigating the list will
   300  // wrap around. That is, navigating downwards on the last item will move the
   301  // selection to the first item (similarly in the other direction). If set to
   302  // false, the selection won't change when navigating downwards on the last item
   303  // or navigating upwards on the first item.
   304  func (l *List) SetWrapAround(wrapAround bool) *List {
   305  	l.wrapAround = wrapAround
   306  	return l
   307  }
   308  
   309  // SetChangedFunc sets the function which is called when the user navigates to
   310  // a list item. The function receives the item's index in the list of items
   311  // (starting with 0), its main text, secondary text, and its shortcut rune.
   312  //
   313  // This function is also called when the first item is added or when
   314  // SetCurrentItem() is called.
   315  func (l *List) SetChangedFunc(handler func(index int, mainText string, secondaryText string, shortcut rune)) *List {
   316  	l.changed = handler
   317  	return l
   318  }
   319  
   320  // SetSelectedFunc sets the function which is called when the user selects a
   321  // list item by pressing Enter on the current selection. The function receives
   322  // the item's index in the list of items (starting with 0), its main text,
   323  // secondary text, and its shortcut rune.
   324  func (l *List) SetSelectedFunc(handler func(int, string, string, rune)) *List {
   325  	l.selected = handler
   326  	return l
   327  }
   328  
   329  // SetDoneFunc sets a function which is called when the user presses the Escape
   330  // key.
   331  func (l *List) SetDoneFunc(handler func()) *List {
   332  	l.done = handler
   333  	return l
   334  }
   335  
   336  // AddItem calls InsertItem() with an index of -1.
   337  func (l *List) AddItem(mainText, secondaryText string, shortcut rune, selected func()) *List {
   338  	l.InsertItem(-1, mainText, secondaryText, shortcut, selected)
   339  	return l
   340  }
   341  
   342  // InsertItem adds a new item to the list at the specified index. An index of 0
   343  // will insert the item at the beginning, an index of 1 before the second item,
   344  // and so on. An index of GetItemCount() or higher will insert the item at the
   345  // end of the list. Negative indices are also allowed: An index of -1 will
   346  // insert the item at the end of the list, an index of -2 before the last item,
   347  // and so on. An index of -GetItemCount()-1 or lower will insert the item at the
   348  // beginning.
   349  //
   350  // An item has a main text which will be highlighted when selected. It also has
   351  // a secondary text which is shown underneath the main text (if it is set to
   352  // visible) but which may remain empty.
   353  //
   354  // The shortcut is a key binding. If the specified rune is entered, the item
   355  // is selected immediately. Set to 0 for no binding.
   356  //
   357  // The "selected" callback will be invoked when the user selects the item. You
   358  // may provide nil if no such callback is needed or if all events are handled
   359  // through the selected callback set with SetSelectedFunc().
   360  //
   361  // The currently selected item will shift its position accordingly. If the list
   362  // was previously empty, a "changed" event is fired because the new item becomes
   363  // selected.
   364  func (l *List) InsertItem(index int, mainText, secondaryText string, shortcut rune, selected func()) *List {
   365  	item := &listItem{
   366  		MainText:      mainText,
   367  		SecondaryText: secondaryText,
   368  		Shortcut:      shortcut,
   369  		Selected:      selected,
   370  	}
   371  
   372  	// Shift index to range.
   373  	if index < 0 {
   374  		index = len(l.items) + index + 1
   375  	}
   376  	if index < 0 {
   377  		index = 0
   378  	} else if index > len(l.items) {
   379  		index = len(l.items)
   380  	}
   381  
   382  	// Shift current item.
   383  	if l.currentItem < len(l.items) && l.currentItem >= index {
   384  		l.currentItem++
   385  	}
   386  
   387  	// Insert item (make space for the new item, then shift and insert).
   388  	l.items = append(l.items, nil)
   389  	if index < len(l.items)-1 { // -1 because l.items has already grown by one item.
   390  		copy(l.items[index+1:], l.items[index:])
   391  	}
   392  	l.items[index] = item
   393  
   394  	// Fire a "change" event for the first item in the list.
   395  	if len(l.items) == 1 && l.changed != nil {
   396  		item := l.items[0]
   397  		l.changed(0, item.MainText, item.SecondaryText, item.Shortcut)
   398  	}
   399  
   400  	return l
   401  }
   402  
   403  // GetItemCount returns the number of items in the list.
   404  func (l *List) GetItemCount() int {
   405  	return len(l.items)
   406  }
   407  
   408  // GetItemText returns an item's texts (main and secondary). Panics if the index
   409  // is out of range.
   410  func (l *List) GetItemText(index int) (main, secondary string) {
   411  	return l.items[index].MainText, l.items[index].SecondaryText
   412  }
   413  
   414  // SetItemText sets an item's main and secondary text. Panics if the index is
   415  // out of range.
   416  func (l *List) SetItemText(index int, main, secondary string) *List {
   417  	item := l.items[index]
   418  	item.MainText = main
   419  	item.SecondaryText = secondary
   420  	return l
   421  }
   422  
   423  // FindItems searches the main and secondary texts for the given strings and
   424  // returns a list of item indices in which those strings are found. One of the
   425  // two search strings may be empty, it will then be ignored. Indices are always
   426  // returned in ascending order.
   427  //
   428  // If mustContainBoth is set to true, mainSearch must be contained in the main
   429  // text AND secondarySearch must be contained in the secondary text. If it is
   430  // false, only one of the two search strings must be contained.
   431  //
   432  // Set ignoreCase to true for case-insensitive search.
   433  func (l *List) FindItems(mainSearch, secondarySearch string, mustContainBoth, ignoreCase bool) (indices []int) {
   434  	if mainSearch == "" && secondarySearch == "" {
   435  		return
   436  	}
   437  
   438  	if ignoreCase {
   439  		mainSearch = strings.ToLower(mainSearch)
   440  		secondarySearch = strings.ToLower(secondarySearch)
   441  	}
   442  
   443  	for index, item := range l.items {
   444  		mainText := item.MainText
   445  		secondaryText := item.SecondaryText
   446  		if ignoreCase {
   447  			mainText = strings.ToLower(mainText)
   448  			secondaryText = strings.ToLower(secondaryText)
   449  		}
   450  
   451  		// strings.Contains() always returns true for a "" search.
   452  		mainContained := strings.Contains(mainText, mainSearch)
   453  		secondaryContained := strings.Contains(secondaryText, secondarySearch)
   454  		if mustContainBoth && mainContained && secondaryContained ||
   455  			!mustContainBoth && (mainText != "" && mainContained || secondaryText != "" && secondaryContained) {
   456  			indices = append(indices, index)
   457  		}
   458  	}
   459  
   460  	return
   461  }
   462  
   463  // Clear removes all items from the list.
   464  func (l *List) Clear() *List {
   465  	l.items = nil
   466  	l.currentItem = 0
   467  	return l
   468  }
   469  
   470  // Draw draws this primitive onto the screen.
   471  func (l *List) Draw(screen tcell.Screen) {
   472  	l.Box.DrawForSubclass(screen, l)
   473  
   474  	// Determine the dimensions.
   475  	x, y, width, height := l.GetInnerRect()
   476  	bottomLimit := y + height
   477  	_, totalHeight := screen.Size()
   478  	if bottomLimit > totalHeight {
   479  		bottomLimit = totalHeight
   480  	}
   481  
   482  	// Do we show any shortcuts?
   483  	var showShortcuts bool
   484  	for _, item := range l.items {
   485  		if item.Shortcut != 0 {
   486  			showShortcuts = true
   487  			x += 4
   488  			width -= 4
   489  			break
   490  		}
   491  	}
   492  
   493  	if l.horizontalOffset < 0 {
   494  		l.horizontalOffset = 0
   495  	}
   496  
   497  	// Draw the list items.
   498  	var (
   499  		maxWidth    int  // The maximum printed item width.
   500  		overflowing bool // Whether a text's end exceeds the right border.
   501  	)
   502  	for index, item := range l.items {
   503  		if index < l.itemOffset {
   504  			continue
   505  		}
   506  
   507  		if y >= bottomLimit {
   508  			break
   509  		}
   510  
   511  		// Shortcuts.
   512  		if showShortcuts && item.Shortcut != 0 {
   513  			printWithStyle(screen, fmt.Sprintf("(%s)", string(item.Shortcut)), x-5, y, 0, 4, AlignRight, l.shortcutStyle, true)
   514  		}
   515  
   516  		// Main text.
   517  		_, printedWidth, _, end := printWithStyle(screen, item.MainText, x, y, l.horizontalOffset, width, AlignLeft, l.mainTextStyle, true)
   518  		if printedWidth > maxWidth {
   519  			maxWidth = printedWidth
   520  		}
   521  		if end < len(item.MainText) {
   522  			overflowing = true
   523  		}
   524  
   525  		// Background color of selected text.
   526  		if index == l.currentItem && (!l.selectedFocusOnly || l.HasFocus()) {
   527  			textWidth := width
   528  			if !l.highlightFullLine {
   529  				if w := TaggedStringWidth(item.MainText); w < textWidth {
   530  					textWidth = w
   531  				}
   532  			}
   533  
   534  			mainTextColor, _, _ := l.mainTextStyle.Decompose()
   535  			for bx := 0; bx < textWidth; bx++ {
   536  				m, c, style, _ := screen.GetContent(x+bx, y)
   537  				fg, _, _ := style.Decompose()
   538  				style = l.selectedStyle
   539  				if fg != mainTextColor {
   540  					style = style.Foreground(fg)
   541  				}
   542  				screen.SetContent(x+bx, y, m, c, style)
   543  			}
   544  		}
   545  
   546  		y++
   547  
   548  		if y >= bottomLimit {
   549  			break
   550  		}
   551  
   552  		// Secondary text.
   553  		if l.showSecondaryText {
   554  			_, printedWidth, _, end := printWithStyle(screen, item.SecondaryText, x, y, l.horizontalOffset, width, AlignLeft, l.secondaryTextStyle, true)
   555  			if printedWidth > maxWidth {
   556  				maxWidth = printedWidth
   557  			}
   558  			if end < len(item.SecondaryText) {
   559  				overflowing = true
   560  			}
   561  			y++
   562  		}
   563  	}
   564  
   565  	// We don't want the item text to get out of view. If the horizontal offset
   566  	// is too high, we reset it and redraw. (That should be about as efficient
   567  	// as calculating everything up front.)
   568  	if l.horizontalOffset > 0 && maxWidth < width {
   569  		l.horizontalOffset -= width - maxWidth
   570  		l.Draw(screen)
   571  	}
   572  	l.overflowing = overflowing
   573  }
   574  
   575  // adjustOffset adjusts the vertical offset to keep the current selection in
   576  // view.
   577  func (l *List) adjustOffset() {
   578  	_, _, _, height := l.GetInnerRect()
   579  	if height == 0 {
   580  		return
   581  	}
   582  	if l.currentItem < l.itemOffset {
   583  		l.itemOffset = l.currentItem
   584  	} else if l.showSecondaryText {
   585  		if 2*(l.currentItem-l.itemOffset) >= height-1 {
   586  			l.itemOffset = (2*l.currentItem + 3 - height) / 2
   587  		}
   588  	} else {
   589  		if l.currentItem-l.itemOffset >= height {
   590  			l.itemOffset = l.currentItem + 1 - height
   591  		}
   592  	}
   593  }
   594  
   595  // InputHandler returns the handler for this primitive.
   596  func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
   597  	return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
   598  		if event.Key() == tcell.KeyEscape {
   599  			if l.done != nil {
   600  				l.done()
   601  			}
   602  			return
   603  		} else if len(l.items) == 0 {
   604  			return
   605  		}
   606  
   607  		previousItem := l.currentItem
   608  
   609  		switch key := event.Key(); key {
   610  		case tcell.KeyTab, tcell.KeyDown:
   611  			l.currentItem++
   612  		case tcell.KeyBacktab, tcell.KeyUp:
   613  			l.currentItem--
   614  		case tcell.KeyRight:
   615  			if l.overflowing {
   616  				l.horizontalOffset += 2 // We shift by 2 to account for two-cell characters.
   617  			} else {
   618  				l.currentItem++
   619  			}
   620  		case tcell.KeyLeft:
   621  			if l.horizontalOffset > 0 {
   622  				l.horizontalOffset -= 2
   623  			} else {
   624  				l.currentItem--
   625  			}
   626  		case tcell.KeyHome:
   627  			l.currentItem = 0
   628  		case tcell.KeyEnd:
   629  			l.currentItem = len(l.items) - 1
   630  		case tcell.KeyPgDn:
   631  			_, _, _, height := l.GetInnerRect()
   632  			l.currentItem += height
   633  			if l.currentItem >= len(l.items) {
   634  				l.currentItem = len(l.items) - 1
   635  			}
   636  		case tcell.KeyPgUp:
   637  			_, _, _, height := l.GetInnerRect()
   638  			l.currentItem -= height
   639  			if l.currentItem < 0 {
   640  				l.currentItem = 0
   641  			}
   642  		case tcell.KeyEnter:
   643  			if l.currentItem >= 0 && l.currentItem < len(l.items) {
   644  				item := l.items[l.currentItem]
   645  				if item.Selected != nil {
   646  					item.Selected()
   647  				}
   648  				if l.selected != nil {
   649  					l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
   650  				}
   651  			}
   652  		case tcell.KeyRune:
   653  			ch := event.Rune()
   654  			if ch != ' ' {
   655  				// It's not a space bar. Is it a shortcut?
   656  				var found bool
   657  				for index, item := range l.items {
   658  					if item.Shortcut == ch {
   659  						// We have a shortcut.
   660  						found = true
   661  						l.currentItem = index
   662  						break
   663  					}
   664  				}
   665  				if !found {
   666  					break
   667  				}
   668  			}
   669  			item := l.items[l.currentItem]
   670  			if item.Selected != nil {
   671  				item.Selected()
   672  			}
   673  			if l.selected != nil {
   674  				l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
   675  			}
   676  		}
   677  
   678  		if l.currentItem < 0 {
   679  			if l.wrapAround {
   680  				l.currentItem = len(l.items) - 1
   681  			} else {
   682  				l.currentItem = 0
   683  			}
   684  		} else if l.currentItem >= len(l.items) {
   685  			if l.wrapAround {
   686  				l.currentItem = 0
   687  			} else {
   688  				l.currentItem = len(l.items) - 1
   689  			}
   690  		}
   691  
   692  		if l.currentItem != previousItem && l.currentItem < len(l.items) {
   693  			if l.changed != nil {
   694  				item := l.items[l.currentItem]
   695  				l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
   696  			}
   697  			l.adjustOffset()
   698  		}
   699  	})
   700  }
   701  
   702  // indexAtPoint returns the index of the list item found at the given position
   703  // or a negative value if there is no such list item.
   704  func (l *List) indexAtPoint(x, y int) int {
   705  	rectX, rectY, width, height := l.GetInnerRect()
   706  	if rectX < 0 || rectX >= rectX+width || y < rectY || y >= rectY+height {
   707  		return -1
   708  	}
   709  
   710  	index := y - rectY
   711  	if l.showSecondaryText {
   712  		index /= 2
   713  	}
   714  	index += l.itemOffset
   715  
   716  	if index >= len(l.items) {
   717  		return -1
   718  	}
   719  	return index
   720  }
   721  
   722  // MouseHandler returns the mouse handler for this primitive.
   723  func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   724  	return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   725  		if !l.InRect(event.Position()) {
   726  			return false, nil
   727  		}
   728  
   729  		// Process mouse event.
   730  		switch action {
   731  		case MouseLeftClick:
   732  			setFocus(l)
   733  			index := l.indexAtPoint(event.Position())
   734  			if index != -1 {
   735  				item := l.items[index]
   736  				if item.Selected != nil {
   737  					item.Selected()
   738  				}
   739  				if l.selected != nil {
   740  					l.selected(index, item.MainText, item.SecondaryText, item.Shortcut)
   741  				}
   742  				if index != l.currentItem {
   743  					if l.changed != nil {
   744  						l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
   745  					}
   746  					l.adjustOffset()
   747  				}
   748  				l.currentItem = index
   749  			}
   750  			consumed = true
   751  		case MouseScrollUp:
   752  			if l.itemOffset > 0 {
   753  				l.itemOffset--
   754  			}
   755  			consumed = true
   756  		case MouseScrollDown:
   757  			lines := len(l.items) - l.itemOffset
   758  			if l.showSecondaryText {
   759  				lines *= 2
   760  			}
   761  			if _, _, _, height := l.GetInnerRect(); lines > height {
   762  				l.itemOffset++
   763  			}
   764  			consumed = true
   765  		}
   766  
   767  		return
   768  	})
   769  }
   770  

View as plain text