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(viewport): fix height calculation method #578

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
42 changes: 34 additions & 8 deletions viewport/viewport.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
)

// New returns a new model with the given width and height as well as default
Expand Down Expand Up @@ -87,12 +88,14 @@ func (m Model) PastBottom() bool {

// ScrollPercent returns the amount scrolled as a float between 0 and 1.
func (m Model) ScrollPercent() float64 {
if m.Height >= len(m.lines) {
wrappedLines := len(wrap(m.lines, m.Width))

if m.Height >= wrappedLines {
return 1.0
}
y := float64(m.YOffset)
h := float64(m.Height)
t := float64(len(m.lines))
t := float64(wrappedLines)
v := y / (t - h)
return math.Max(0.0, math.Min(1.0, v))
}
Expand All @@ -103,24 +106,27 @@ func (m *Model) SetContent(s string) {
s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings
m.lines = strings.Split(s, "\n")

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

// 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)
linesHeight := len(wrap(m.lines, m.Width))
return max(0, linesHeight-m.Height)
}

// visibleLines returns the lines that should currently be visible in the
// viewport.
func (m Model) visibleLines() (lines []string) {
if len(m.lines) > 0 {
wrappedLines := wrap(m.lines, m.Width)
top := max(0, m.YOffset)
bottom := clamp(m.YOffset+m.Height, top, len(m.lines))
lines = m.lines[top:bottom]
bottom := clamp(m.YOffset+m.Height, top, len(wrappedLines))

lines = wrappedLines[top:bottom]
}
return lines
}
Expand Down Expand Up @@ -191,7 +197,7 @@ func (m *Model) LineDown(n int) (lines []string) {
// Gather lines to send off for performance scrolling.
bottom := clamp(m.YOffset+m.Height, 0, len(m.lines))
top := clamp(m.YOffset+m.Height-n, 0, bottom)
return m.lines[top:bottom]
return wrap(m.lines, m.Width)[top:bottom]
}

// LineUp moves the view down by the given number of lines. Returns the new
Expand All @@ -208,7 +214,7 @@ func (m *Model) LineUp(n int) (lines []string) {
// Gather lines to send off for performance scrolling.
top := max(0, m.YOffset)
bottom := clamp(m.YOffset+n, 0, m.maxYOffset())
return m.lines[top:bottom]
return wrap(m.lines, m.Width)[top:bottom]
}

// TotalLineCount returns the total number of lines (both hidden and visible) within the viewport.
Expand Down Expand Up @@ -292,6 +298,10 @@ func (m Model) updateAsModel(msg tea.Msg) (Model, tea.Cmd) {
var cmd tea.Cmd

switch msg := msg.(type) {
case tea.WindowSizeMsg:
if m.PastBottom() {
m.SetYOffset(m.maxYOffset())
}
case tea.KeyMsg:
switch {
case key.Matches(msg, m.KeyMap.PageDown):
Expand Down Expand Up @@ -403,3 +413,19 @@ func max(a, b int) int {
}
return b
}

// wrap returns lines wrapped to the given width.
func wrap(lines []string, width int) []string {
var out []string
for _, line := range lines {
// word wrap lines
wrapWords := ansi.Wordwrap(line, width, "")
// wrap lines (handles lines that could not be word wrapped)
wrap := ansi.Hardwrap(wrapWords, width, true)
// split string by new lines
wrapLines := strings.Split(strings.TrimSpace(wrap), "\n")

out = append(out, wrapLines...)
}
return out
}
50 changes: 50 additions & 0 deletions viewport/viewport_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package viewport

import (
"testing"
)

func TestWrap(t *testing.T) {
t.Parallel()
tests := map[string]struct {
lines []string
width int
want []string
}{
"empty slice": {
lines: []string{},
width: 3,
want: []string{},
},
"all lines are within width": {
lines: []string{"aaa", "bbb", "ccc"},
width: 3,
want: []string{"aaa", "bbb", "ccc"},
},
"some lines exceeds width": {
lines: []string{"aaaaaa", "bbbbbbbb", "ccc"},
width: 3,
want: []string{"aaa", "aaa", "bbb", "bbb", "bb", "ccc"},
},
"full sentence exceeding width": {
lines: []string{"hello bob, I like yogurt in the mornings."},
width: 12,
want: []string{"hello bob, I", "like yogurt", "in the", "mornings."},
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
got := wrap(tt.lines, tt.width)

if len(got) != len(tt.want) {
t.Fatalf("expected len is %d but got %d", len(tt.want), len(got))
}
for i := range tt.want {
if tt.want[i] != got[i] {
t.Fatalf("expected %s but got %s", tt.want[i], got[i])
}
}
})
}
}
Loading