dropdown.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. package tview
  2. import (
  3. "strings"
  4. "github.com/gdamore/tcell/v2"
  5. "github.com/rivo/uniseg"
  6. )
  7. // dropDownOption is one option that can be selected in a drop-down primitive.
  8. type dropDownOption struct {
  9. Text string // The text to be displayed in the drop-down.
  10. Selected func() // The (optional) callback for when this option was selected.
  11. }
  12. // DropDown implements a selection widget whose options become visible in a
  13. // drop-down list when activated.
  14. //
  15. // See https://github.com/rivo/tview/wiki/DropDown for an example.
  16. type DropDown struct {
  17. *Box
  18. // Whether or not this drop-down is disabled/read-only.
  19. disabled bool
  20. // The options from which the user can choose.
  21. options []*dropDownOption
  22. // Strings to be placed before and after each drop-down option.
  23. optionPrefix, optionSuffix string
  24. // The index of the currently selected option. Negative if no option is
  25. // currently selected.
  26. currentOption int
  27. // Strings to be placed before and after the current option.
  28. currentOptionPrefix, currentOptionSuffix string
  29. // The text to be displayed when no option has yet been selected.
  30. noSelection string
  31. // Set to true if the options are visible and selectable.
  32. open bool
  33. // The runes typed so far to directly access one of the list items.
  34. prefix string
  35. // The list element for the options.
  36. list *List
  37. // The text to be displayed before the input area.
  38. label string
  39. // The label color.
  40. labelColor tcell.Color
  41. // The background color of the input area.
  42. fieldBackgroundColor tcell.Color
  43. // The text color of the input area.
  44. fieldTextColor tcell.Color
  45. // The color for prefixes.
  46. prefixTextColor tcell.Color
  47. // The screen width of the label area. A value of 0 means use the width of
  48. // the label text.
  49. labelWidth int
  50. // The screen width of the input area. A value of 0 means extend as much as
  51. // possible.
  52. fieldWidth int
  53. // An optional function which is called when the user indicated that they
  54. // are done selecting options. The key which was pressed is provided (tab,
  55. // shift-tab, or escape).
  56. done func(tcell.Key)
  57. // A callback function set by the Form class and called when the user leaves
  58. // this form item.
  59. finished func(tcell.Key)
  60. // A callback function which is called when the user changes the drop-down's
  61. // selection.
  62. selected func(text string, index int)
  63. dragging bool // Set to true when mouse dragging is in progress.
  64. }
  65. // NewDropDown returns a new drop-down.
  66. func NewDropDown() *DropDown {
  67. list := NewList()
  68. list.ShowSecondaryText(false).
  69. SetMainTextColor(Styles.PrimitiveBackgroundColor).
  70. SetSelectedTextColor(Styles.PrimitiveBackgroundColor).
  71. SetSelectedBackgroundColor(Styles.PrimaryTextColor).
  72. SetHighlightFullLine(true).
  73. SetBackgroundColor(Styles.MoreContrastBackgroundColor)
  74. d := &DropDown{
  75. Box: NewBox(),
  76. currentOption: -1,
  77. list: list,
  78. labelColor: Styles.SecondaryTextColor,
  79. fieldBackgroundColor: Styles.ContrastBackgroundColor,
  80. fieldTextColor: Styles.PrimaryTextColor,
  81. prefixTextColor: Styles.ContrastSecondaryTextColor,
  82. }
  83. return d
  84. }
  85. // SetCurrentOption sets the index of the currently selected option. This may
  86. // be a negative value to indicate that no option is currently selected. Calling
  87. // this function will also trigger the "selected" callback (if there is one).
  88. func (d *DropDown) SetCurrentOption(index int) *DropDown {
  89. if index >= 0 && index < len(d.options) {
  90. d.currentOption = index
  91. d.list.SetCurrentItem(index)
  92. if d.selected != nil {
  93. d.selected(d.options[index].Text, index)
  94. }
  95. if d.options[index].Selected != nil {
  96. d.options[index].Selected()
  97. }
  98. } else {
  99. d.currentOption = -1
  100. d.list.SetCurrentItem(0) // Set to 0 because -1 means "last item".
  101. if d.selected != nil {
  102. d.selected("", -1)
  103. }
  104. }
  105. return d
  106. }
  107. // GetCurrentOption returns the index of the currently selected option as well
  108. // as its text. If no option was selected, -1 and an empty string is returned.
  109. func (d *DropDown) GetCurrentOption() (int, string) {
  110. var text string
  111. if d.currentOption >= 0 && d.currentOption < len(d.options) {
  112. text = d.options[d.currentOption].Text
  113. }
  114. return d.currentOption, text
  115. }
  116. // SetTextOptions sets the text to be placed before and after each drop-down
  117. // option (prefix/suffix), the text placed before and after the currently
  118. // selected option (currentPrefix/currentSuffix) as well as the text to be
  119. // displayed when no option is currently selected. Per default, all of these
  120. // strings are empty.
  121. func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, noSelection string) *DropDown {
  122. d.currentOptionPrefix = currentPrefix
  123. d.currentOptionSuffix = currentSuffix
  124. d.noSelection = noSelection
  125. d.optionPrefix = prefix
  126. d.optionSuffix = suffix
  127. for index := 0; index < d.list.GetItemCount(); index++ {
  128. d.list.SetItemText(index, prefix+d.options[index].Text+suffix, "")
  129. }
  130. return d
  131. }
  132. // SetLabel sets the text to be displayed before the input area.
  133. func (d *DropDown) SetLabel(label string) *DropDown {
  134. d.label = label
  135. return d
  136. }
  137. // GetLabel returns the text to be displayed before the input area.
  138. func (d *DropDown) GetLabel() string {
  139. return d.label
  140. }
  141. // SetLabelWidth sets the screen width of the label. A value of 0 will cause the
  142. // primitive to use the width of the label string.
  143. func (d *DropDown) SetLabelWidth(width int) *DropDown {
  144. d.labelWidth = width
  145. return d
  146. }
  147. // SetLabelColor sets the color of the label.
  148. func (d *DropDown) SetLabelColor(color tcell.Color) *DropDown {
  149. d.labelColor = color
  150. return d
  151. }
  152. // SetFieldBackgroundColor sets the background color of the options area.
  153. func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) *DropDown {
  154. d.fieldBackgroundColor = color
  155. return d
  156. }
  157. // SetFieldTextColor sets the text color of the options area.
  158. func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown {
  159. d.fieldTextColor = color
  160. return d
  161. }
  162. // SetPrefixTextColor sets the color of the prefix string. The prefix string is
  163. // shown when the user starts typing text, which directly selects the first
  164. // option that starts with the typed string.
  165. func (d *DropDown) SetPrefixTextColor(color tcell.Color) *DropDown {
  166. d.prefixTextColor = color
  167. return d
  168. }
  169. // SetListStyles sets the styles of the items in the drop-down list (unselected
  170. // as well as selected items). Style attributes are currently ignored but may be
  171. // used in the future.
  172. func (d *DropDown) SetListStyles(unselected, selected tcell.Style) *DropDown {
  173. fg, bg, _ := unselected.Decompose()
  174. d.list.SetMainTextColor(fg).SetBackgroundColor(bg)
  175. fg, bg, _ = selected.Decompose()
  176. d.list.SetSelectedTextColor(fg).SetSelectedBackgroundColor(bg)
  177. return d
  178. }
  179. // SetFormAttributes sets attributes shared by all form items.
  180. func (d *DropDown) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
  181. d.labelWidth = labelWidth
  182. d.labelColor = labelColor
  183. d.backgroundColor = bgColor
  184. d.fieldTextColor = fieldTextColor
  185. d.fieldBackgroundColor = fieldBgColor
  186. return d
  187. }
  188. // SetFieldWidth sets the screen width of the options area. A value of 0 means
  189. // extend to as long as the longest option text.
  190. func (d *DropDown) SetFieldWidth(width int) *DropDown {
  191. d.fieldWidth = width
  192. return d
  193. }
  194. // GetFieldWidth returns this primitive's field screen width.
  195. func (d *DropDown) GetFieldWidth() int {
  196. if d.fieldWidth > 0 {
  197. return d.fieldWidth
  198. }
  199. fieldWidth := 0
  200. for _, option := range d.options {
  201. width := TaggedStringWidth(option.Text)
  202. if width > fieldWidth {
  203. fieldWidth = width
  204. }
  205. }
  206. return fieldWidth
  207. }
  208. // GetFieldHeight returns this primitive's field height.
  209. func (d *DropDown) GetFieldHeight() int {
  210. return 1
  211. }
  212. // SetDisabled sets whether or not the item is disabled / read-only.
  213. func (d *DropDown) SetDisabled(disabled bool) FormItem {
  214. d.disabled = disabled
  215. if d.finished != nil {
  216. d.finished(-1)
  217. }
  218. return d
  219. }
  220. // AddOption adds a new selectable option to this drop-down. The "selected"
  221. // callback is called when this option was selected. It may be nil.
  222. func (d *DropDown) AddOption(text string, selected func()) *DropDown {
  223. d.options = append(d.options, &dropDownOption{Text: text, Selected: selected})
  224. d.list.AddItem(d.optionPrefix+text+d.optionSuffix, "", 0, nil)
  225. return d
  226. }
  227. // SetOptions replaces all current options with the ones provided and installs
  228. // one callback function which is called when one of the options is selected.
  229. // It will be called with the option's text and its index into the options
  230. // slice. The "selected" parameter may be nil.
  231. func (d *DropDown) SetOptions(texts []string, selected func(text string, index int)) *DropDown {
  232. d.list.Clear()
  233. d.options = nil
  234. for index, text := range texts {
  235. func(t string, i int) {
  236. d.AddOption(text, nil)
  237. }(text, index)
  238. }
  239. d.selected = selected
  240. return d
  241. }
  242. // GetOptionCount returns the number of options in the drop-down.
  243. func (d *DropDown) GetOptionCount() int {
  244. return len(d.options)
  245. }
  246. // RemoveOption removes the specified option from the drop-down. Panics if the
  247. // index is out of range.
  248. func (d *DropDown) RemoveOption(index int) *DropDown {
  249. d.options = append(d.options[:index], d.options[index+1:]...)
  250. d.list.RemoveItem(index)
  251. return d
  252. }
  253. // SetSelectedFunc sets a handler which is called when the user changes the
  254. // drop-down's option. This handler will be called in addition and prior to
  255. // an option's optional individual handler. The handler is provided with the
  256. // selected option's text and index. If "no option" was selected, these values
  257. // are an empty string and -1.
  258. func (d *DropDown) SetSelectedFunc(handler func(text string, index int)) *DropDown {
  259. d.selected = handler
  260. return d
  261. }
  262. // SetDoneFunc sets a handler which is called when the user is done selecting
  263. // options. The callback function is provided with the key that was pressed,
  264. // which is one of the following:
  265. //
  266. // - KeyEscape: Abort selection.
  267. // - KeyTab: Move to the next field.
  268. // - KeyBacktab: Move to the previous field.
  269. func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) *DropDown {
  270. d.done = handler
  271. return d
  272. }
  273. // SetFinishedFunc sets a callback invoked when the user leaves this form item.
  274. func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
  275. d.finished = handler
  276. return d
  277. }
  278. // Draw draws this primitive onto the screen.
  279. func (d *DropDown) Draw(screen tcell.Screen) {
  280. d.Box.DrawForSubclass(screen, d)
  281. // Prepare.
  282. x, y, width, height := d.GetInnerRect()
  283. rightLimit := x + width
  284. if height < 1 || rightLimit <= x {
  285. return
  286. }
  287. // Draw label.
  288. if d.labelWidth > 0 {
  289. labelWidth := d.labelWidth
  290. if labelWidth > rightLimit-x {
  291. labelWidth = rightLimit - x
  292. }
  293. Print(screen, d.label, x, y, labelWidth, AlignLeft, d.labelColor)
  294. x += labelWidth
  295. } else {
  296. _, drawnWidth := Print(screen, d.label, x, y, rightLimit-x, AlignLeft, d.labelColor)
  297. x += drawnWidth
  298. }
  299. // What's the longest option text?
  300. maxWidth := 0
  301. optionWrapWidth := TaggedStringWidth(d.optionPrefix + d.optionSuffix)
  302. for _, option := range d.options {
  303. strWidth := TaggedStringWidth(option.Text) + optionWrapWidth
  304. if strWidth > maxWidth {
  305. maxWidth = strWidth
  306. }
  307. }
  308. // Draw selection area.
  309. fieldWidth := d.fieldWidth
  310. if fieldWidth == 0 {
  311. fieldWidth = maxWidth
  312. if d.currentOption < 0 {
  313. noSelectionWidth := TaggedStringWidth(d.noSelection)
  314. if noSelectionWidth > fieldWidth {
  315. fieldWidth = noSelectionWidth
  316. }
  317. } else if d.currentOption < len(d.options) {
  318. currentOptionWidth := TaggedStringWidth(d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix)
  319. if currentOptionWidth > fieldWidth {
  320. fieldWidth = currentOptionWidth
  321. }
  322. }
  323. }
  324. if rightLimit-x < fieldWidth {
  325. fieldWidth = rightLimit - x
  326. }
  327. fieldStyle := tcell.StyleDefault.Background(d.fieldBackgroundColor)
  328. if d.HasFocus() && !d.open {
  329. fieldStyle = fieldStyle.Background(d.fieldTextColor)
  330. }
  331. if d.disabled {
  332. fieldStyle = fieldStyle.Background(d.backgroundColor)
  333. }
  334. for index := 0; index < fieldWidth; index++ {
  335. screen.SetContent(x+index, y, ' ', nil, fieldStyle)
  336. }
  337. // Draw selected text.
  338. if d.open && len(d.prefix) > 0 {
  339. // Show the prefix.
  340. currentOptionPrefixWidth := TaggedStringWidth(d.currentOptionPrefix)
  341. prefixWidth := uniseg.StringWidth(d.prefix)
  342. listItemText := d.options[d.list.GetCurrentItem()].Text
  343. Print(screen, d.currentOptionPrefix, x, y, fieldWidth, AlignLeft, d.fieldTextColor)
  344. Print(screen, d.prefix, x+currentOptionPrefixWidth, y, fieldWidth-currentOptionPrefixWidth, AlignLeft, d.prefixTextColor)
  345. if len(d.prefix) < len(listItemText) {
  346. Print(screen, listItemText[len(d.prefix):]+d.currentOptionSuffix, x+prefixWidth+currentOptionPrefixWidth, y, fieldWidth-prefixWidth-currentOptionPrefixWidth, AlignLeft, d.fieldTextColor)
  347. }
  348. } else {
  349. color := d.fieldTextColor
  350. text := d.noSelection
  351. if d.currentOption >= 0 && d.currentOption < len(d.options) {
  352. text = d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix
  353. }
  354. // Just show the current selection.
  355. if d.HasFocus() && !d.open && !d.disabled {
  356. color = d.fieldBackgroundColor
  357. }
  358. Print(screen, text, x, y, fieldWidth, AlignLeft, color)
  359. }
  360. // Draw options list.
  361. if d.HasFocus() && d.open {
  362. lx := x
  363. ly := y + 1
  364. lwidth := maxWidth
  365. lheight := len(d.options)
  366. swidth, sheight := screen.Size()
  367. // We prefer to align the left sides of the list and the main widget, but
  368. // if there is no space to the right, then shift the list to the left.
  369. if lx+lwidth >= swidth {
  370. lx = swidth - lwidth
  371. if lx < 0 {
  372. lx = 0
  373. }
  374. }
  375. // We prefer to drop down but if there is no space, maybe drop up?
  376. if ly+lheight >= sheight && ly-2 > lheight-ly {
  377. ly = y - lheight
  378. if ly < 0 {
  379. ly = 0
  380. }
  381. }
  382. if ly+lheight >= sheight {
  383. lheight = sheight - ly
  384. }
  385. d.list.SetRect(lx, ly, lwidth, lheight)
  386. d.list.Draw(screen)
  387. }
  388. }
  389. // InputHandler returns the handler for this primitive.
  390. func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
  391. return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
  392. if d.disabled {
  393. return
  394. }
  395. // If the list has focus, let it process its own key events.
  396. if d.list.HasFocus() {
  397. if handler := d.list.InputHandler(); handler != nil {
  398. handler(event, setFocus)
  399. }
  400. return
  401. }
  402. // Process key event.
  403. switch key := event.Key(); key {
  404. case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
  405. d.prefix = ""
  406. // If the first key was a letter already, it becomes part of the prefix.
  407. if r := event.Rune(); key == tcell.KeyRune && r != ' ' {
  408. d.prefix += string(r)
  409. d.evalPrefix()
  410. }
  411. d.openList(setFocus)
  412. case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
  413. if d.done != nil {
  414. d.done(key)
  415. }
  416. if d.finished != nil {
  417. d.finished(key)
  418. }
  419. }
  420. })
  421. }
  422. // evalPrefix selects an item in the drop-down list based on the current prefix.
  423. func (d *DropDown) evalPrefix() {
  424. if len(d.prefix) > 0 {
  425. for index, option := range d.options {
  426. if strings.HasPrefix(strings.ToLower(option.Text), d.prefix) {
  427. d.list.SetCurrentItem(index)
  428. return
  429. }
  430. }
  431. // Prefix does not match any item. Remove last rune.
  432. r := []rune(d.prefix)
  433. d.prefix = string(r[:len(r)-1])
  434. }
  435. }
  436. // openList hands control over to the embedded List primitive.
  437. func (d *DropDown) openList(setFocus func(Primitive)) {
  438. d.open = true
  439. optionBefore := d.currentOption
  440. d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
  441. if d.dragging {
  442. return // If we're dragging the mouse, we don't want to trigger any events.
  443. }
  444. // An option was selected. Close the list again.
  445. d.currentOption = index
  446. d.closeList(setFocus)
  447. // Trigger "selected" event.
  448. if d.selected != nil {
  449. d.selected(d.options[d.currentOption].Text, d.currentOption)
  450. }
  451. if d.options[d.currentOption].Selected != nil {
  452. d.options[d.currentOption].Selected()
  453. }
  454. }).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
  455. if event.Key() == tcell.KeyRune {
  456. d.prefix += string(event.Rune())
  457. d.evalPrefix()
  458. } else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
  459. if len(d.prefix) > 0 {
  460. r := []rune(d.prefix)
  461. d.prefix = string(r[:len(r)-1])
  462. }
  463. d.evalPrefix()
  464. } else if event.Key() == tcell.KeyEscape {
  465. d.currentOption = optionBefore
  466. d.closeList(setFocus)
  467. } else {
  468. d.prefix = ""
  469. }
  470. return event
  471. })
  472. setFocus(d.list)
  473. }
  474. // closeList closes the embedded List element by hiding it and removing focus
  475. // from it.
  476. func (d *DropDown) closeList(setFocus func(Primitive)) {
  477. d.open = false
  478. if d.list.HasFocus() {
  479. setFocus(d)
  480. }
  481. }
  482. // IsOpen returns true if the drop-down list is currently open.
  483. func (d *DropDown) IsOpen() bool {
  484. return d.open
  485. }
  486. // Focus is called by the application when the primitive receives focus.
  487. func (d *DropDown) Focus(delegate func(p Primitive)) {
  488. // If we're part of a form and this item is disabled, there's nothing the
  489. // user can do here so we're finished.
  490. if d.finished != nil && d.disabled {
  491. d.finished(-1)
  492. return
  493. }
  494. if d.open {
  495. delegate(d.list)
  496. } else {
  497. d.Box.Focus(delegate)
  498. }
  499. }
  500. // HasFocus returns whether or not this primitive has focus.
  501. func (d *DropDown) HasFocus() bool {
  502. if d.open {
  503. return d.list.HasFocus()
  504. }
  505. return d.Box.HasFocus()
  506. }
  507. // MouseHandler returns the mouse handler for this primitive.
  508. func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
  509. return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
  510. if d.disabled {
  511. return false, nil
  512. }
  513. // Was the mouse event in the drop-down box itself (or on its label)?
  514. x, y := event.Position()
  515. rectX, rectY, rectWidth, _ := d.GetInnerRect()
  516. inRect := y == rectY && x >= rectX && x < rectX+rectWidth
  517. if !d.open && !inRect {
  518. return d.InRect(x, y), nil // No, and it's not expanded either. Ignore.
  519. }
  520. // As long as the drop-down is open, we capture all mouse events.
  521. if d.open {
  522. capture = d
  523. }
  524. switch action {
  525. case MouseLeftDown:
  526. consumed = d.open || inRect
  527. capture = d
  528. if !d.open {
  529. d.openList(setFocus)
  530. d.dragging = true
  531. } else if consumed, _ := d.list.MouseHandler()(MouseLeftClick, event, setFocus); !consumed {
  532. d.closeList(setFocus) // Close drop-down if clicked outside of it.
  533. }
  534. case MouseMove:
  535. if d.dragging {
  536. // We pretend it's a left click so we can see the selection during
  537. // dragging. Because we don't act upon it, it's not a problem.
  538. d.list.MouseHandler()(MouseLeftClick, event, setFocus)
  539. consumed = true
  540. }
  541. case MouseLeftUp:
  542. if d.dragging {
  543. d.dragging = false
  544. d.list.MouseHandler()(MouseLeftClick, event, setFocus)
  545. consumed = true
  546. }
  547. }
  548. return
  549. })
  550. }