Files
arr-tui/internal/ui/form.go
T

163 lines
4.3 KiB
Go

package ui
import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
// arrowNavigation lets a form be driven with the arrow keys:
// - Up/Down move between the fields and buttons,
// - Left/Right change the value of a closed dropdown without opening it
// (and move between the buttons),
//
// Enter or Space still opens a dropdown list. While a list is open it gets all keys.
//
// With hooks, the highlighted entry of an open list is reported while the user
// moves through it, before anything is selected.
//
// cancel is called on Esc, except when Esc only closes an open dropdown list
// (tview would otherwise treat that as cancelling the whole form).
func arrowNavigation(f *tview.Form, cancel func(), hooks *browseHooks) {
var b browser
var closingList bool
f.SetCancelFunc(func() {
if closingList {
closingList = false
return
}
if cancel != nil {
cancel()
}
})
f.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
closingList = false
item, button := f.GetFocusedItemIndex()
if item < 0 && button < 0 {
return ev
}
dd, _ := focusedDropDown(f, item)
if dd != nil && dd.IsOpen() {
closingList = ev.Key() == tcell.KeyEscape
if hooks != nil {
b.track(dd, ev, hooks)
}
return ev
}
b.dd = nil
total := f.GetFormItemCount() + f.GetButtonCount()
pos := item
if button >= 0 {
pos = f.GetFormItemCount() + button
}
switch ev.Key() {
case tcell.KeyDown:
f.SetFocus(min(pos+1, total-1))
case tcell.KeyUp:
f.SetFocus(max(pos-1, 0))
case tcell.KeyLeft, tcell.KeyRight:
step := 1
if ev.Key() == tcell.KeyLeft {
step = -1
}
switch {
case dd != nil:
n := dd.GetOptionCount()
cur, _ := dd.GetCurrentOption()
dd.SetCurrentOption((cur + step + n) % n)
case button >= 0:
f.SetFocus(min(max(pos+step, f.GetFormItemCount()), total-1))
default:
return ev
}
default:
return ev
}
return nil
})
}
func focusedDropDown(f *tview.Form, item int) (*tview.DropDown, bool) {
if item < 0 {
return nil, false
}
dd, ok := f.GetFormItem(item).(*tview.DropDown)
return dd, ok
}
// bindSearchNow keeps a "Search now" checkbox consistent with whether a search
// can find anything. While searchable() is false the box is unchecked, greyed
// out and cannot be ticked (blocked is called instead); it returns to the
// user's earlier choice afterwards. The returned function must be called
// whenever the answer of searchable() may have changed.
//
// tview's SetDisabled is avoided on purpose: on a form item it re-triggers
// the form's focus handling and leaves two fields highlighted.
func bindSearchNow(box *tview.Checkbox, searchable func() bool, blocked func()) (sync func()) {
want := true
box.SetChangedFunc(func(checked bool) {
switch {
case searchable():
want = checked
case checked:
box.SetChecked(false)
blocked()
}
})
return func() {
if searchable() {
box.SetLabelColor(tview.Styles.SecondaryTextColor)
box.SetChecked(want)
return
}
box.SetLabelColor(tcell.ColorGray)
box.SetChecked(false)
}
}
// browseHooks lets a form follow the highlight of an open dropdown list.
type browseHooks struct {
browse func(dd *tview.DropDown, index int) // the highlight moved to entry index
abort func() // the list was closed without selecting
}
// browser mirrors the highlight of an open dropdown list, which tview does
// not expose: it starts at the current option and follows the navigation keys.
type browser struct {
dd *tview.DropDown
idx int
known bool // false after keys that move the highlight in ways we do not track
}
func (b *browser) track(dd *tview.DropDown, ev *tcell.EventKey, h *browseHooks) {
n := dd.GetOptionCount()
if b.dd != dd {
cur, _ := dd.GetCurrentOption()
b.dd, b.idx, b.known = dd, max(cur, 0), true
}
move := func(to int) {
b.idx = to
if b.known && h.browse != nil {
h.browse(dd, b.idx)
}
}
switch ev.Key() {
case tcell.KeyDown:
move((b.idx + 1) % n)
case tcell.KeyUp:
move((b.idx - 1 + n) % n)
case tcell.KeyHome:
move(0)
case tcell.KeyEnd:
move(n - 1)
case tcell.KeyEscape:
b.dd = nil
if h.abort != nil {
h.abort()
}
case tcell.KeyEnter:
b.dd = nil // the selection is reported by the dropdown itself
default:
b.known = false // paging, typed prefixes
}
}