Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bug where viewport doesn't render final lines #74

Merged
merged 2 commits into from Sep 17, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
143 changes: 44 additions & 99 deletions viewport/viewport.go
Expand Up @@ -21,7 +21,7 @@ type Model struct {
YOffset int

// YPosition is the position of the viewport in relation to the terminal
// window. It's used in high performance rendering.
// window. It's used in high performance rendering only.
YPosition int

// HighPerformanceRendering bypasses the normal Bubble Tea renderer to
Expand All @@ -45,13 +45,13 @@ func (m Model) AtTop() bool {
// AtBottom returns whether or not the viewport is at or past the very bottom
// position.
func (m Model) AtBottom() bool {
return m.YOffset >= len(m.lines)-1-m.Height
return m.YOffset >= len(m.lines)-m.Height
}

// PastBottom returns whether or not the viewport is scrolled beyond the last
// line. This can happen when adjusting the viewport height.
func (m Model) PastBottom() bool {
return m.YOffset > len(m.lines)-1-m.Height
return m.YOffset > len(m.lines)-m.Height
}

// ScrollPercent returns the amount scrolled as a float between 0 and 1.
Expand All @@ -69,15 +69,16 @@ func (m Model) ScrollPercent() float64 {
// SetContent set the pager's text content. For high performance rendering the
// Sync command should also be called.
func (m *Model) SetContent(s string) {
s = strings.Replace(s, "\r\n", "\n", -1) // normalize line endings
s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings
m.lines = strings.Split(s, "\n")

if m.YOffset > len(m.lines)-1 {
m.GotoBottom()
}
}

// Return the lines that should currently be visible in the viewport.
// 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)
Expand All @@ -87,18 +88,29 @@ func (m Model) visibleLines() (lines []string) {
return lines
}

// scrollArea returns the scrollable boundaries for high performance rendering.
func (m Model) scrollArea() (top, bottom int) {
top = max(0, m.YPosition)
bottom = max(top, top+m.Height)
if top > 0 && bottom > top {
bottom--
}
return top, bottom
}

// SetYOffset sets the Y offset.
func (m *Model) SetYOffset(n int) {
m.YOffset = clamp(n, 0, len(m.lines)-m.Height)
}

// ViewDown moves the view down by the number of lines in the viewport.
// Basically, "page down".
func (m *Model) ViewDown() []string {
if m.AtBottom() {
return nil
}

m.YOffset = min(
m.YOffset+m.Height, // target
len(m.lines)-1-m.Height, // fallback
)

m.SetYOffset(m.YOffset + m.Height)
return m.visibleLines()
}

Expand All @@ -108,11 +120,7 @@ func (m *Model) ViewUp() []string {
return nil
}

m.YOffset = max(
m.YOffset-m.Height, // target
0, // fallback
)

m.SetYOffset(m.YOffset - m.Height)
return m.visibleLines()
}

Expand All @@ -122,18 +130,8 @@ func (m *Model) HalfViewDown() (lines []string) {
return nil
}

m.YOffset = min(
m.YOffset+m.Height/2, // target
len(m.lines)-1-m.Height, // fallback
)

if len(m.lines) > 0 {
top := max(m.YOffset+m.Height/2, 0)
bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}

return lines
m.SetYOffset(m.YOffset + m.Height/2)
return m.visibleLines()
}

// HalfViewUp moves the view up by half the height of the viewport.
Expand All @@ -142,18 +140,8 @@ func (m *Model) HalfViewUp() (lines []string) {
return nil
}

m.YOffset = max(
m.YOffset-m.Height/2, // target
0, // fallback
)

if len(m.lines) > 0 {
top := max(m.YOffset, 0)
bottom := clamp(m.YOffset+m.Height/2, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}

return lines
m.SetYOffset(m.YOffset - m.Height/2)
return m.visibleLines()
}

// LineDown moves the view down by the given number of lines.
Expand All @@ -165,21 +153,8 @@ func (m *Model) LineDown(n int) (lines []string) {
// Make sure the number of lines by which we're going to scroll isn't
// greater than the number of lines we actually have left before we reach
// the bottom.
maxDelta := (len(m.lines) - 1) - (m.YOffset + m.Height) // number of lines - viewport bottom edge
n = min(n, maxDelta)

m.YOffset = min(
m.YOffset+n, // target
len(m.lines)-1-m.Height, // fallback
)

if len(m.lines) > 0 {
top := max(m.YOffset+m.Height-n, 0)
bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}

return lines
m.SetYOffset(m.YOffset + n)
return m.visibleLines()
}

// LineUp moves the view down by the given number of lines. Returns the new
Expand All @@ -191,17 +166,8 @@ func (m *Model) LineUp(n int) (lines []string) {

// Make sure the number of lines by which we're going to scroll isn't
// greater than the number of lines we are from the top.
n = min(n, m.YOffset)

m.YOffset = max(m.YOffset-n, 0)

if len(m.lines) > 0 {
top := max(0, m.YOffset)
bottom := clamp(m.YOffset+n, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}

return lines
m.SetYOffset(m.YOffset - n)
return m.visibleLines()
}

// GotoTop sets the viewport to the top position.
Expand All @@ -210,28 +176,14 @@ func (m *Model) GotoTop() (lines []string) {
return nil
}

m.YOffset = 0

if len(m.lines) > 0 {
top := m.YOffset
bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}

return lines
m.SetYOffset(0)
return m.visibleLines()
}

// GotoBottom sets the viewport to the bottom position.
func (m *Model) GotoBottom() (lines []string) {
m.YOffset = max(len(m.lines)-1-m.Height, 0)

if len(m.lines) > 0 {
top := m.YOffset
bottom := max(len(m.lines)-1, 0)
lines = m.lines[top:bottom]
}

return lines
m.SetYOffset(len(m.lines) - 1 - m.Height)
return m.visibleLines()
}

// COMMANDS
Expand All @@ -245,17 +197,8 @@ func Sync(m Model) tea.Cmd {
if len(m.lines) == 0 {
return nil
}

// TODO: we should probably use m.visibleLines() rather than these two
// expressions.
top := max(m.YOffset, 0)
bottom := clamp(m.YOffset+m.Height, 0, len(m.lines)-1)

return tea.SyncScrollArea(
m.lines[top:bottom],
m.YPosition,
m.YPosition+m.Height,
)
top, bottom := m.scrollArea()
return tea.SyncScrollArea(m.visibleLines(), top, bottom)
}

// ViewDown is a high performance command that moves the viewport up by a given
Expand All @@ -269,7 +212,8 @@ func ViewDown(m Model, lines []string) tea.Cmd {
if len(lines) == 0 {
return nil
}
return tea.ScrollDown(lines, m.YPosition, m.YPosition+m.Height)
top, bottom := m.scrollArea()
return tea.ScrollDown(lines, top, bottom)
}

// ViewUp is a high performance command the moves the viewport down by a given
Expand All @@ -279,7 +223,8 @@ func ViewUp(m Model, lines []string) tea.Cmd {
if len(lines) == 0 {
return nil
}
return tea.ScrollUp(lines, m.YPosition, m.YPosition+m.Height)
top, bottom := m.scrollArea()
return tea.ScrollUp(lines, top, bottom)
}

// UPDATE
Expand Down Expand Up @@ -360,8 +305,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
// View renders the viewport into a string.
func (m Model) View() string {
if m.HighPerformanceRendering {
// Just send newlines since we're doing to be rendering the actual
// content seprately. We still need send something that equals the
// Just send newlines since we're going to be rendering the actual
// content seprately. We still need to send something that equals the
// height of this view so that the Bubble Tea standard renderer can
// position anything below this view properly.
return strings.Repeat("\n", m.Height-1)
Expand All @@ -372,7 +317,7 @@ func (m Model) View() string {
// Fill empty space with newlines
extraLines := ""
if len(lines) < m.Height {
extraLines = strings.Repeat("\n", m.Height-len(lines))
extraLines = strings.Repeat("\n", max(0, m.Height-len(lines)))
}

return strings.Join(lines, "\n") + extraLines
Expand Down