scrollable: extract scrolling behavior for reuse

Extract the vertical scrolling ability into its own Scrollable struct
that can be embedded and reused across any ui element that relies on
scrolling.

Signed-off-by: Koni Marti <koni.marti@gmail.com>
Acked-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
Koni Marti 2022-02-25 17:53:33 +01:00 committed by Robin Jarry
parent bd65ce1010
commit 515a8b56f6
4 changed files with 101 additions and 139 deletions

View file

@ -41,6 +41,7 @@ type DirectoryLister interface {
type DirectoryList struct { type DirectoryList struct {
ui.Invalidatable ui.Invalidatable
Scrollable
aercConf *config.AercConfig aercConf *config.AercConfig
acctConf *config.AccountConfig acctConf *config.AccountConfig
store *lib.DirStore store *lib.DirStore
@ -48,7 +49,6 @@ type DirectoryList struct {
logger *log.Logger logger *log.Logger
selecting string selecting string
selected string selected string
scroll int
spinner *Spinner spinner *Spinner
worker *types.Worker worker *types.Worker
skipSelect chan bool skipSelect chan bool
@ -261,16 +261,11 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
return return
} }
dirlist.ensureScroll(ctx.Height()) dirlist.UpdateScroller(ctx.Height(), len(dirlist.dirs))
dirlist.EnsureScroll(findString(dirlist.dirs, dirlist.selecting))
needScrollbar := true
percentVisible := float64(ctx.Height()) / float64(len(dirlist.dirs))
if percentVisible >= 1.0 {
needScrollbar = false
}
textWidth := ctx.Width() textWidth := ctx.Width()
if needScrollbar { if dirlist.NeedScrollbar() {
textWidth -= 1 textWidth -= 1
} }
if textWidth < 0 { if textWidth < 0 {
@ -278,10 +273,10 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
} }
for i, name := range dirlist.dirs { for i, name := range dirlist.dirs {
if i < dirlist.scroll { if i < dirlist.Scroll() {
continue continue
} }
row := i - dirlist.scroll row := i - dirlist.Scroll()
if row >= ctx.Height() { if row >= ctx.Height() {
break break
} }
@ -299,13 +294,13 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
ctx.Printf(0, row, style, dirString) ctx.Printf(0, row, style, dirString)
} }
if needScrollbar { if dirlist.NeedScrollbar() {
scrollBarCtx := ctx.Subcontext(ctx.Width()-1, 0, 1, ctx.Height()) scrollBarCtx := ctx.Subcontext(ctx.Width()-1, 0, 1, ctx.Height())
dirlist.drawScrollbar(scrollBarCtx, percentVisible) dirlist.drawScrollbar(scrollBarCtx)
} }
} }
func (dirlist *DirectoryList) drawScrollbar(ctx *ui.Context, percentVisible float64) { func (dirlist *DirectoryList) drawScrollbar(ctx *ui.Context) {
gutterStyle := tcell.StyleDefault gutterStyle := tcell.StyleDefault
pillStyle := tcell.StyleDefault.Reverse(true) pillStyle := tcell.StyleDefault.Reverse(true)
@ -313,44 +308,11 @@ func (dirlist *DirectoryList) drawScrollbar(ctx *ui.Context, percentVisible floa
ctx.Fill(0, 0, 1, ctx.Height(), ' ', gutterStyle) ctx.Fill(0, 0, 1, ctx.Height(), ' ', gutterStyle)
// pill // pill
pillSize := int(math.Ceil(float64(ctx.Height()) * percentVisible)) pillSize := int(math.Ceil(float64(ctx.Height()) * dirlist.PercentVisible()))
percentScrolled := float64(dirlist.scroll) / float64(len(dirlist.dirs)) pillOffset := int(math.Floor(float64(ctx.Height()) * dirlist.PercentScrolled()))
pillOffset := int(math.Floor(float64(ctx.Height()) * percentScrolled))
ctx.Fill(0, pillOffset, 1, pillSize, ' ', pillStyle) ctx.Fill(0, pillOffset, 1, pillSize, ' ', pillStyle)
} }
func (dirlist *DirectoryList) ensureScroll(h int) {
selectingIdx := findString(dirlist.dirs, dirlist.selecting)
if selectingIdx < 0 {
// dir not found, meaning we are currently adding / removing a dir.
// we can simply ignore this until we get redrawn with the new
// dirlist.dir content
return
}
maxScroll := len(dirlist.dirs) - h
if maxScroll < 0 {
maxScroll = 0
}
if selectingIdx >= dirlist.scroll && selectingIdx < dirlist.scroll+h {
if dirlist.scroll > maxScroll {
dirlist.scroll = maxScroll
}
return
}
if selectingIdx >= dirlist.scroll+h {
dirlist.scroll = selectingIdx - h + 1
} else if selectingIdx < dirlist.scroll {
dirlist.scroll = selectingIdx
}
if dirlist.scroll > maxScroll {
dirlist.scroll = maxScroll
}
}
func (dirlist *DirectoryList) MouseEvent(localX int, localY int, event tcell.Event) { func (dirlist *DirectoryList) MouseEvent(localX int, localY int, event tcell.Event) {
switch event := event.(type) { switch event := event.(type) {
case *tcell.EventMouse: case *tcell.EventMouse:

View file

@ -40,7 +40,7 @@ func (dt *DirectoryTree) UpdateList(done func([]string)) {
dt.buildTree() dt.buildTree()
dt.listIdx = findString(dt.dirs, dt.selecting) dt.listIdx = findString(dt.dirs, dt.selecting)
dt.Select(dt.selecting) dt.Select(dt.selecting)
dt.scroll = 0 dt.Scrollable = Scrollable{}
}) })
} }
@ -60,7 +60,8 @@ func (dt *DirectoryTree) Draw(ctx *ui.Context) {
return return
} }
dt.ensureScroll(ctx.Height()) dt.UpdateScroller(ctx.Height(), n)
dt.EnsureScroll(dt.countVisible(dt.list[:dt.listIdx]))
needScrollbar := true needScrollbar := true
percentVisible := float64(ctx.Height()) / float64(n) percentVisible := float64(ctx.Height()) / float64(n)
@ -78,10 +79,10 @@ func (dt *DirectoryTree) Draw(ctx *ui.Context) {
rowNr := 0 rowNr := 0
for i, node := range dt.list { for i, node := range dt.list {
if i < dt.scroll || !isVisible(node) { if i < dt.Scroll() || !isVisible(node) {
continue continue
} }
row := rowNr - dt.scroll row := rowNr - dt.Scroll()
if row >= ctx.Height() { if row >= ctx.Height() {
break break
} }
@ -105,41 +106,9 @@ func (dt *DirectoryTree) Draw(ctx *ui.Context) {
ctx.Printf(0, row, style, dirString) ctx.Printf(0, row, style, dirString)
} }
if needScrollbar { if dt.NeedScrollbar() {
scrollBarCtx := ctx.Subcontext(ctx.Width()-1, 0, 1, ctx.Height()) scrollBarCtx := ctx.Subcontext(ctx.Width()-1, 0, 1, ctx.Height())
dt.drawScrollbar(scrollBarCtx, percentVisible) dt.drawScrollbar(scrollBarCtx)
}
}
func (dt *DirectoryTree) ensureScroll(h int) {
selectingIdx := dt.countVisible(dt.list[:dt.listIdx])
if selectingIdx < 0 {
// dir not found, meaning we are currently adding / removing a dir.
// we can simply ignore this until we get redrawn with the new
// dirlist.dir content
return
}
maxScroll := dt.countVisible(dt.list) - h
if maxScroll < 0 {
maxScroll = 0
}
if selectingIdx >= dt.scroll && selectingIdx < dt.scroll+h {
if dt.scroll > maxScroll {
dt.scroll = maxScroll
}
return
}
if selectingIdx >= dt.scroll+h {
dt.scroll = selectingIdx - h + 1
} else if selectingIdx < dt.scroll {
dt.scroll = selectingIdx
}
if dt.scroll > maxScroll {
dt.scroll = maxScroll
} }
} }

View file

@ -20,10 +20,10 @@ import (
type MessageList struct { type MessageList struct {
ui.Invalidatable ui.Invalidatable
Scrollable
conf *config.AercConfig conf *config.AercConfig
logger *log.Logger logger *log.Logger
height int height int
scroll int
nmsgs int nmsgs int
spinner *Spinner spinner *Spinner
store *lib.MessageStore store *lib.MessageStore
@ -70,16 +70,13 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
} }
} }
ml.ensureScroll() ml.UpdateScroller(ml.height, len(store.Uids()))
if store := ml.Store(); store != nil && len(store.Uids()) > 0 {
needScrollbar := true ml.EnsureScroll(store.SelectedIndex())
percentVisible := float64(ctx.Height()) / float64(len(store.Uids()))
if percentVisible >= 1.0 {
needScrollbar = false
} }
textWidth := ctx.Width() textWidth := ctx.Width()
if needScrollbar { if ml.NeedScrollbar() {
textWidth -= 1 textWidth -= 1
} }
if textWidth < 0 { if textWidth < 0 {
@ -105,7 +102,7 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
return nil return nil
} }
counter-- counter--
if counter > len(store.Uids())-1-ml.scroll { if counter > len(store.Uids())-1-ml.Scroll() {
//skip messages which are higher than the viewport //skip messages which are higher than the viewport
return nil return nil
} }
@ -142,7 +139,7 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
} }
} else { } else {
uids := store.Uids() uids := store.Uids()
for i := len(uids) - 1 - ml.scroll; i >= 0; i-- { for i := len(uids) - 1 - ml.Scroll(); i >= 0; i-- {
uid := uids[i] uid := uids[i]
msg := store.Messages[uid] msg := store.Messages[uid]
fmtCtx := format.Ctx{ fmtCtx := format.Ctx{
@ -159,9 +156,9 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
} }
} }
if needScrollbar { if ml.NeedScrollbar() {
scrollbarCtx := ctx.Subcontext(ctx.Width()-1, 0, 1, ctx.Height()) scrollbarCtx := ctx.Subcontext(ctx.Width()-1, 0, 1, ctx.Height())
ml.drawScrollbar(scrollbarCtx, percentVisible) ml.drawScrollbar(scrollbarCtx)
} }
if len(store.Uids()) == 0 { if len(store.Uids()) == 0 {
@ -241,7 +238,7 @@ func (ml *MessageList) drawRow(textWidth int, ctx *ui.Context, uid uint32, row i
var style tcell.Style var style tcell.Style
// current row // current row
if row == ml.store.SelectedIndex()-ml.scroll { if row == ml.store.SelectedIndex()-ml.Scroll() {
style = uiConfig.GetComposedStyleSelected(config.STYLE_MSGLIST_DEFAULT, msg_styles) style = uiConfig.GetComposedStyleSelected(config.STYLE_MSGLIST_DEFAULT, msg_styles)
} else { } else {
style = uiConfig.GetComposedStyle(config.STYLE_MSGLIST_DEFAULT, msg_styles) style = uiConfig.GetComposedStyle(config.STYLE_MSGLIST_DEFAULT, msg_styles)
@ -265,7 +262,7 @@ func (ml *MessageList) drawRow(textWidth int, ctx *ui.Context, uid uint32, row i
return false return false
} }
func (ml *MessageList) drawScrollbar(ctx *ui.Context, percentVisible float64) { func (ml *MessageList) drawScrollbar(ctx *ui.Context) {
gutterStyle := tcell.StyleDefault gutterStyle := tcell.StyleDefault
pillStyle := tcell.StyleDefault.Reverse(true) pillStyle := tcell.StyleDefault.Reverse(true)
@ -273,9 +270,8 @@ func (ml *MessageList) drawScrollbar(ctx *ui.Context, percentVisible float64) {
ctx.Fill(0, 0, 1, ctx.Height(), ' ', gutterStyle) ctx.Fill(0, 0, 1, ctx.Height(), ' ', gutterStyle)
// pill // pill
pillSize := int(math.Ceil(float64(ctx.Height()) * percentVisible)) pillSize := int(math.Ceil(float64(ctx.Height()) * ml.PercentVisible()))
percentScrolled := float64(ml.scroll) / float64(len(ml.Store().Uids())) pillOffset := int(math.Floor(float64(ctx.Height()) * ml.PercentScrolled()))
pillOffset := int(math.Floor(float64(ctx.Height()) * percentScrolled))
ctx.Fill(0, pillOffset, 1, pillSize, ' ', pillStyle) ctx.Fill(0, pillOffset, 1, pillSize, ' ', pillStyle)
} }
@ -328,7 +324,7 @@ func (ml *MessageList) Clicked(x, y int) (int, bool) {
if store == nil || ml.nmsgs == 0 || y >= ml.nmsgs { if store == nil || ml.nmsgs == 0 || y >= ml.nmsgs {
return 0, false return 0, false
} }
return y + ml.scroll, true return y + ml.Scroll(), true
} }
func (ml *MessageList) Height() int { func (ml *MessageList) Height() int {
@ -364,7 +360,7 @@ func (ml *MessageList) storeUpdate(store *lib.MessageStore) {
func (ml *MessageList) SetStore(store *lib.MessageStore) { func (ml *MessageList) SetStore(store *lib.MessageStore) {
if ml.Store() != store { if ml.Store() != store {
ml.scroll = 0 ml.Scrollable = Scrollable{}
} }
ml.store = store ml.store = store
if store != nil { if store != nil {
@ -402,39 +398,6 @@ func (ml *MessageList) Select(index int) {
ml.Invalidate() ml.Invalidate()
} }
func (ml *MessageList) ensureScroll() {
store := ml.Store()
if store == nil || len(store.Uids()) == 0 {
return
}
h := ml.Height()
maxScroll := len(store.Uids()) - h
if maxScroll < 0 {
maxScroll = 0
}
selectedIndex := store.SelectedIndex()
if selectedIndex >= ml.scroll && selectedIndex < ml.scroll+h {
if ml.scroll > maxScroll {
ml.scroll = maxScroll
}
return
}
if selectedIndex >= ml.scroll+h {
ml.scroll = selectedIndex - h + 1
} else if selectedIndex < ml.scroll {
ml.scroll = selectedIndex
}
if ml.scroll > maxScroll {
ml.scroll = maxScroll
}
}
func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) { func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) {
uiConfig := ml.aerc.SelectedAccountUiConfig() uiConfig := ml.aerc.SelectedAccountUiConfig()
msg := uiConfig.EmptyMessage msg := uiConfig.EmptyMessage

68
widgets/scrollable.go Normal file
View file

@ -0,0 +1,68 @@
package widgets
// Scrollable implements vertical scrolling
type Scrollable struct {
scroll int
height int
elems int
}
func (s *Scrollable) Scroll() int {
return s.scroll
}
func (s *Scrollable) PercentVisible() float64 {
if s.elems <= 0 {
return 1.0
}
return float64(s.height) / float64(s.elems)
}
func (s *Scrollable) PercentScrolled() float64 {
if s.elems <= 0 {
return 1.0
}
return float64(s.scroll) / float64(s.elems)
}
func (s *Scrollable) NeedScrollbar() bool {
needScrollbar := true
if s.PercentVisible() >= 1.0 {
needScrollbar = false
}
return needScrollbar
}
func (s *Scrollable) UpdateScroller(height, elems int) {
s.height = height
s.elems = elems
}
func (s *Scrollable) EnsureScroll(selectingIdx int) {
if selectingIdx < 0 {
return
}
maxScroll := s.elems - s.height
if maxScroll < 0 {
maxScroll = 0
}
if selectingIdx >= s.scroll && selectingIdx < s.scroll+s.height {
if s.scroll > maxScroll {
s.scroll = maxScroll
}
return
}
if selectingIdx >= s.scroll+s.height {
s.scroll = selectingIdx - s.height + 1
} else if selectingIdx < s.scroll {
s.scroll = selectingIdx
}
if s.scroll > maxScroll {
s.scroll = maxScroll
}
}