fix: normalize yoffset

This commit is contained in:
Carlos Alexandro Becker
2025-03-27 16:34:10 -03:00
parent 4286c736cd
commit b1cef26c7d
4 changed files with 106 additions and 112 deletions
+16 -11
View File
@@ -350,14 +350,17 @@ func (m *Model) SetCursor(n int) {
// It can not go above the first row.
func (m *Model) MoveUp(n int) {
m.cursor = clamp(m.cursor-n, 0, len(m.rows)-1)
offset := m.viewport.YOffset()
switch {
case m.start == 0:
m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor))
offset = clamp(offset, 0, m.cursor)
case m.start < m.viewport.Height():
m.viewport.YOffset = (clamp(clamp(m.viewport.YOffset+n, 0, m.cursor), 0, m.viewport.Height()))
case m.viewport.YOffset >= 1:
m.viewport.YOffset = clamp(m.viewport.YOffset+n, 1, m.viewport.Height())
offset = clamp(clamp(offset+n, 0, m.cursor), 0, m.viewport.Height())
case offset >= 1:
offset = clamp(offset+n, 1, m.viewport.Height())
}
m.viewport.SetYOffset(offset)
m.UpdateViewport()
}
@@ -367,15 +370,17 @@ func (m *Model) MoveDown(n int) {
m.cursor = clamp(m.cursor+n, 0, len(m.rows)-1)
m.UpdateViewport()
offset := m.viewport.YOffset()
switch {
case m.end == len(m.rows) && m.viewport.YOffset > 0:
m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.viewport.Height()))
case m.cursor > (m.end-m.start)/2 && m.viewport.YOffset > 0:
m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.cursor))
case m.viewport.YOffset > 1:
case m.cursor > m.viewport.YOffset+m.viewport.Height()-1:
m.viewport.SetYOffset(clamp(m.viewport.YOffset+1, 0, 1))
case m.end == len(m.rows) && offset > 0:
offset = clamp(offset-n, 1, m.viewport.Height())
case m.cursor > (m.end-m.start)/2 && offset > 0:
offset = clamp(offset-n, 1, m.cursor)
case offset > 1:
case m.cursor > offset+m.viewport.Height()-1:
offset = clamp(offset+1, 0, 1)
}
m.viewport.SetYOffset(offset)
}
// GotoTop moves the selection to the first row.
+4 -4
View File
@@ -943,7 +943,7 @@ func (m Model) LineInfo() LineInfo {
// repositionView repositions the view of the viewport based on the defined
// scrolling behavior.
func (m *Model) repositionView() {
minimum := m.viewport.YOffset
minimum := m.viewport.YOffset()
maximum := minimum + m.viewport.Height() - 1
if row := m.cursorLineNumber(); row < minimum {
m.viewport.ScrollUp(minimum - row)
@@ -1268,7 +1268,7 @@ func (m Model) View() string {
// Always show at least `m.Height` lines at all times.
// To do this we can simply pad out a few extra new lines in the view.
for i := 0; i < m.height; i++ {
for range m.height {
s.WriteString(m.promptView(displayLine))
displayLine++
@@ -1446,7 +1446,7 @@ func (m Model) Cursor() *tea.Cursor {
baseStyle.GetBorderLeftSize()
yOffset := m.cursorLineNumber() -
m.viewport.YOffset +
m.viewport.YOffset() +
baseStyle.GetMarginTop() +
baseStyle.GetPaddingTop() +
baseStyle.GetBorderTopSize()
@@ -1472,7 +1472,7 @@ func (m Model) memoizedWrap(runes []rune, width int) [][]rune {
// This accounts for soft wrapped lines.
func (m Model) cursorLineNumber() int {
line := 0
for i := 0; i < m.row; i++ {
for i := range m.row {
// Calculate the number of lines that the current line will be split
// into.
line += len(m.memoizedWrap(m.value[i], m.width))
+72 -84
View File
@@ -66,8 +66,8 @@ type Model struct {
// The number of lines the mouse wheel will scroll. By default, this is 3.
MouseWheelDelta int
// YOffset is the vertical scroll position.
YOffset int
// yOffset is the vertical scroll position.
yOffset int
// xOffset is the horizontal scroll position.
xOffset int
@@ -172,19 +172,19 @@ func (m *Model) SetWidth(w int) {
// AtTop returns whether or not the viewport is at the very top position.
func (m Model) AtTop() bool {
return m.YOffset <= 0
return m.YOffset() <= 0
}
// AtBottom returns whether or not the viewport is at or past the very bottom
// position.
func (m Model) AtBottom() bool {
return m.YOffset >= m.maxYOffset()
return m.YOffset() >= m.maxYOffset()
}
// 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 > m.maxYOffset()
return m.YOffset() > m.maxYOffset()
}
// ScrollPercent returns the amount scrolled as a float between 0 and 1.
@@ -193,7 +193,7 @@ func (m Model) ScrollPercent() float64 {
if m.Height() >= count {
return 1.0
}
y := float64(m.YOffset)
y := float64(m.YOffset())
h := float64(m.Height())
t := float64(count)
v := y / (t - h)
@@ -233,7 +233,7 @@ func (m *Model) SetContentLines(lines []string) {
m.longestLineWidth = maxLineWidth(m.lines)
m.ClearHighlights()
if m.YOffset > m.maxYOffset() {
if m.YOffset() > m.maxYOffset() {
m.GotoBottom()
}
}
@@ -326,7 +326,7 @@ func (m Model) visibleLines() (lines []string) {
maxWidth := m.maxWidth()
if m.lineCount() > 0 {
pos := m.lineToIndex(m.YOffset)
pos := m.lineToIndex(m.YOffset())
top := max(0, pos)
bottom := clamp(pos+maxHeight, top, len(m.lines))
lines = make([]string, bottom-top)
@@ -406,7 +406,7 @@ func (m Model) softWrap(lines []string, maxWidth int) []string {
truncatedLine := ansi.Cut(line, idx, maxWidth+idx)
if m.LeftGutterFunc != nil {
truncatedLine = m.LeftGutterFunc(GutterContext{
Index: i + m.YOffset,
Index: i + m.YOffset(),
TotalLines: total,
Soft: idx > 0,
}) + truncatedLine
@@ -424,7 +424,7 @@ func (m Model) setupGutter(lines []string) []string {
return lines
}
offset := max(0, m.lineToIndex(m.YOffset))
offset := max(0, m.lineToIndex(m.YOffset()))
total := m.TotalLineCount()
result := make([]string, len(lines))
for i := range lines {
@@ -443,17 +443,11 @@ func (m Model) setupGutter(lines []string) []string {
// SetYOffset sets the Y offset.
func (m *Model) SetYOffset(n int) {
m.YOffset = clamp(n, 0, m.maxYOffset())
m.yOffset = clamp(n, 0, m.maxYOffset())
}
// SetXOffset sets the X offset.
// No-op when soft wrap is enabled.
func (m *Model) SetXOffset(n int) {
if m.SoftWrap {
return
}
m.xOffset = clamp(n, 0, m.maxXOffset())
}
// YOffset returns the current Y offset - the vertical scroll position.
func (m *Model) YOffset() int { return m.yOffset }
// EnsureVisible ensures that the given line and column are in the viewport.
func (m *Model) EnsureVisible(line, colstart, colend int) {
@@ -464,52 +458,51 @@ func (m *Model) EnsureVisible(line, colstart, colend int) {
m.SetXOffset(colstart - m.horizontalStep) // put one step to the left, feels more natural
}
if line < m.YOffset || line >= m.YOffset+m.maxHeight() {
if line < m.YOffset() || line >= m.YOffset()+m.maxHeight() {
m.SetYOffset(line)
}
m.visibleLines()
}
// ViewDown moves the view down by the number of lines in the viewport.
// Basically, "page down".
func (m *Model) ViewDown() {
// PageDown moves the view down by the number of lines in the viewport.
func (m *Model) PageDown() {
if m.AtBottom() {
return
}
m.LineDown(m.Height())
m.ScrollDown(m.Height())
}
// ViewUp moves the view up by one height of the viewport. Basically, "page up".
func (m *Model) ViewUp() {
// PageUp moves the view up by one height of the viewport.
func (m *Model) PageUp() {
if m.AtTop() {
return
}
m.LineUp(m.Height())
m.ScrollUp(m.Height())
}
// HalfViewDown moves the view down by half the height of the viewport.
func (m *Model) HalfViewDown() {
// HalfPageDown moves the view down by half the height of the viewport.
func (m *Model) HalfPageDown() {
if m.AtBottom() {
return
}
m.LineDown(m.Height() / 2) //nolint:mnd
m.ScrollDown(m.Height() / 2) //nolint:mnd
}
// HalfViewUp moves the view up by half the height of the viewport.
func (m *Model) HalfViewUp() {
// HalfPageUp moves the view up by half the height of the viewport.
func (m *Model) HalfPageUp() {
if m.AtTop() {
return
}
m.LineUp(m.Height() / 2) //nolint:mnd
m.ScrollUp(m.Height() / 2) //nolint:mnd
}
// LineDown moves the view down by the given number of lines.
func (m *Model) LineDown(n int) {
// ScrollDown moves the view down by the given number of lines.
func (m *Model) ScrollDown(n int) {
if m.AtBottom() || n == 0 || len(m.lines) == 0 {
return
}
@@ -517,23 +510,52 @@ func (m *Model) LineDown(n int) {
// 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.
m.SetYOffset(m.YOffset + n)
m.SetYOffset(m.YOffset() + n)
m.hiIdx = m.findNearedtMatch()
}
// LineUp moves the view down by the given number of lines. Returns the new
// ScrollUp moves the view down by the given number of lines. Returns the new
// lines to show.
func (m *Model) LineUp(n int) {
func (m *Model) ScrollUp(n int) {
if m.AtTop() || n == 0 || len(m.lines) == 0 {
return
}
// 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.
m.SetYOffset(m.YOffset - n)
m.SetYOffset(m.YOffset() - n)
m.hiIdx = m.findNearedtMatch()
}
// SetHorizontalStep sets the amount of cells that the viewport moves in the
// default viewport keymapping. If set to 0 or less, horizontal scrolling is
// disabled.
func (m *Model) SetHorizontalStep(n int) {
m.horizontalStep = max(0, n)
}
// XOffset returns the current X offset - the horizontal scroll position.
func (m *Model) XOffset() int { return m.xOffset }
// SetXOffset sets the X offset.
// No-op when soft wrap is enabled.
func (m *Model) SetXOffset(n int) {
if m.SoftWrap {
return
}
m.xOffset = clamp(n, 0, m.maxXOffset())
}
// ScrollLeft moves the viewport to the left by the given number of columns.
func (m *Model) ScrollLeft(n int) {
m.SetXOffset(m.xOffset - n)
}
// ScrollRight moves viewport to the right by the given number of columns.
func (m *Model) ScrollRight(n int) {
m.SetXOffset(m.xOffset + n)
}
// TotalLineCount returns the total number of lines (both hidden and visible) within the viewport.
func (m Model) TotalLineCount() int {
return m.lineCount()
@@ -562,40 +584,6 @@ func (m *Model) GotoBottom() (lines []string) {
return m.visibleLines()
}
// SetHorizontalStep sets the amount of cells that the viewport moves in the
// default viewport keymapping. If set to 0 or less, horizontal scrolling is
// disabled.
func (m *Model) SetHorizontalStep(n int) {
if n < 0 {
n = 0
}
m.horizontalStep = n
}
// MoveLeft moves the viewport to the left by the given number of columns.
func (m *Model) MoveLeft(cols int) {
m.xOffset -= cols
if m.xOffset < 0 {
m.xOffset = 0
}
}
// MoveRight moves viewport to the right by the given number of columns.
func (m *Model) MoveRight(cols int) {
// prevents over scrolling to the right
w := m.maxWidth()
if m.xOffset > m.longestLineWidth-w {
return
}
m.xOffset += cols
}
// Resets lines indent to zero.
func (m *Model) ResetIndent() {
m.xOffset = 0
}
// SetHighlights sets ranges of characters to highlight.
// For instance, `[]int{[]int{2, 10}, []int{20, 30}}` will highlight characters
// 2 to 10 and 20 to 30.
@@ -649,7 +637,7 @@ func (m *Model) HighlightPrevious() {
func (m Model) findNearedtMatch() int {
for i, match := range m.highlights {
if match.lineStart >= m.YOffset {
if match.lineStart >= m.YOffset() {
return i
}
}
@@ -673,28 +661,28 @@ func (m Model) updateAsModel(msg tea.Msg) Model {
case tea.KeyPressMsg:
switch {
case key.Matches(msg, m.KeyMap.PageDown):
m.ViewDown()
m.PageDown()
case key.Matches(msg, m.KeyMap.PageUp):
m.ViewUp()
m.PageUp()
case key.Matches(msg, m.KeyMap.HalfPageDown):
m.HalfViewDown()
m.HalfPageDown()
case key.Matches(msg, m.KeyMap.HalfPageUp):
m.HalfViewUp()
m.HalfPageUp()
case key.Matches(msg, m.KeyMap.Down):
m.LineDown(1)
m.ScrollDown(1)
case key.Matches(msg, m.KeyMap.Up):
m.LineUp(1)
m.ScrollUp(1)
case key.Matches(msg, m.KeyMap.Left):
m.MoveLeft(m.horizontalStep)
m.ScrollLeft(m.horizontalStep)
case key.Matches(msg, m.KeyMap.Right):
m.MoveRight(m.horizontalStep)
m.ScrollRight(m.horizontalStep)
}
case tea.MouseWheelMsg:
@@ -704,10 +692,10 @@ func (m Model) updateAsModel(msg tea.Msg) Model {
switch msg.Button {
case tea.MouseWheelDown:
m.LineDown(m.MouseWheelDelta)
m.ScrollDown(m.MouseWheelDelta)
case tea.MouseWheelUp:
m.LineUp(m.MouseWheelDelta)
m.ScrollUp(m.MouseWheelDelta)
}
}
+14 -13
View File
@@ -99,7 +99,7 @@ func TestMoveLeft(t *testing.T) {
t.Errorf("default indent should be %d, got %d", zeroPosition, m.xOffset)
}
m.MoveLeft(m.horizontalStep)
m.ScrollLeft(m.horizontalStep)
if m.xOffset != zeroPosition {
t.Errorf("indent should be %d, got %d", zeroPosition, m.xOffset)
}
@@ -108,12 +108,13 @@ func TestMoveLeft(t *testing.T) {
t.Run("move", func(t *testing.T) {
t.Parallel()
m := New(WithHeight(10), WithWidth(10))
m.longestLineWidth = 100
if m.xOffset != zeroPosition {
t.Errorf("default indent should be %d, got %d", zeroPosition, m.xOffset)
}
m.xOffset = defaultHorizontalStep * 2
m.MoveLeft(m.horizontalStep)
m.ScrollLeft(m.horizontalStep)
newIndent := defaultHorizontalStep
if m.xOffset != newIndent {
t.Errorf("indent should be %d, got %d", newIndent, m.xOffset)
@@ -135,7 +136,7 @@ func TestMoveRight(t *testing.T) {
t.Errorf("default indent should be %d, got %d", zeroPosition, m.xOffset)
}
m.MoveRight(m.horizontalStep)
m.ScrollRight(m.horizontalStep)
newIndent := defaultHorizontalStep
if m.xOffset != newIndent {
t.Errorf("indent should be %d, got %d", newIndent, m.xOffset)
@@ -154,7 +155,7 @@ func TestResetIndent(t *testing.T) {
m := New(WithHeight(10), WithWidth(10))
m.xOffset = 500
m.ResetIndent()
m.SetXOffset(0)
if m.xOffset != zeroPosition {
t.Errorf("indent should be %d, got %d", zeroPosition, m.xOffset)
}
@@ -233,7 +234,7 @@ func TestVisibleLines(t *testing.T) {
m := New(WithHeight(numberOfLines), WithWidth(10))
m.SetContent(strings.Join(defaultList, "\n"))
m.YOffset = 5
m.SetYOffset(5)
list := m.visibleLines()
if len(list) != numberOfLines {
@@ -246,7 +247,7 @@ func TestVisibleLines(t *testing.T) {
lastItemIdx := numberOfLines - 1
// we trim line if it doesn't fit to width of the viewport
shouldGet := defaultList[m.YOffset+lastItemIdx][:m.Width()]
shouldGet := defaultList[m.YOffset()+lastItemIdx][:m.Width()]
if list[lastItemIdx] != shouldGet {
t.Errorf(`%dth list item should be '%s', got '%s'`, lastItemIdx, shouldGet, list[lastItemIdx])
}
@@ -258,7 +259,7 @@ func TestVisibleLines(t *testing.T) {
m := New(WithHeight(numberOfLines), WithWidth(10))
m.lines = defaultList
m.YOffset = 7
m.SetYOffset(7)
// default list
list := m.visibleLines()
@@ -278,7 +279,7 @@ func TestVisibleLines(t *testing.T) {
}
// move right
m.MoveRight(m.horizontalStep)
m.ScrollRight(m.horizontalStep)
list = m.visibleLines()
newPrefix := perceptPrefix[m.xOffset:]
@@ -291,7 +292,7 @@ func TestVisibleLines(t *testing.T) {
}
// move left
m.MoveLeft(m.horizontalStep)
m.ScrollLeft(m.horizontalStep)
list = m.visibleLines()
if !strings.HasPrefix(list[0], perceptPrefix) {
t.Errorf("first list item has to have prefix %s", perceptPrefix)
@@ -333,7 +334,7 @@ func TestVisibleLines(t *testing.T) {
}
// move right
m.MoveRight(horizontalStep)
m.ScrollRight(horizontalStep)
list = m.visibleLines()
for i := range list {
@@ -344,7 +345,7 @@ func TestVisibleLines(t *testing.T) {
}
// move left
m.MoveLeft(horizontalStep)
m.ScrollLeft(horizontalStep)
list = m.visibleLines()
for i := range list {
if list[i] != initList[i] {
@@ -354,7 +355,7 @@ func TestVisibleLines(t *testing.T) {
// move left second times do not change lites if indent == 0
m.xOffset = 0
m.MoveLeft(horizontalStep)
m.ScrollLeft(horizontalStep)
list = m.visibleLines()
for i := range list {
if list[i] != initList[i] {
@@ -374,7 +375,7 @@ func TestRightOverscroll(t *testing.T) {
m.SetContent(content)
for i := 0; i < 10; i++ {
m.MoveRight(m.horizontalStep)
m.ScrollRight(m.horizontalStep)
}
visibleLines := m.visibleLines()