Skip to content

Commit 25da682

Browse files
authored
Add horizontal freeze (#87)
1 parent 2359e82 commit 25da682

File tree

8 files changed

+117
-33
lines changed

8 files changed

+117
-33
lines changed

examples/scrolling/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
This example shows how to use scrolling to navigate particularly large tables
44
that may not fit nicely onto the screen.
55

6-
<img width="423" alt="image" src="https://user-images.githubusercontent.com/5923958/171999237-fea16b62-c378-4996-8143-501452e3ab90.png">
6+
<img width="423" alt="image" src="https://user-images.githubusercontent.com/5923958/172004548-7052993e-9e60-44a4-b9b2-b49506c48fb6.png">

examples/scrolling/main.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,14 @@ func NewModel() Model {
5151
cols = append(cols, table.NewColumn(colKey(i), colKey(i+1), 5))
5252
}
5353

54+
t := table.New(cols).
55+
WithRows(rows).
56+
WithMaxTotalWidth(30).
57+
WithHorizontalFreezeColumnCount(1).
58+
Focused(true)
59+
5460
return Model{
55-
scrollableTable: table.New(cols).WithRows(rows).WithMaxTotalWidth(30).Focused(true),
61+
scrollableTable: t,
5662
}
5763
}
5864

table/header.go

+16-11
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import "github.com/charmbracelet/lipgloss"
44

55
// This is long and could use some refactoring in the future, but unsure of how
66
// to pick it apart right now.
7-
// nolint: funlen
7+
// nolint: funlen, cyclop
88
func (m Model) renderHeaders() string {
99
headerStrings := []string{}
1010

@@ -20,22 +20,27 @@ func (m Model) renderHeaders() string {
2020
return borderStyle.Render(headerSection)
2121
}
2222

23-
if m.horizontalScrollOffsetCol > 0 {
24-
borderStyle := headerStyles.left.Copy()
23+
for columnIndex, column := range m.columns {
24+
var borderStyle lipgloss.Style
2525

26-
rendered := renderHeader(genOverflowColumnLeft(1), borderStyle)
26+
if m.horizontalScrollOffsetCol > 0 && columnIndex == m.horizontalScrollFreezeColumnsCount {
27+
if columnIndex == 0 {
28+
borderStyle = headerStyles.left.Copy()
29+
} else {
30+
borderStyle = headerStyles.inner.Copy()
31+
}
2732

28-
totalRenderedWidth += lipgloss.Width(rendered)
33+
rendered := renderHeader(genOverflowColumnLeft(1), borderStyle)
2934

30-
headerStrings = append(headerStrings, rendered)
31-
}
35+
totalRenderedWidth += lipgloss.Width(rendered)
3236

33-
for columnIndex, column := range m.columns {
34-
if columnIndex < m.horizontalScrollOffsetCol {
35-
continue
37+
headerStrings = append(headerStrings, rendered)
3638
}
3739

38-
var borderStyle lipgloss.Style
40+
if columnIndex >= m.horizontalScrollFreezeColumnsCount &&
41+
columnIndex < m.horizontalScrollOffsetCol+m.horizontalScrollFreezeColumnsCount {
42+
continue
43+
}
3944

4045
if len(headerStrings) == 0 {
4146
borderStyle = headerStyles.left.Copy()

table/model.go

+3
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ type Model struct {
6363

6464
// How far to scroll to the right, in columns
6565
horizontalScrollOffsetCol int
66+
67+
// How many columns to freeze when scrolling horizontally
68+
horizontalScrollFreezeColumnsCount int
6669
}
6770

6871
// New creates a new table ready for further modifications.

table/options.go

+9
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,12 @@ func (m Model) WithMaxTotalWidth(maxTotalWidth int) Model {
277277

278278
return m
279279
}
280+
281+
// WithHorizontalFreezeColumnCount freezes the given number of columns to the
282+
// left side. This is useful for things like ID or Name columns that should
283+
// always be visible even when scrolling.
284+
func (m Model) WithHorizontalFreezeColumnCount(columnsToFreeze int) Model {
285+
m.horizontalScrollFreezeColumnsCount = columnsToFreeze
286+
287+
return m
288+
}

table/row.go

+19-19
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func (m Model) renderRowColumnData(row Row, column Column, rowStyle lipgloss.Sty
7777

7878
// This is long and could use some refactoring in the future, but not quite sure
7979
// how to pick it apart yet.
80-
// nolint: funlen, cyclop
80+
// nolint: funlen, cyclop, gocognit
8181
func (m Model) renderRow(rowIndex int, last bool) string {
8282
numColumns := len(m.columns)
8383
row := m.GetVisibleRows()[rowIndex]
@@ -94,35 +94,35 @@ func (m Model) renderRow(rowIndex int, last bool) string {
9494

9595
stylesInner, stylesLast := m.styleRows()
9696

97-
if m.horizontalScrollOffsetCol > 0 {
97+
for columnIndex, column := range m.columns {
9898
var borderStyle lipgloss.Style
99+
var rowStyles borderStyleRow
99100

100101
if !last {
101-
borderStyle = stylesInner.left.Copy()
102+
rowStyles = stylesInner
102103
} else {
103-
borderStyle = stylesLast.left.Copy()
104+
rowStyles = stylesLast
104105
}
105106

106-
rendered := m.renderRowColumnData(row, genOverflowColumnLeft(1), rowStyle, borderStyle)
107-
108-
totalRenderedWidth += lipgloss.Width(rendered)
107+
if m.horizontalScrollOffsetCol > 0 && columnIndex == m.horizontalScrollFreezeColumnsCount {
108+
var borderStyle lipgloss.Style
109109

110-
columnStrings = append(columnStrings, rendered)
111-
}
110+
if columnIndex == 0 {
111+
borderStyle = rowStyles.left.Copy()
112+
} else {
113+
borderStyle = rowStyles.inner.Copy()
114+
}
112115

113-
for columnIndex, column := range m.columns {
114-
if columnIndex < m.horizontalScrollOffsetCol {
115-
continue
116-
}
116+
rendered := m.renderRowColumnData(row, genOverflowColumnLeft(1), rowStyle, borderStyle)
117117

118-
var borderStyle lipgloss.Style
118+
totalRenderedWidth += lipgloss.Width(rendered)
119119

120-
var rowStyles borderStyleRow
120+
columnStrings = append(columnStrings, rendered)
121+
}
121122

122-
if !last {
123-
rowStyles = stylesInner
124-
} else {
125-
rowStyles = stylesLast
123+
if columnIndex >= m.horizontalScrollFreezeColumnsCount &&
124+
columnIndex < m.horizontalScrollOffsetCol+m.horizontalScrollFreezeColumnsCount {
125+
continue
126126
}
127127

128128
if len(columnStrings) == 0 {

table/scrolling.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package table
22

33
func (m *Model) scrollRight() {
4-
if m.horizontalScrollOffsetCol < len(m.columns)-1 {
4+
maxCol := len(m.columns) - 1 - m.horizontalScrollFreezeColumnsCount
5+
if m.horizontalScrollOffsetCol < maxCol {
56
m.horizontalScrollOffsetCol++
67
}
78
}

table/update_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,63 @@ func TestScrollRightWithFooter(t *testing.T) {
433433
hitScrollLeft()
434434
assert.Equal(t, expectedTableOriginal, model.View())
435435
}
436+
437+
func TestScrollRightWithFooterAndFrozenCols(t *testing.T) {
438+
model := New([]Column{
439+
NewColumn("Name", "Name", 4),
440+
NewColumn("1", "1", 4),
441+
NewColumn("2", "2", 4),
442+
NewColumn("3", "3", 4),
443+
NewColumn("4", "4", 4),
444+
}).
445+
WithRows([]Row{
446+
NewRow(RowData{
447+
"Name": "A",
448+
"1": "x1",
449+
"2": "x2",
450+
"3": "x3",
451+
"4": "x4",
452+
}),
453+
}).
454+
WithStaticFooter("Footer").
455+
WithMaxTotalWidth(21).
456+
WithHorizontalFreezeColumnCount(1).
457+
Focused(true)
458+
459+
const expectedTableOriginal = `┏━━━━┳━━━━┳━━━━┳━━━━┓
460+
┃Name┃ 1┃ 2┃ >┃
461+
┣━━━━╋━━━━╋━━━━╋━━━━┫
462+
┃ A┃ x1┃ x2┃ >┃
463+
┣━━━━┻━━━━┻━━━━┻━━━━┫
464+
┃ Footer┃
465+
┗━━━━━━━━━━━━━━━━━━━┛`
466+
467+
const expectedTableAfter = `┏━━━━┳━┳━━━━┳━━━━┳━━┓
468+
┃Name┃<┃ 2┃ 3┃ >┃
469+
┣━━━━╋━╋━━━━╋━━━━╋━━┫
470+
┃ A┃<┃ x2┃ x3┃ >┃
471+
┣━━━━┻━┻━━━━┻━━━━┻━━┫
472+
┃ Footer┃
473+
┗━━━━━━━━━━━━━━━━━━━┛`
474+
475+
hitScrollRight := func() {
476+
model, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftRight})
477+
}
478+
479+
hitScrollLeft := func() {
480+
model, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftLeft})
481+
}
482+
483+
assert.Equal(t, expectedTableOriginal, model.View())
484+
485+
hitScrollRight()
486+
487+
assert.Equal(t, expectedTableAfter, model.View())
488+
489+
hitScrollLeft()
490+
assert.Equal(t, expectedTableOriginal, model.View())
491+
492+
// Try it again, should do nothing
493+
hitScrollLeft()
494+
assert.Equal(t, expectedTableOriginal, model.View())
495+
}

0 commit comments

Comments
 (0)