diff --git a/viewport/viewport.go b/viewport/viewport.go index 27b5509c..0d82ab93 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -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 @@ -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. @@ -69,7 +69,7 @@ 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 { @@ -77,7 +77,8 @@ func (m *Model) SetContent(s string) { } } -// 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) @@ -87,6 +88,21 @@ 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 { @@ -94,11 +110,7 @@ func (m *Model) ViewDown() []string { 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() } @@ -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() } @@ -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. @@ -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. @@ -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 @@ -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. @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) @@ -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