nt

A sensible note-taking program
git clone git://git.laack.co/nt.git
Log | Files | Refs | README

list.go (24123B)


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