refactor(ui): scroll the trees and lists with a viewport

This commit is contained in:
ayn2op 2026-10-08 19:01:30 -04:00
parent 715ed5aaeb
commit 6449f7d470
15 changed files with 166 additions and 79 deletions

2
go.mod
View file

@ -15,7 +15,7 @@ require (
github.com/andybalholm/brotli v1.2.6
github.com/ayn2op/arikawa/v3 v3.0.0-20260929021956-7d0802bf4d7c
github.com/ayn2op/ningen/v3 v3.0.1-0.20260929010920-29794d5fa70e
github.com/ayn2op/tview v0.0.0-20261008190544-5da45c25c67d
github.com/ayn2op/tview v0.0.0-20261008230130-de4c692f9141
github.com/bogdanfinn/fhttp v0.6.9
github.com/bogdanfinn/tls-client v1.16.0
github.com/gdamore/tcell/v3 v3.5.0

4
go.sum
View file

@ -14,8 +14,8 @@ github.com/ayn2op/arikawa/v3 v3.0.0-20260929021956-7d0802bf4d7c h1:Qkl3W5RXzUQUR
github.com/ayn2op/arikawa/v3 v3.0.0-20260929021956-7d0802bf4d7c/go.mod h1:SHqcr+EmZKeNtVKZybHJn4HNz+ufA1XbeKNSTigBCEE=
github.com/ayn2op/ningen/v3 v3.0.1-0.20260929010920-29794d5fa70e h1:QAei/IVEyZV7GcpkpcFUh37+QRqwrt57z7OT3ciHrhI=
github.com/ayn2op/ningen/v3 v3.0.1-0.20260929010920-29794d5fa70e/go.mod h1:RsvYiMzi4Si1F7vmml5NCL2DaKIZXkNUQC7QlGTMtCw=
github.com/ayn2op/tview v0.0.0-20261008190544-5da45c25c67d h1:nueCN+u4FjTTU2ZdyDfRG68eCwa9Oq4dQY3CMgRUZQc=
github.com/ayn2op/tview v0.0.0-20261008190544-5da45c25c67d/go.mod h1:sMf5NqBlGJjsaGCUycTqTyNG5vewJU6jq/3BdJIzbCU=
github.com/ayn2op/tview v0.0.0-20261008230130-de4c692f9141 h1:c3pb7UfbQqtmBn2HbJ+XRXJTdo/vHgIsK+CKuYoFPYU=
github.com/ayn2op/tview v0.0.0-20261008230130-de4c692f9141/go.mod h1:sMf5NqBlGJjsaGCUycTqTyNG5vewJU6jq/3BdJIzbCU=
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic=

View file

@ -4,8 +4,8 @@ import (
"fmt"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/list"
"github.com/ayn2op/tview/scrollbar"
"github.com/ayn2op/tview/viewport"
)
// enum pairs the names an option accepts with their values; a value is written back as the first of its names.
@ -56,13 +56,13 @@ var (
{"vertical", scrollbar.SymbolSetVertical()},
{"double_vertical", scrollbar.SymbolSetDoubleVertical()},
}
scrollBarVisibilities = enum[list.ScrollBarVisibility]{
{"auto", list.ScrollBarVisibilityAutomatic},
{"automatic", list.ScrollBarVisibilityAutomatic},
{"always", list.ScrollBarVisibilityAlways},
{"never", list.ScrollBarVisibilityNever},
{"hidden", list.ScrollBarVisibilityNever},
{"off", list.ScrollBarVisibilityNever},
scrollBarVisibilities = enum[viewport.ScrollBarVisibility]{
{"auto", viewport.ScrollBarVisibilityAutomatic},
{"automatic", viewport.ScrollBarVisibilityAutomatic},
{"always", viewport.ScrollBarVisibilityAlways},
{"never", viewport.ScrollBarVisibilityNever},
{"hidden", viewport.ScrollBarVisibilityNever},
{"off", viewport.ScrollBarVisibilityNever},
}
)
@ -93,7 +93,7 @@ func (sw *SymbolSetWrapper) UnmarshalText(text []byte) (err error) {
func (sw SymbolSetWrapper) MarshalText() ([]byte, error) { return symbolSets.text(sw.SymbolSet) }
type ScrollBarVisibilityWrapper struct{ list.ScrollBarVisibility }
type ScrollBarVisibilityWrapper struct{ viewport.ScrollBarVisibility }
func (vw *ScrollBarVisibilityWrapper) UnmarshalText(text []byte) (err error) {
vw.ScrollBarVisibility, err = scrollBarVisibilities.parse(string(text))
@ -195,7 +195,7 @@ func defaultUI() UIConfig {
Border: BorderUIConfig{Enabled: true, Padding: [4]int{0, 0, 1, 1}, NormalSet: round, ActiveSet: round},
GuildsTree: GuildsTreeUIConfig{TreeUIConfig{Graphics: true}, true},
MembersTree: TreeUIConfig{Graphics: true},
ScrollBar: ScrollBarUIConfig{SymbolSet: SymbolSetWrapper{boxDrawing}},
ScrollBar: ScrollBarUIConfig{SymbolSet: SymbolSetWrapper{boxDrawing}, Visibility: ScrollBarVisibilityWrapper{viewport.ScrollBarVisibilityAutomatic}},
MessagesList: MessagesListUIConfig{ReplyIndicator: ">", ForwardedIndicator: "<"},
MentionsList: MentionsListUIConfig{MinWidth: 20},
}

View file

@ -14,6 +14,7 @@ import (
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/keybind"
"github.com/ayn2op/tview/tree"
"github.com/ayn2op/tview/viewport"
"github.com/gdamore/tcell/v3"
)
@ -22,6 +23,7 @@ type dmNode struct{}
type Model struct {
root *tree.Node
selectionState tree.SelectionState
scrollState viewport.ScrollState
cfg *config.Config
state *ningen.State
@ -53,6 +55,7 @@ func (m Model) View(focused bool) tview.Widget {
return uitree.New(
m.root,
m.selectionState,
m.scrollState,
m.cfg,
m.cfg.Theme.GuildsTree,
m.cfg.UI.GuildsTree.Graphics,
@ -109,7 +112,7 @@ func (m *Model) rebuild(event *gateway.ReadyEvent) {
m.createFolderNode(folder, guildsByID)
}
}
m.selectionState.SetCurrentNode(root)
m.setCurrentNode(root)
}
func (m Model) refreshReadStyles(event *read.UpdateEvent) {
@ -298,7 +301,13 @@ func (m *Model) collapseParentNode(node *tree.Node) {
}
parent := path[len(path)-2]
parent.Collapse()
m.selectionState.SetCurrentNode(parent)
m.setCurrentNode(parent)
}
// setCurrentNode selects node and scrolls to it.
func (m *Model) setCurrentNode(node *tree.Node) {
m.selectionState.SetCurrentNode(node)
m.scrollState.ScrollToTarget()
}
// Update returns m changed in response to msg and a command to run, or nil.
@ -323,9 +332,16 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
return m, m.navigate(msg.ChannelID)
case Msg:
m.selectionState.Apply(tree.Change(msg))
m.scrollState.ScrollToTarget()
return m, nil
case uitree.ScrollMsg:
m.scrollState.Apply(msg.Change)
if msg.Msg != nil {
return m.Update(msg.Msg)
}
return m, nil
case SelectedMsg:
m.selectionState.SetCurrentNode(msg.Node)
m.setCurrentNode(msg.Node)
return m, m.selectNode(msg.Node)
case tview.KeyMsg:
switch {
@ -348,7 +364,7 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
}
}
m.expandPathToNode(node)
m.selectionState.SetCurrentNode(node)
m.setCurrentNode(node)
}
return m, nil
case keybind.Matches(msg, m.cfg.Keybinds.GuildsTree.YankID.Keybind):

View file

@ -10,7 +10,7 @@ import (
"github.com/ayn2op/tview/tree"
)
// Msg moves or scrolls the tree.
// Msg moves the tree.
type Msg tree.Change
// SelectedMsg is sent when the user selects a node.
@ -37,7 +37,7 @@ func (m *Model) navigate(channelID discord.ChannelID) tview.Cmd {
return nil
}
m.expandPathToNode(node)
m.selectionState.SetCurrentNode(node)
m.setCurrentNode(node)
if channel.Type == discord.GuildCategory {
return nil
}

View file

@ -16,6 +16,7 @@ import (
"github.com/ayn2op/tview/keybind"
"github.com/ayn2op/tview/richtext"
"github.com/ayn2op/tview/tree"
"github.com/ayn2op/tview/viewport"
"github.com/gdamore/tcell/v3"
)
@ -40,6 +41,7 @@ func (r memberRef) String() string {
type Model struct {
root *tree.Node
selectionState tree.SelectionState
scrollState viewport.ScrollState
cfg *config.Config
state *ningen.State
@ -85,7 +87,13 @@ func (m *Model) SetChannel(channel *discord.Channel) tview.Cmd {
func (m Model) View(focused bool) tview.Widget {
onChange := func(c tree.Change) tview.Msg { return Msg(c) }
onSelect := func(n *tree.Node) tview.Msg { return SelectedMsg{Node: n} }
return uitree.New(m.root, m.selectionState, m.cfg, m.cfg.Theme.MembersTree, m.cfg.UI.MembersTree.Graphics, m.cfg.Keybinds.MembersTree, focused, onChange, onSelect).Title("Members")
return uitree.New(m.root, m.selectionState, m.scrollState, m.cfg, m.cfg.Theme.MembersTree, m.cfg.UI.MembersTree.Graphics, m.cfg.Keybinds.MembersTree, focused, onChange, onSelect).Title("Members")
}
// setCurrentNode selects node and scrolls to it.
func (m *Model) setCurrentNode(node *tree.Node) {
m.selectionState.SetCurrentNode(node)
m.scrollState.ScrollToTarget()
}
// Update returns m changed in response to msg and a command to run, or nil.
@ -106,9 +114,16 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
case Msg:
m.selectionState.Apply(tree.Change(msg))
m.scrollState.ScrollToTarget()
return m, m.requestSelectedMembers()
case uitree.ScrollMsg:
m.scrollState.Apply(msg.Change)
if msg.Msg != nil {
return m.Update(msg.Msg)
}
return m, nil
case SelectedMsg:
m.selectionState.SetCurrentNode(msg.Node)
m.setCurrentNode(msg.Node)
if _, ok := msg.Node.Reference().(groupRef); ok {
msg.Node.SetExpanded(!msg.Node.Expanded())
}
@ -124,7 +139,7 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
case keybind.Matches(msg, kbs.CollapseParentNode.Keybind):
if parent := m.parent(m.selectionState.CurrentNode()); parent != nil {
parent.Collapse()
m.selectionState.SetCurrentNode(parent)
m.setCurrentNode(parent)
}
return m, nil
case keybind.Matches(msg, kbs.YankID.Keybind):

View file

@ -6,7 +6,7 @@ import (
"github.com/ayn2op/tview/tree"
)
// Msg moves or scrolls the tree.
// Msg moves the tree.
type Msg tree.Change
// SelectedMsg is sent when the user selects a node.

View file

@ -6,6 +6,8 @@ import (
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/layout"
"github.com/ayn2op/tview/list"
"github.com/ayn2op/tview/scrollbar"
"github.com/ayn2op/tview/viewport"
"github.com/rivo/uniseg"
)
@ -13,6 +15,7 @@ import (
type Model struct {
cfg *config.Config
selectionState list.SelectionState
scrollState viewport.ScrollState
items []Item
}
@ -29,8 +32,8 @@ func (m Model) View() tview.Widget {
func (Model) Init() tview.Cmd { return nil }
func (m Model) listView() list.Widget {
return list.New(m.selectionState, len(m.items), func(i int) tview.Widget {
func (m Model) listView() viewport.Widget {
l := list.New(m.selectionState, len(m.items), func(i int) tview.Widget {
item := m.items[i]
return row{text: item.DisplayText, style: item.Style}
}).
@ -38,12 +41,20 @@ func (m Model) listView() list.Widget {
Keybind(ui.SelectionKeybind(m.cfg.Keybinds.MentionsList.SelectionKeybinds)).
// The list is only shown while mentions are being completed, so it takes its keys first.
Focused(true).
OnChange(func(a list.Change) tview.Msg { return Msg(a) })
OnChange(func(a list.Change) tview.Msg { return listMsg(a) })
return viewport.New(l, m.scrollState).
Target(l.Target).
ScrollBar(scrollbar.New(), viewport.ScrollBarVisibilityAutomatic).
OnChange(func(a viewport.Change) tview.Msg { return scrollMsg(a) })
}
func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
if msg, ok := msg.(Msg); ok {
switch msg := msg.(type) {
case listMsg:
m.selectionState.Apply(list.Change(msg))
m.scrollState.ScrollToTarget()
case scrollMsg:
m.scrollState.Apply(viewport.Change(msg))
}
return m, nil
}
@ -79,6 +90,7 @@ func (m Model) MaxDisplayWidth() int {
func (m *Model) Rebuild() {
m.selectionState.SetCursor(min(0, len(m.items)-1))
m.scrollState.ScrollToTarget()
}
var _ tview.Model[Model] = Model{}

View file

@ -1,6 +1,17 @@
package mentionslist
import "github.com/ayn2op/tview/list"
import (
"github.com/ayn2op/tview/list"
"github.com/ayn2op/tview/viewport"
)
// Msg moves the list.
type Msg list.Change
// Msg is implemented by the messages that move or scroll the list.
type Msg interface{ mentionsList() }
type listMsg list.Change
func (listMsg) mentionsList() {}
type scrollMsg viewport.Change
func (scrollMsg) mentionsList() {}

View file

@ -30,6 +30,7 @@ import (
"github.com/ayn2op/tview/richtext"
"github.com/ayn2op/tview/scrollbar"
"github.com/ayn2op/tview/textview"
"github.com/ayn2op/tview/viewport"
"github.com/gdamore/tcell/v3"
"github.com/gdamore/tcell/v3/color"
"github.com/rivo/uniseg"
@ -48,6 +49,7 @@ type messageItem struct {
type Model struct {
selectionState list.SelectionState
scrollState viewport.ScrollState
cfg *config.Config
state *ningen.State
@ -75,7 +77,7 @@ func NewModel(cfg *config.Config, state *ningen.State) Model {
renderer: markdown.NewRenderer(cfg),
}
ml.selectionState.ScrollToEnd()
ml.scrollState.ScrollToEnd()
return ml
}
@ -96,43 +98,34 @@ func (m Model) title() string {
return title
}
func (m Model) listView(focused bool) list.Widget {
func (m Model) listView(focused bool) viewport.Widget {
cfg, theme := m.cfg.UI.ScrollBar, m.cfg.Theme.ScrollBar
bar := scrollbar.New().
SymbolSet(cfg.SymbolSet.SymbolSet).
Style(theme.TrackStyle.Style).
ThumbStyle(theme.ThumbStyle.Style)
return list.New(m.selectionState, len(m.items), m.buildItem).
// The model moves the selection itself, so the list is not focused and only takes clicks.
l := list.New(m.selectionState, len(m.items), m.buildItem).
SelectedStyle(m.cfg.Theme.MessagesList.SelectedMessageStyle.Style).
OnChange(func(a list.Change) tview.Msg { return listMsg(a) })
return viewport.New(l, m.scrollState).
Target(l.Target).
ScrollBar(bar, cfg.Visibility.ScrollBarVisibility).
TrackEnd(true).
Keybind(scrollKeybind(m.cfg.Keybinds.MessagesList.ScrollKeybinds)).
Keybind(ui.ScrollKeybind(m.cfg.Keybinds.MessagesList.ScrollKeybinds)).
Focused(focused).
OnChange(func(a list.Change) tview.Msg { return listMsg(a) })
}
// scrollKeybind binds kbs to scrolling the list.
func scrollKeybind(kbs config.ScrollKeybinds) func(tview.KeyMsg) list.Action {
return func(key tview.KeyMsg) list.Action {
switch {
case keybind.Matches(key, kbs.ScrollUp.Keybind):
return list.ActionScrollUp
case keybind.Matches(key, kbs.ScrollDown.Keybind):
return list.ActionScrollDown
case keybind.Matches(key, kbs.ScrollTop.Keybind):
return list.ActionScrollTop
case keybind.Matches(key, kbs.ScrollBottom.Keybind):
return list.ActionScrollBottom
}
return list.ActionNone
}
OnChange(func(a viewport.Change) tview.Msg { return scrollMsg(a) })
}
func (m Model) cursor() int {
return m.selectionState.Cursor()
}
// setCursor selects the item at index and scrolls to it if it changed, or selects none if it is negative.
func (m *Model) setCursor(index int) {
if index >= 0 && index != m.cursor() {
m.scrollState.ScrollToTarget()
}
m.selectionState.SetCursor(index)
}
@ -150,7 +143,7 @@ func (m *Model) SetChannel(channel *discord.Channel, messages []discord.Message)
// ShowNewest clears the selection and scrolls to the newest message, which then stays in view as messages arrive until the user scrolls up.
func (m *Model) ShowNewest() {
m.clearSelection()
m.selectionState.ScrollToEnd()
m.scrollState.ScrollToEnd()
}
// UpdateMessage replaces the shown message with the same ID.
@ -182,7 +175,8 @@ func (m Model) indexOf(channelID discord.ChannelID, id discord.MessageID) int {
func (m *Model) reset() {
m.items = nil
m.selectionState = list.NewSelectionState()
m.selectionState.ScrollToEnd()
m.scrollState = viewport.ScrollState{}
m.scrollState.ScrollToEnd()
}
func (m *Model) setMessages(messages []discord.Message) {
@ -928,6 +922,8 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
// WezTerm and Konsole support kitty graphics but not Unicode placeholders.
m.kitty = msg.Name == "kitty" || msg.Name == "ghostty"
}
case scrollMsg:
m.scrollState.Apply(viewport.Change(msg))
case listMsg:
top := m.atTop()
m.selectionState.Apply(list.Change(msg))

View file

@ -16,6 +16,7 @@ import (
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/list"
"github.com/ayn2op/tview/picker"
"github.com/ayn2op/tview/viewport"
"github.com/ncruces/zenity"
"github.com/skratchdot/open-golang/open"
"golang.design/x/clipboard"
@ -44,9 +45,12 @@ type EditMsg discord.Message
// ShowAttachmentsMsg asks to pick one of the attachments and links of the selected message.
type ShowAttachmentsMsg picker.Items
// listMsg moves or scrolls the messages list.
// listMsg moves the messages list.
type listMsg list.Change
// scrollMsg scrolls the messages list.
type scrollMsg viewport.Change
func (m *Model) selectUp() tview.Cmd {
cursor := m.cursor()
if cursor == -1 {

View file

@ -5,6 +5,7 @@ import (
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/keybind"
"github.com/ayn2op/tview/list"
"github.com/ayn2op/tview/viewport"
)
// SelectionKeybind binds kbs to moving a list's selection.
@ -23,3 +24,20 @@ func SelectionKeybind(kbs config.SelectionKeybinds) func(tview.KeyMsg) list.Acti
return list.ActionNone
}
}
// ScrollKeybind binds kbs to scrolling a viewport.
func ScrollKeybind(kbs config.ScrollKeybinds) func(tview.KeyMsg) viewport.Action {
return func(key tview.KeyMsg) viewport.Action {
switch {
case keybind.Matches(key, kbs.ScrollUp.Keybind):
return viewport.ActionUp
case keybind.Matches(key, kbs.ScrollDown.Keybind):
return viewport.ActionDown
case keybind.Matches(key, kbs.ScrollTop.Keybind):
return viewport.ActionTop
case keybind.Matches(key, kbs.ScrollBottom.Keybind):
return viewport.ActionBottom
}
return viewport.ActionNone
}
}

View file

@ -10,12 +10,13 @@ import (
"github.com/ayn2op/tview/center"
"github.com/ayn2op/tview/richtext"
"github.com/ayn2op/tview/textview"
"github.com/ayn2op/tview/viewport"
"github.com/gdamore/tcell/v3"
"github.com/skip2/go-qrcode"
)
type Model struct {
scrollState textview.ScrollState
scrollState viewport.ScrollState
conn *gatewayConn
heartbeatInterval time.Duration
@ -55,7 +56,7 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) {
}
return m, nil
case scrollMsg:
m.scrollState.Apply(textview.Change(msg))
m.scrollState.Apply(viewport.Change(msg))
return m, nil
case connCreateMsg:
@ -131,13 +132,10 @@ func halfBlock(top, bottom bool) rune {
func (m Model) View() tview.Widget {
code := m.code()
return center.New(
textview.New(code).
ScrollState(&m.scrollState).
Wrap(false).
Alignment(tview.AlignmentCenter).
viewport.New(textview.New(code).Wrap(false).Alignment(tview.AlignmentCenter), m.scrollState).
Height(layout.Fixed(len(code))).
Focused(true).
OnChange(func(a textview.Change) tview.Msg { return scrollMsg(a) }),
OnChange(func(a viewport.Change) tview.Msg { return scrollMsg(a) }),
)
}

View file

@ -18,7 +18,7 @@ import (
"github.com/ayn2op/discordo/internal/gateway"
"github.com/ayn2op/discordo/internal/http"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/textview"
"github.com/ayn2op/tview/viewport"
"github.com/gorilla/websocket"
"github.com/skip2/go-qrcode"
)
@ -302,7 +302,7 @@ func exchangeTicket(fingerprint string, privateKey *rsa.PrivateKey, ticket strin
}
// scrollMsg scrolls the code.
type scrollMsg textview.Change
type scrollMsg viewport.Change
// startMsg starts connecting to the Remote Auth Gateway.
type startMsg struct{}

View file

@ -7,12 +7,20 @@ import (
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/box"
"github.com/ayn2op/tview/keybind"
"github.com/ayn2op/tview/layout"
"github.com/ayn2op/tview/tree"
"github.com/ayn2op/tview/viewport"
"github.com/gdamore/tcell/v3"
)
// New returns a boxed tree of the nodes under root, drawn with theme and graphics and moved with kbs, that turns changes and selected nodes into the messages onChange and onSelect return.
func New(root *tree.Node, selectionState tree.SelectionState, cfg *config.Config, theme config.TreeThemeConfig, graphics bool, kbs config.TreeKeybinds, focused bool, onChange func(tree.Change) tview.Msg, onSelect func(*tree.Node) tview.Msg) box.Widget {
// ScrollMsg scrolls the tree, before passing on Msg if the tree sent one.
type ScrollMsg struct {
Change viewport.Change
Msg tview.Msg
}
// New returns a boxed tree of the nodes under root in a viewport.
func New(root *tree.Node, selectionState tree.SelectionState, scrollState viewport.ScrollState, cfg *config.Config, theme config.TreeThemeConfig, graphics bool, kbs config.TreeKeybinds, focused bool, onChange func(tree.Change) tview.Msg, onSelect func(*tree.Node) tview.Msg) box.Widget {
set := cfg.UI.Border.NormalSet.BorderSet
if focused {
set = cfg.UI.Border.ActiveSet.BorderSet
@ -31,7 +39,15 @@ func New(root *tree.Node, selectionState tree.SelectionState, cfg *config.Config
Focused(focused).
OnChange(onChange).
OnSelect(onSelect)
return ui.Box(t, cfg, focused)
v := viewport.New(t, scrollState).
Target(t.Target).
ContentWidth(t.RowsWidth).
OnChildMsg(func(c viewport.Change, msg tview.Msg) tview.Msg { return ScrollMsg{Change: c, Msg: msg} }).
Axes(layout.Axes{Width: true, Height: true}).
Keybind(bindScrollKeys(kbs)).
Focused(focused).
OnChange(func(c viewport.Change) tview.Msg { return ScrollMsg{Change: c} })
return ui.Box(v, cfg, focused)
}
func bindKeys(kbs config.TreeKeybinds) func(tview.KeyMsg) tree.Action {
@ -49,19 +65,20 @@ func bindKeys(kbs config.TreeKeybinds) func(tview.KeyMsg) tree.Action {
return tree.ActionMoveToParent
case keybind.Matches(key, kbs.SelectCurrent.Keybind):
return tree.ActionSelect
case keybind.Matches(key, kbs.ScrollUp.Keybind):
return tree.ActionScrollUp
case keybind.Matches(key, kbs.ScrollDown.Keybind):
return tree.ActionScrollDown
case keybind.Matches(key, kbs.ScrollTop.Keybind):
return tree.ActionScrollTop
case keybind.Matches(key, kbs.ScrollBottom.Keybind):
return tree.ActionScrollBottom
case keybind.Matches(key, kbs.ScrollLeft.Keybind):
return tree.ActionScrollLeft
case keybind.Matches(key, kbs.ScrollRight.Keybind):
return tree.ActionScrollRight
}
return tree.ActionNone
}
}
func bindScrollKeys(kbs config.TreeKeybinds) func(tview.KeyMsg) viewport.Action {
scroll := ui.ScrollKeybind(kbs.ScrollKeybinds)
return func(key tview.KeyMsg) viewport.Action {
switch {
case keybind.Matches(key, kbs.ScrollLeft.Keybind):
return viewport.ActionLeft
case keybind.Matches(key, kbs.ScrollRight.Keybind):
return viewport.ActionRight
}
return scroll(key)
}
}