@@ -50,7 +50,10 @@ type ItemDelegate interface {
5050 // Height is the height of the list item.
5151 Height () int
5252
53- // Spacing is the size of the horizontal gap between list items in cells.
53+ // Width is the width of the list item.
54+ Width () int
55+
56+ // Spacing is the size of the vertical (1:1 ratio) and horizontal (1:2 ratio) gap between list items in cells.
5457 Spacing () int
5558
5659 // Update is the update loop for items. All messages in the list's update
@@ -145,12 +148,13 @@ func (f FilterState) String() string {
145148
146149// Model contains the state of this component.
147150type Model struct {
148- showTitle bool
149- showFilter bool
150- showStatusBar bool
151- showPagination bool
152- showHelp bool
153- filteringEnabled bool
151+ showTitle bool
152+ showFilter bool
153+ showStatusBar bool
154+ showPagination bool
155+ showHelp bool
156+ horizontalEnabled bool
157+ filteringEnabled bool
154158
155159 itemNameSingular string
156160 itemNamePlural string
@@ -232,6 +236,7 @@ func New(items []Item, delegate ItemDelegate, width, height int) Model {
232236 itemNameSingular : "item" ,
233237 itemNamePlural : "items" ,
234238 filteringEnabled : true ,
239+ horizontalEnabled : false ,
235240 KeyMap : DefaultKeyMap (),
236241 Filter : DefaultFilter ,
237242 Styles : styles ,
@@ -273,6 +278,19 @@ func (m Model) FilteringEnabled() bool {
273278 return m .filteringEnabled
274279}
275280
281+ // SetHorizontalEnabled enables or disables horizontal.
282+ func (m * Model ) SetHorizontalEnabled (v bool ) {
283+ m .horizontalEnabled = v
284+
285+ m .updatePagination ()
286+ m .updateKeybindings ()
287+ }
288+
289+ // HorizontalEnabled returns whether or not horizontal is enabled.
290+ func (m Model ) HorizontalEnabled () bool {
291+ return m .horizontalEnabled
292+ }
293+
276294// SetShowTitle shows or hides the title bar.
277295func (m * Model ) SetShowTitle (v bool ) {
278296 m .showTitle = v
@@ -519,7 +537,11 @@ func (m Model) Cursor() int {
519537// CursorUp moves the cursor up. This can also move the state to the previous
520538// page.
521539func (m * Model ) CursorUp () {
522- m .cursor --
540+ if m .horizontalEnabled {
541+ m .cursor -= m .columnsPerPage ()
542+ } else {
543+ m .cursor --
544+ }
523545
524546 // If we're at the start, stop
525547 if m .cursor < 0 && m .Paginator .OnFirstPage () {
@@ -547,7 +569,11 @@ func (m *Model) CursorUp() {
547569func (m * Model ) CursorDown () {
548570 maxCursorIndex := m .maxCursorIndex ()
549571
550- m .cursor ++
572+ if m .horizontalEnabled {
573+ m .cursor += m .columnsPerPage ()
574+ } else {
575+ m .cursor ++
576+ }
551577
552578 // We're still within bounds of the current page, so no need to do anything.
553579 if m .cursor <= maxCursorIndex {
@@ -569,6 +595,78 @@ func (m *Model) CursorDown() {
569595 }
570596}
571597
598+ // CursorLeft moves the cursor to the left. This can also move the state to the previous
599+ // page.
600+ func (m * Model ) CursorLeft () {
601+ if ! m .horizontalEnabled {
602+ return
603+ }
604+
605+ m .cursor --
606+
607+ // If we're at the start, stop
608+ if m .cursor < 0 && m .Paginator .Page == 0 {
609+ // if infinite scrolling is enabled, go to the last item
610+ if m .InfiniteScrolling {
611+ m .Paginator .Page = m .Paginator .TotalPages - 1
612+ m .cursor = m .Paginator .ItemsOnPage (len (m .VisibleItems ())) - 1
613+ return
614+ }
615+
616+ m .cursor = 0
617+ return
618+ }
619+
620+ // Move the cursor as normal
621+ if m .cursor >= 0 {
622+ return
623+ }
624+
625+ // Go to the previous page
626+ m .Paginator .PrevPage ()
627+ m .cursor = m .Paginator .ItemsOnPage (len (m .VisibleItems ())) - 1
628+ }
629+
630+ // CursorRight moves the cursor to the right. This can also advance the state to the
631+ // next page.
632+ func (m * Model ) CursorRight () {
633+ if ! m .horizontalEnabled {
634+ return
635+ }
636+
637+ itemsOnPage := m .Paginator .ItemsOnPage (len (m .VisibleItems ()))
638+
639+ m .cursor ++
640+
641+ // If we're at the end, stop
642+ if m .cursor < itemsOnPage {
643+ return
644+ }
645+
646+ // Go to the next page
647+ if ! m .Paginator .OnLastPage () {
648+ m .Paginator .NextPage ()
649+ m .cursor = 0
650+ return
651+ }
652+
653+ // During filtering the cursor position can exceed the number of
654+ // itemsOnPage. It's more intuitive to start the cursor at the
655+ // topmost position when moving it down in this scenario.
656+ if m .cursor > itemsOnPage {
657+ m .cursor = 0
658+ return
659+ }
660+
661+ m .cursor = itemsOnPage - 1
662+
663+ // if infinite scrolling is enabled, go to the first item
664+ if m .InfiniteScrolling {
665+ m .Paginator .Page = 0
666+ m .cursor = 0
667+ }
668+ }
669+
572670// GoToStart moves to the first page, and first item on the first page.
573671func (m * Model ) GoToStart () {
574672 m .Paginator .Page = 0
@@ -736,6 +834,12 @@ func (m *Model) updateKeybindings() {
736834 case Filtering :
737835 m .KeyMap .CursorUp .SetEnabled (false )
738836 m .KeyMap .CursorDown .SetEnabled (false )
837+
838+ if m .horizontalEnabled {
839+ m .KeyMap .CursorLeft .SetEnabled (false )
840+ m .KeyMap .CursorRight .SetEnabled (false )
841+ }
842+
739843 m .KeyMap .NextPage .SetEnabled (false )
740844 m .KeyMap .PrevPage .SetEnabled (false )
741845 m .KeyMap .GoToStart .SetEnabled (false )
@@ -753,6 +857,11 @@ func (m *Model) updateKeybindings() {
753857 m .KeyMap .CursorUp .SetEnabled (hasItems )
754858 m .KeyMap .CursorDown .SetEnabled (hasItems )
755859
860+ if m .horizontalEnabled {
861+ m .KeyMap .CursorLeft .SetEnabled (hasItems )
862+ m .KeyMap .CursorRight .SetEnabled (hasItems )
863+ }
864+
756865 hasPages := m .Paginator .TotalPages > 1
757866 m .KeyMap .NextPage .SetEnabled (hasPages )
758867 m .KeyMap .PrevPage .SetEnabled (hasPages )
@@ -781,6 +890,7 @@ func (m *Model) updateKeybindings() {
781890func (m * Model ) updatePagination () {
782891 index := m .Index ()
783892 availHeight := m .height
893+ availWidth := m .width
784894
785895 if m .showTitle || (m .showFilter && m .filteringEnabled ) {
786896 availHeight -= lipgloss .Height (m .titleView ())
@@ -795,7 +905,14 @@ func (m *Model) updatePagination() {
795905 availHeight -= lipgloss .Height (m .helpView ())
796906 }
797907
798- m .Paginator .PerPage = max (1 , availHeight / (m .delegate .Height ()+ m .delegate .Spacing ()))
908+ if m .horizontalEnabled {
909+ availRows := availHeight / (m .delegate .Height () + m .delegate .Spacing ())
910+ availColumns := availWidth / (m .delegate .Width () + (m .delegate .Spacing () * 2 ))
911+
912+ m .Paginator .PerPage = max (1 , availRows * availColumns )
913+ } else {
914+ m .Paginator .PerPage = max (1 , availHeight / (m .delegate .Height ()+ m .delegate .Spacing ()))
915+ }
799916
800917 if pages := len (m .VisibleItems ()); pages < 1 {
801918 m .Paginator .SetTotalPages (1 )
@@ -875,6 +992,12 @@ func (m *Model) handleBrowsing(msg tea.Msg) tea.Cmd {
875992 case key .Matches (msg , m .KeyMap .CursorDown ):
876993 m .CursorDown ()
877994
995+ case key .Matches (msg , m .KeyMap .CursorLeft ):
996+ m .CursorLeft ()
997+
998+ case key .Matches (msg , m .KeyMap .CursorRight ):
999+ m .CursorRight ()
1000+
8781001 case key .Matches (msg , m .KeyMap .PrevPage ):
8791002 m .Paginator .PrevPage ()
8801003
@@ -979,6 +1102,10 @@ func (m Model) ShortHelp() []key.Binding {
9791102 m .KeyMap .CursorDown ,
9801103 }
9811104
1105+ if m .horizontalEnabled {
1106+ kb = append (kb , m .KeyMap .CursorLeft , m .KeyMap .CursorRight )
1107+ }
1108+
9821109 filtering := m .filterState == Filtering
9831110
9841111 // If the delegate implements the help.KeyMap interface add the short help
@@ -1018,6 +1145,10 @@ func (m Model) FullHelp() [][]key.Binding {
10181145 m .KeyMap .GoToEnd ,
10191146 }}
10201147
1148+ if m .horizontalEnabled {
1149+ kb = append (kb , [][]key.Binding {{m .KeyMap .CursorLeft , m .KeyMap .CursorRight }}... )
1150+ }
1151+
10211152 filtering := m .filterState == Filtering
10221153
10231154 // If the delegate implements the help.KeyMap interface add full help
@@ -1052,6 +1183,7 @@ func (m Model) View() string {
10521183 var (
10531184 sections []string
10541185 availHeight = m .height
1186+ availWidth = m .width
10551187 )
10561188
10571189 if m .showTitle || (m .showFilter && m .filteringEnabled ) {
@@ -1078,7 +1210,7 @@ func (m Model) View() string {
10781210 availHeight -= lipgloss .Height (help )
10791211 }
10801212
1081- content := lipgloss .NewStyle ().Height (availHeight ).Render (m .populatedView ())
1213+ content := lipgloss .NewStyle ().Height (availHeight ).Width ( availWidth ). Render (m .populatedView ())
10821214 sections = append (sections , content )
10831215
10841216 if m .showPagination {
@@ -1209,6 +1341,16 @@ func (m Model) paginationView() string {
12091341 return style .Render (s )
12101342}
12111343
1344+ // rowsPerPage returns the amount of rows that fits into current height.
1345+ func (m Model ) rowsPerPage () int {
1346+ return m .height / (m .delegate .Height () + m .delegate .Spacing ())
1347+ }
1348+
1349+ // columnsPerPage returns the amount of columns that fits into current width.
1350+ func (m Model ) columnsPerPage () int {
1351+ return m .width / (m .delegate .Width () + (m .delegate .Spacing () * 2 ))
1352+ }
1353+
12121354func (m Model ) populatedView () string {
12131355 items := m .VisibleItems ()
12141356
@@ -1224,26 +1366,67 @@ func (m Model) populatedView() string {
12241366
12251367 if len (items ) > 0 {
12261368 start , end := m .Paginator .GetSliceBounds (len (items ))
1227- docs := items [start :end ]
1369+ itms := items [start :end ]
12281370
1229- for i , item := range docs {
1230- m .delegate .Render (& b , m , i + start , item )
1231- if i != len (docs )- 1 {
1232- fmt .Fprint (& b , strings .Repeat ("\n " , m .delegate .Spacing ()+ 1 ))
1371+ if m .horizontalEnabled {
1372+ rowsPerPage := m .rowsPerPage ()
1373+ columnsPerPage := m .columnsPerPage ()
1374+
1375+ var br strings.Builder
1376+
1377+ i := 0
1378+
1379+ for range rowsPerPage {
1380+ var r string
1381+
1382+ for range columnsPerPage {
1383+ br .Reset ()
1384+
1385+ // handle last page
1386+ if len (itms ) < rowsPerPage * columnsPerPage {
1387+ if i < len (itms ) {
1388+ m .delegate .Render (& br , m , i + start , itms [i ])
1389+ } else {
1390+ fmt .Fprint (& br , " " )
1391+ }
1392+ } else {
1393+ // render items
1394+ m .delegate .Render (& br , m , i + start , itms [i ])
1395+ }
1396+
1397+ if i % columnsPerPage == 0 {
1398+ r = lipgloss .JoinHorizontal (lipgloss .Left , r , br .String ())
1399+ } else {
1400+ r = lipgloss .JoinHorizontal (lipgloss .Left , r , strings .Repeat (" " , m .delegate .Spacing ()* 2 ), br .String ())
1401+ }
1402+
1403+ i ++
1404+ }
1405+
1406+ fmt .Fprint (& b , r , "\n " )
1407+ }
1408+ } else {
1409+ for i , item := range itms {
1410+ m .delegate .Render (& b , m , i + start , item )
1411+ if i != len (itms )- 1 {
1412+ fmt .Fprint (& b , strings .Repeat ("\n " , m .delegate .Spacing ()+ 1 ))
1413+ }
12331414 }
12341415 }
12351416 }
12361417
1237- // If there aren't enough items to fill up this page (always the last page)
1238- // then we need to add some newlines to fill up the space where items would
1239- // have been.
1240- itemsOnPage := m .Paginator .ItemsOnPage (len (items ))
1241- if itemsOnPage < m .Paginator .PerPage {
1242- n := (m .Paginator .PerPage - itemsOnPage ) * (m .delegate .Height () + m .delegate .Spacing ())
1243- if len (items ) == 0 {
1244- n -= m .delegate .Height () - 1
1418+ if ! m .horizontalEnabled {
1419+ // If there aren't enough items to fill up this page (always the last page)
1420+ // then we need to add some newlines to fill up the space where items would
1421+ // have been.
1422+ itemsOnPage := m .Paginator .ItemsOnPage (len (items ))
1423+ if itemsOnPage < m .Paginator .PerPage {
1424+ n := (m .Paginator .PerPage - itemsOnPage ) * (m .delegate .Height () + m .delegate .Spacing ())
1425+ if len (items ) == 0 {
1426+ n -= m .delegate .Height () - 1
1427+ }
1428+ fmt .Fprint (& b , strings .Repeat ("\n " , n ))
12451429 }
1246- fmt .Fprint (& b , strings .Repeat ("\n " , n ))
12471430 }
12481431
12491432 return b .String ()
0 commit comments