diff options
Diffstat (limited to 'app/listbox.go')
-rw-r--r-- | app/listbox.go | 299 |
1 files changed, 299 insertions, 0 deletions
diff --git a/app/listbox.go b/app/listbox.go new file mode 100644 index 00000000..5a80261e --- /dev/null +++ b/app/listbox.go @@ -0,0 +1,299 @@ +package app + +import ( + "math" + "strings" + "sync" + + "git.sr.ht/~rjarry/aerc/config" + "git.sr.ht/~rjarry/aerc/lib/ui" + "git.sr.ht/~rjarry/aerc/log" + "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 { + log.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) +} |