list.go 23 KB

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