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:
parent
bd65ce1010
commit
515a8b56f6
4 changed files with 101 additions and 139 deletions
|
@ -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:
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
68
widgets/scrollable.go
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue