From e0c7c5d1f73bf5bcd59aa5e76ce5cabdf150d025 Mon Sep 17 00:00:00 2001 From: Zebbeni Date: Fri, 24 Feb 2023 00:00:33 -0700 Subject: [PATCH 1/3] consider viewport frame size when calculating visible lines --- viewport/viewport.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/viewport/viewport.go b/viewport/viewport.go index b13e33c0..acd2f403 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -111,18 +111,20 @@ func (m *Model) SetContent(s string) { // maxYOffset returns the maximum possible value of the y-offset based on the // viewport's content and set height. func (m Model) maxYOffset() int { - return max(0, len(m.lines)-m.Height) + height := m.Height - m.Style.GetVerticalFrameSize() + return clamp(len(m.lines)-height, 0, len(m.lines)-1) } // visibleLines returns the lines that should currently be visible in the // viewport. -func (m Model) visibleLines() (lines []string) { - if len(m.lines) > 0 { - top := max(0, m.YOffset) - bottom := clamp(m.YOffset+m.Height, top, len(m.lines)) - lines = m.lines[top:bottom] +func (m Model) visibleLines() []string { + verticalSpace := m.Height - m.Style.GetVerticalFrameSize() + if verticalSpace <= 0 || len(m.lines) <= 0 { + return []string{} } - return lines + top := clamp(m.YOffset, 0, len(m.lines)-1) + bottom := min(top+verticalSpace, len(m.lines)) + return m.lines[top:bottom] } // scrollArea returns the scrollable boundaries for high performance rendering. From 7ad0f7426419fe5ca45812473bf5feaf4155ffba Mon Sep 17 00:00:00 2001 From: Zebbeni Date: Sun, 17 Mar 2024 01:27:17 -0600 Subject: [PATCH 2/3] start adding viewport unit tests --- viewport/viewport_test.go | 170 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 viewport/viewport_test.go diff --git a/viewport/viewport_test.go b/viewport/viewport_test.go new file mode 100644 index 00000000..a649616f --- /dev/null +++ b/viewport/viewport_test.go @@ -0,0 +1,170 @@ +package viewport + +import ( + "strings" + "testing" + "unicode" + + "github.com/MakeNowJust/heredoc" + "github.com/acarl005/stripansi" + "github.com/charmbracelet/lipgloss" +) + +const ( + viewportH = 8 + viewportW = 15 + content = "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10" +) + +var ( +//borderlessStyle = lipgloss.NewStyle() +//borderStyle = borderlessStyle.Copy().Border(lipgloss.RoundedBorder()) +//borderPadStyle = borderStyle.Copy().Padding(1, 1) +//borderPadMargin1Style = borderPadStyle.Copy().Margin(1, 0) +//borderPadMargin2Style = borderPadStyle.Copy().Margin(2, 0) + +// headerStyle = lipgloss.NewStyle().Padding(1, 0) +// +// headers = []string{"simple", "+ border", "+ padding", "+ margin (1)", "+ margin (2)"} +// styles = []lipgloss.Style{borderlessStyle, borderStyle, borderPadStyle, borderPadMargin1Style, borderPadMargin2Style} +) + +func TestMaxYOffset(t *testing.T) { + type want struct { + maxYOffset int + viewTop string + viewBot string + } + + tests := []struct { + name string + style lipgloss.Style + want want + }{ + { + name: "no style", + style: lipgloss.NewStyle(), + want: want{ + maxYOffset: 2, + viewTop: heredoc.Doc(` + line 1 + line 2 + line 3 + line 4 + line 5 + line 6 + line 7 + line 8 + `), + viewBot: heredoc.Doc(` + line 3 + line 4 + line 5 + line 6 + line 7 + line 8 + line 9 + line 10 + `), + }, + }, + { + name: "no style", + style: lipgloss.NewStyle(), + want: want{ + maxYOffset: 2, + viewTop: heredoc.Doc(` + line 1 + line 2 + line 3 + line 4 + line 5 + line 6 + line 7 + line 8 + `), + viewBot: heredoc.Doc(` + line 3 + line 4 + line 5 + line 6 + line 7 + line 8 + line 9 + line 10 + `), + }, + }, + { + name: "with border", + style: lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()), + want: want{ + maxYOffset: 2, + viewTop: heredoc.Doc(` + + line 1 + line 2 + line 3 + line 4 + line 5 + line 6 + + `), + viewBot: heredoc.Doc(` + + line 5 + line 6 + line 7 + line 8 + line 9 + line 10 + + `), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + viewport := New(viewportW, viewportH) + viewport.Style = tt.style + viewport.SetContent(content) + + maxYOffset := viewport.maxYOffset() + if maxYOffset != tt.want.maxYOffset { + t.Fatalf("\nWant maxYOffset:\n%v\nGot:\n%v\n", tt.want.maxYOffset, maxYOffset) + } + + viewport.SetYOffset(0) + viewTop := stripString(viewport.View()) + wantViewTop := stripString(tt.want.viewTop) + + if viewTop != wantViewTop { + t.Fatalf("Want view (when scrolled to top):\n%v\nGot:\n%v\n", wantViewTop, viewTop) + } + + viewport.SetYOffset(100) + viewBot := stripString(viewport.View()) + wantViewBot := stripString(tt.want.viewBot) + + if viewBot != wantViewBot { + t.Fatalf("Want view (when scrolled to bottom):\n%v\nGot:\n%v\n", wantViewBot, viewBot) + } + }) + } +} + +func stripString(str string) string { + s := stripansi.Strip(str) + ss := strings.Split(s, "\n") + + var lines []string + for _, l := range ss { + trim := strings.TrimRightFunc(l, unicode.IsSpace) + if trim != "" { + lines = append(lines, trim) + } + } + + return strings.Join(lines, "\n") +} From fde97acf7cdce0b59f07e30e4e4caa901863d278 Mon Sep 17 00:00:00 2001 From: Zebbeni Date: Sun, 17 Mar 2024 02:15:40 -0600 Subject: [PATCH 3/3] add viewport scroll unit test cases --- viewport/viewport_test.go | 161 ++++++++++++++++++++++++-------------- 1 file changed, 102 insertions(+), 59 deletions(-) diff --git a/viewport/viewport_test.go b/viewport/viewport_test.go index a649616f..61c962d4 100644 --- a/viewport/viewport_test.go +++ b/viewport/viewport_test.go @@ -16,24 +16,11 @@ const ( content = "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10" ) -var ( -//borderlessStyle = lipgloss.NewStyle() -//borderStyle = borderlessStyle.Copy().Border(lipgloss.RoundedBorder()) -//borderPadStyle = borderStyle.Copy().Padding(1, 1) -//borderPadMargin1Style = borderPadStyle.Copy().Margin(1, 0) -//borderPadMargin2Style = borderPadStyle.Copy().Margin(2, 0) - -// headerStyle = lipgloss.NewStyle().Padding(1, 0) -// -// headers = []string{"simple", "+ border", "+ padding", "+ margin (1)", "+ margin (2)"} -// styles = []lipgloss.Style{borderlessStyle, borderStyle, borderPadStyle, borderPadMargin1Style, borderPadMargin2Style} -) - func TestMaxYOffset(t *testing.T) { type want struct { - maxYOffset int - viewTop string - viewBot string + maxYOffset int + scrollTopView string + scrollBotView string } tests := []struct { @@ -46,7 +33,7 @@ func TestMaxYOffset(t *testing.T) { style: lipgloss.NewStyle(), want: want{ maxYOffset: 2, - viewTop: heredoc.Doc(` + scrollTopView: heredoc.Doc(` line 1 line 2 line 3 @@ -56,7 +43,7 @@ func TestMaxYOffset(t *testing.T) { line 7 line 8 `), - viewBot: heredoc.Doc(` + scrollBotView: heredoc.Doc(` line 3 line 4 line 5 @@ -69,55 +56,109 @@ func TestMaxYOffset(t *testing.T) { }, }, { - name: "no style", - style: lipgloss.NewStyle(), + name: "with single border", + style: lipgloss.NewStyle().Border(lipgloss.RoundedBorder()), want: want{ - maxYOffset: 2, - viewTop: heredoc.Doc(` - line 1 - line 2 - line 3 - line 4 - line 5 - line 6 - line 7 - line 8 + maxYOffset: 4, + scrollTopView: heredoc.Doc(` + ╭─────────────╮ + │line 1 │ + │line 2 │ + │line 3 │ + │line 4 │ + │line 5 │ + │line 6 │ + ╰─────────────╯ `), - viewBot: heredoc.Doc(` - line 3 - line 4 - line 5 - line 6 - line 7 - line 8 - line 9 - line 10 + scrollBotView: heredoc.Doc(` + ╭─────────────╮ + │line 5 │ + │line 6 │ + │line 7 │ + │line 8 │ + │line 9 │ + │line 10 │ + ╰─────────────╯ `), }, }, { - name: "with border", - style: lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()), + name: "with border + padding", + style: lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Padding(1, 1), want: want{ - maxYOffset: 2, - viewTop: heredoc.Doc(` + maxYOffset: 6, + scrollTopView: heredoc.Doc(` + ╭─────────────╮ + │ │ + │ line 1 │ + │ line 2 │ + │ line 3 │ + │ line 4 │ + │ │ + ╰─────────────╯ + `), + scrollBotView: heredoc.Doc(` + ╭─────────────╮ + │ │ + │ line 7 │ + │ line 8 │ + │ line 9 │ + │ line 10 │ + │ │ + ╰─────────────╯ + `), + }, + }, + { + name: "with border + margin", + style: lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Margin(1, 0), + want: want{ + maxYOffset: 6, + scrollTopView: heredoc.Doc(` - line 1 - line 2 - line 3 - line 4 - line 5 - line 6 + ╭─────────────╮ + │line 1 │ + │line 2 │ + │line 3 │ + │line 4 │ + ╰─────────────╯ `), - viewBot: heredoc.Doc(` + scrollBotView: heredoc.Doc(` - line 5 - line 6 - line 7 - line 8 - line 9 - line 10 + ╭─────────────╮ + │line 7 │ + │line 8 │ + │line 9 │ + │line 10 │ + ╰─────────────╯ + + `), + }, + }, + { + name: "with border + margin + padding", + style: lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Margin(1, 0).Padding(1, 1), + want: want{ + maxYOffset: 8, + scrollTopView: heredoc.Doc(` + + ╭─────────────╮ + │ │ + │ line 1 │ + │ line 2 │ + │ │ + ╰─────────────╯ + + `), + scrollBotView: heredoc.Doc(` + + ╭─────────────╮ + │ │ + │ line 9 │ + │ line 10 │ + │ │ + ╰─────────────╯ `), }, @@ -127,9 +168,11 @@ func TestMaxYOffset(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { viewport := New(viewportW, viewportH) - viewport.Style = tt.style + viewport.Style = tt.style.Copy() viewport.SetContent(content) + viewport, _ = viewport.Update(nil) + maxYOffset := viewport.maxYOffset() if maxYOffset != tt.want.maxYOffset { t.Fatalf("\nWant maxYOffset:\n%v\nGot:\n%v\n", tt.want.maxYOffset, maxYOffset) @@ -137,7 +180,7 @@ func TestMaxYOffset(t *testing.T) { viewport.SetYOffset(0) viewTop := stripString(viewport.View()) - wantViewTop := stripString(tt.want.viewTop) + wantViewTop := stripString(tt.want.scrollTopView) if viewTop != wantViewTop { t.Fatalf("Want view (when scrolled to top):\n%v\nGot:\n%v\n", wantViewTop, viewTop) @@ -145,7 +188,7 @@ func TestMaxYOffset(t *testing.T) { viewport.SetYOffset(100) viewBot := stripString(viewport.View()) - wantViewBot := stripString(tt.want.viewBot) + wantViewBot := stripString(tt.want.scrollBotView) if viewBot != wantViewBot { t.Fatalf("Want view (when scrolled to bottom):\n%v\nGot:\n%v\n", wantViewBot, viewBot)