Skip to content

Commit 7c44f63

Browse files
v1: fix(list): ensure correct cursor positions with page/cursor methods (#831)
* fix(list): misc cursor position fixes Signed-off-by: Liam Stanley <liam@liam.sh> * chore(list): add low/high check in clamp --------- Signed-off-by: Liam Stanley <liam@liam.sh> Co-authored-by: Christian Rocha <christian@rocha.is>
1 parent 4b2d311 commit 7c44f63

File tree

1 file changed

+45
-44
lines changed

1 file changed

+45
-44
lines changed

list/list.go

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package list
55

66
import (
7+
"cmp"
78
"fmt"
89
"io"
910
"sort"
@@ -22,6 +23,13 @@ import (
2223
"github.com/charmbracelet/bubbles/textinput"
2324
)
2425

26+
func clamp[T cmp.Ordered](v, low, high T) T {
27+
if low > high {
28+
low, high = high, low
29+
}
30+
return min(high, max(low, v))
31+
}
32+
2533
// Item is an item that appears in the list.
2634
type Item interface {
2735
// FilterValue is the value we use when filtering against this item when
@@ -282,17 +290,15 @@ func (m *Model) SetFilterText(filter string) {
282290
fmm, _ := msg.(FilterMatchesMsg)
283291
m.filteredItems = filteredItems(fmm)
284292
m.filterState = FilterApplied
285-
m.Paginator.Page = 0
286-
m.cursor = 0
293+
m.GoToStart()
287294
m.FilterInput.CursorEnd()
288295
m.updatePagination()
289296
m.updateKeybindings()
290297
}
291298

292299
// SetFilterState allows setting the filtering state manually.
293300
func (m *Model) SetFilterState(state FilterState) {
294-
m.Paginator.Page = 0
295-
m.cursor = 0
301+
m.GoToStart()
296302
m.filterState = state
297303
m.FilterInput.CursorEnd()
298304
m.FilterInput.Focus()
@@ -516,14 +522,12 @@ func (m *Model) CursorUp() {
516522
m.cursor--
517523

518524
// If we're at the start, stop
519-
if m.cursor < 0 && m.Paginator.Page == 0 {
525+
if m.cursor < 0 && m.Paginator.OnFirstPage() {
520526
// if infinite scrolling is enabled, go to the last item
521527
if m.InfiniteScrolling {
522-
m.Paginator.Page = m.Paginator.TotalPages - 1
523-
m.cursor = m.Paginator.ItemsOnPage(len(m.VisibleItems())) - 1
528+
m.GoToEnd()
524529
return
525530
}
526-
527531
m.cursor = 0
528532
return
529533
}
@@ -535,18 +539,18 @@ func (m *Model) CursorUp() {
535539

536540
// Go to the previous page
537541
m.Paginator.PrevPage()
538-
m.cursor = m.Paginator.ItemsOnPage(len(m.VisibleItems())) - 1
542+
m.cursor = m.maxCursorIndex()
539543
}
540544

541545
// CursorDown moves the cursor down. This can also advance the state to the
542546
// next page.
543547
func (m *Model) CursorDown() {
544-
itemsOnPage := m.Paginator.ItemsOnPage(len(m.VisibleItems()))
548+
maxCursorIndex := m.maxCursorIndex()
545549

546550
m.cursor++
547551

548-
// If we're at the end, stop
549-
if m.cursor < itemsOnPage {
552+
// We're still within bounds of the current page, so no need to do anything.
553+
if m.cursor <= maxCursorIndex {
550554
return
551555
}
552556

@@ -557,31 +561,40 @@ func (m *Model) CursorDown() {
557561
return
558562
}
559563

560-
// During filtering the cursor position can exceed the number of
561-
// itemsOnPage. It's more intuitive to start the cursor at the
562-
// topmost position when moving it down in this scenario.
563-
if m.cursor > itemsOnPage {
564-
m.cursor = 0
565-
return
566-
}
564+
m.cursor = max(0, maxCursorIndex)
567565

568-
m.cursor = itemsOnPage - 1
569-
570-
// if infinite scrolling is enabled, go to the first item
566+
// if infinite scrolling is enabled, go to the first item.
571567
if m.InfiniteScrolling {
572-
m.Paginator.Page = 0
573-
m.cursor = 0
568+
m.GoToStart()
574569
}
575570
}
576571

572+
// GoToStart moves to the first page, and first item on the first page.
573+
func (m *Model) GoToStart() {
574+
m.Paginator.Page = 0
575+
m.cursor = 0
576+
}
577+
578+
// GoToEnd moves to the last page, and last item on the last page.
579+
func (m *Model) GoToEnd() {
580+
m.Paginator.Page = max(0, m.Paginator.TotalPages-1)
581+
m.cursor = m.maxCursorIndex()
582+
}
583+
577584
// PrevPage moves to the previous page, if available.
578585
func (m *Model) PrevPage() {
579586
m.Paginator.PrevPage()
587+
m.cursor = clamp(m.cursor, 0, m.maxCursorIndex())
580588
}
581589

582590
// NextPage moves to the next page, if available.
583591
func (m *Model) NextPage() {
584592
m.Paginator.NextPage()
593+
m.cursor = clamp(m.cursor, 0, m.maxCursorIndex())
594+
}
595+
596+
func (m *Model) maxCursorIndex() int {
597+
return max(0, m.Paginator.ItemsOnPage(len(m.VisibleItems()))-1)
585598
}
586599

587600
// FilterState returns the current filter state.
@@ -673,22 +686,18 @@ func (m *Model) NewStatusMessage(s string) tea.Cmd {
673686
}
674687
}
675688

676-
// SetSize sets the width and height of this component.
677-
func (m *Model) SetSize(width, height int) {
678-
m.setSize(width, height)
679-
}
680-
681689
// SetWidth sets the width of this component.
682690
func (m *Model) SetWidth(v int) {
683-
m.setSize(v, m.height)
691+
m.SetSize(v, m.height)
684692
}
685693

686694
// SetHeight sets the height of this component.
687695
func (m *Model) SetHeight(v int) {
688-
m.setSize(m.width, v)
696+
m.SetSize(m.width, v)
689697
}
690698

691-
func (m *Model) setSize(width, height int) {
699+
// SetSize sets the width and height of this component.
700+
func (m *Model) SetSize(width, height int) {
692701
promptWidth := lipgloss.Width(m.Styles.Title.Render(m.FilterInput.Prompt))
693702

694703
m.width = width
@@ -848,7 +857,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
848857
// Updates for when a user is browsing the list.
849858
func (m *Model) handleBrowsing(msg tea.Msg) tea.Cmd {
850859
var cmds []tea.Cmd
851-
numItems := len(m.VisibleItems())
852860

853861
switch msg := msg.(type) {
854862
case tea.KeyMsg:
@@ -874,21 +882,18 @@ func (m *Model) handleBrowsing(msg tea.Msg) tea.Cmd {
874882
m.Paginator.NextPage()
875883

876884
case key.Matches(msg, m.KeyMap.GoToStart):
877-
m.Paginator.Page = 0
878-
m.cursor = 0
885+
m.GoToStart()
879886

880887
case key.Matches(msg, m.KeyMap.GoToEnd):
881-
m.Paginator.Page = m.Paginator.TotalPages - 1
882-
m.cursor = m.Paginator.ItemsOnPage(numItems) - 1
888+
m.GoToEnd()
883889

884890
case key.Matches(msg, m.KeyMap.Filter):
885891
m.hideStatusMessage()
886892
if m.FilterInput.Value() == "" {
887893
// Populate filter with all items only if the filter is empty.
888894
m.filteredItems = m.itemsAsFilterItems()
889895
}
890-
m.Paginator.Page = 0
891-
m.cursor = 0
896+
m.GoToStart()
892897
m.filterState = Filtering
893898
m.FilterInput.CursorEnd()
894899
m.FilterInput.Focus()
@@ -906,11 +911,7 @@ func (m *Model) handleBrowsing(msg tea.Msg) tea.Cmd {
906911
cmd := m.delegate.Update(msg, m)
907912
cmds = append(cmds, cmd)
908913

909-
// Keep the index in bounds when paginating
910-
itemsOnPage := m.Paginator.ItemsOnPage(len(m.VisibleItems()))
911-
if m.cursor > itemsOnPage-1 {
912-
m.cursor = max(0, itemsOnPage-1)
913-
}
914+
m.cursor = clamp(m.cursor, 0, m.maxCursorIndex())
914915

915916
return tea.Batch(cmds...)
916917
}

0 commit comments

Comments
 (0)