aerc/widgets/listbox.go

300 lines
6.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package widgets
import (
"math"
"strings"
"sync"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/logging"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
)
type ListBox struct {
Scrollable
title string
lines []string
selected string
cursorPos int
horizPos int
jump int
showCursor bool
showFilter bool
filterMutex sync.Mutex
filter *ui.TextInput
uiConfig *config.UIConfig
cb func(string)
}
func NewListBox(title string, lines []string, uiConfig *config.UIConfig, cb func(string)) *ListBox {
lb := &ListBox{
title: title,
lines: lines,
cursorPos: -1,
jump: -1,
uiConfig: uiConfig,
cb: cb,
filter: ui.NewTextInput("", uiConfig),
}
lb.filter.OnChange(func(ti *ui.TextInput) {
var show bool
if ti.String() == "" {
show = false
} else {
show = true
}
lb.setShowFilterField(show)
lb.filter.Focus(show)
lb.Invalidate()
})
lb.dedup()
return lb
}
func (lb *ListBox) dedup() {
dedupped := make([]string, 0, len(lb.lines))
dedup := make(map[string]struct{})
for _, line := range lb.lines {
if _, dup := dedup[line]; dup {
logging.Warnf("ignore duplicate: %s", line)
continue
}
dedup[line] = struct{}{}
dedupped = append(dedupped, line)
}
lb.lines = dedupped
}
func (lb *ListBox) setShowFilterField(b bool) {
lb.filterMutex.Lock()
defer lb.filterMutex.Unlock()
lb.showFilter = b
}
func (lb *ListBox) showFilterField() bool {
lb.filterMutex.Lock()
defer lb.filterMutex.Unlock()
return lb.showFilter
}
func (lb *ListBox) Draw(ctx *ui.Context) {
defaultStyle := lb.uiConfig.GetStyle(config.STYLE_DEFAULT)
titleStyle := lb.uiConfig.GetStyle(config.STYLE_TITLE)
w, h := ctx.Width(), ctx.Height()
ctx.Fill(0, 0, w, h, ' ', defaultStyle)
ctx.Fill(0, 0, w, 1, ' ', titleStyle)
ctx.Printf(0, 0, titleStyle, "%s", lb.title)
y := 0
if lb.showFilterField() {
y = 1
x := ctx.Printf(0, y, defaultStyle, "Filter: ")
lb.filter.Draw(ctx.Subcontext(x, y, w-x, 1))
}
lb.drawBox(ctx.Subcontext(0, y+1, w, h-(y+1)))
}
func (lb *ListBox) moveCursor(delta int) {
list := lb.filtered()
if len(list) == 0 {
return
}
lb.cursorPos += delta
if lb.cursorPos < 0 {
lb.cursorPos = 0
}
if lb.cursorPos >= len(list) {
lb.cursorPos = len(list) - 1
}
lb.selected = list[lb.cursorPos]
lb.showCursor = true
lb.horizPos = 0
}
func (lb *ListBox) moveHorizontal(delta int) {
lb.horizPos += delta
if lb.horizPos > len(lb.selected) {
lb.horizPos = len(lb.selected)
}
if lb.horizPos < 0 {
lb.horizPos = 0
}
}
func (lb *ListBox) filtered() []string {
list := []string{}
filterTerm := lb.filter.String()
for _, line := range lb.lines {
if strings.Contains(line, filterTerm) {
list = append(list, line)
}
}
return list
}
func (lb *ListBox) drawBox(ctx *ui.Context) {
defaultStyle := lb.uiConfig.GetStyle(config.STYLE_DEFAULT)
selectedStyle := lb.uiConfig.GetComposedStyleSelected(config.STYLE_MSGLIST_DEFAULT, nil)
w, h := ctx.Width(), ctx.Height()
lb.jump = h
list := lb.filtered()
lb.UpdateScroller(ctx.Height(), len(list))
scroll := 0
lb.cursorPos = -1
for i := 0; i < len(list); i++ {
if lb.selected == list[i] {
scroll = i
lb.cursorPos = i
break
}
}
lb.EnsureScroll(scroll)
needScrollbar := lb.NeedScrollbar()
if needScrollbar {
w -= 1
if w < 0 {
w = 0
}
}
if lb.lines == nil || len(list) == 0 {
return
}
y := 0
for i := lb.Scroll(); i < len(list) && y < h; i++ {
style := defaultStyle
line := runewidth.Truncate(list[i], w-1, "")
if lb.selected == list[i] && lb.showCursor {
style = selectedStyle
if len(list[i]) > w {
if len(list[i])-lb.horizPos < w {
lb.horizPos = len(list[i]) - w + 1
}
rest := list[i][lb.horizPos:]
line = runewidth.Truncate(rest,
w-1, "")
if lb.horizPos > 0 && len(line) > 0 {
line = "" + line[1:]
}
}
}
ctx.Printf(1, y, style, line)
y += 1
}
if needScrollbar {
scrollBarCtx := ctx.Subcontext(w, 0, 1, ctx.Height())
lb.drawScrollbar(scrollBarCtx)
}
}
func (lb *ListBox) drawScrollbar(ctx *ui.Context) {
gutterStyle := tcell.StyleDefault
pillStyle := tcell.StyleDefault.Reverse(true)
// gutter
h := ctx.Height()
ctx.Fill(0, 0, 1, h, ' ', gutterStyle)
// pill
pillSize := int(math.Ceil(float64(h) * lb.PercentVisible()))
pillOffset := int(math.Floor(float64(h) * lb.PercentScrolled()))
ctx.Fill(0, pillOffset, 1, pillSize, ' ', pillStyle)
}
func (lb *ListBox) Invalidate() {
ui.Invalidate()
}
func (lb *ListBox) Event(event tcell.Event) bool {
if event, ok := event.(*tcell.EventKey); ok {
switch event.Key() {
case tcell.KeyLeft:
lb.moveHorizontal(-1)
lb.Invalidate()
return true
case tcell.KeyRight:
lb.moveHorizontal(+1)
lb.Invalidate()
return true
case tcell.KeyCtrlB:
line := lb.selected[:lb.horizPos]
fds := strings.Fields(line)
if len(fds) > 1 {
lb.moveHorizontal(
strings.LastIndex(line,
fds[len(fds)-1]) - lb.horizPos - 1)
} else {
lb.horizPos = 0
}
lb.Invalidate()
return true
case tcell.KeyCtrlW:
line := lb.selected[lb.horizPos+1:]
fds := strings.Fields(line)
if len(fds) > 1 {
lb.moveHorizontal(strings.Index(line, fds[1]))
}
lb.Invalidate()
return true
case tcell.KeyCtrlA, tcell.KeyHome:
lb.horizPos = 0
lb.Invalidate()
return true
case tcell.KeyCtrlE, tcell.KeyEnd:
lb.horizPos = len(lb.selected)
lb.Invalidate()
return true
case tcell.KeyCtrlP, tcell.KeyUp:
lb.moveCursor(-1)
lb.Invalidate()
return true
case tcell.KeyCtrlN, tcell.KeyDown:
lb.moveCursor(+1)
lb.Invalidate()
return true
case tcell.KeyPgUp:
if lb.jump >= 0 {
lb.moveCursor(-lb.jump)
lb.Invalidate()
}
return true
case tcell.KeyPgDn:
if lb.jump >= 0 {
lb.moveCursor(+lb.jump)
lb.Invalidate()
}
return true
case tcell.KeyEnter:
return lb.quit(lb.selected)
case tcell.KeyEsc:
return lb.quit("")
}
}
if lb.filter != nil {
handled := lb.filter.Event(event)
lb.Invalidate()
return handled
}
return false
}
func (lb *ListBox) quit(s string) bool {
lb.filter.Focus(false)
if lb.cb != nil {
lb.cb(s)
}
return true
}
func (lb *ListBox) Focus(f bool) {
lb.filter.Focus(f)
}