Skip to content

Commit

Permalink
feat(viewport): horizontal scroll (#240)
Browse files Browse the repository at this point in the history
* horizontal scroll

* rebase branch

* add tests

* add tests with 2 cells symbols

* trimLeft, move to charmbracelete/x/ansi lib

* up ansi package

* Update viewport/viewport.go

Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: do not navigate out to the right

* fix: cache line width on setcontent

* fix tests

* fix viewport tests

* add test for preventing right overscroll

* chore(viewport): increase horizontal step to 6

* chore(viewport): make horizontal scroll API better match vertical scroll API

* fix: nolint

* fix: use ansi.Cut

* perf: do not cut anything if not needed

* feat: expose HorizontalScrollPercent

* fix: do not scroll if width is 0

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

* fix: visible lines take frame into account

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Co-authored-by: Christian Rocha <christian@rocha.is>
  • Loading branch information
3 people authored Jan 10, 2025
1 parent 8624776 commit 2d53a61
Show file tree
Hide file tree
Showing 5 changed files with 491 additions and 9 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/charmbracelet/bubbletea v1.1.2
github.com/charmbracelet/harmonica v0.2.0
github.com/charmbracelet/lipgloss v1.0.0
github.com/charmbracelet/x/ansi v0.4.2
github.com/charmbracelet/x/ansi v0.6.1-0.20250107110353-48b574af22a5
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91
github.com/dustin/go-humanize v1.0.1
github.com/lucasb-eyer/go-colorful v1.2.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk=
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/ansi v0.6.1-0.20250107110353-48b574af22a5 h1:TSjbA80sXnABV/Vxhnb67Ho7p8bEYqz6NIdhLAx+1yg=
github.com/charmbracelet/x/ansi v0.6.1-0.20250107110353-48b574af22a5/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
Expand Down
10 changes: 10 additions & 0 deletions viewport/keymap.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type KeyMap struct {
HalfPageDown key.Binding
Down key.Binding
Up key.Binding
Left key.Binding
Right key.Binding
}

// DefaultKeyMap returns a set of pager-like default keybindings.
Expand Down Expand Up @@ -44,5 +46,13 @@ func DefaultKeyMap() KeyMap {
key.WithKeys("down", "j"),
key.WithHelp("↓/j", "down"),
),
Left: key.NewBinding(
key.WithKeys("left", "h"),
key.WithHelp("←/h", "move left"),
),
Right: key.NewBinding(
key.WithKeys("right", "l"),
key.WithHelp("→/l", "move right"),
),
}
}
101 changes: 95 additions & 6 deletions viewport/viewport.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
)

const (
defaultHorizontalStep = 6
)

// New returns a new model with the given width and height as well as default
Expand Down Expand Up @@ -34,6 +39,13 @@ type Model struct {
// YOffset is the vertical scroll position.
YOffset int

// xOffset is the horizontal scroll position.
xOffset int

// horizontalStep is the number of columns we move left or right during a
// default horizontal scroll.
horizontalStep int

// YPosition is the position of the viewport in relation to the terminal
// window. It's used in high performance rendering only.
YPosition int
Expand All @@ -54,15 +66,17 @@ type Model struct {
// Deprecated: high performance rendering is now deprecated in Bubble Tea.
HighPerformanceRendering bool

initialized bool
lines []string
initialized bool
lines []string
longestLineWidth int
}

func (m *Model) setInitialValues() {
m.KeyMap = DefaultKeyMap()
m.MouseWheelEnabled = true
m.MouseWheelDelta = 3
m.initialized = true
m.horizontalStep = defaultHorizontalStep
}

// Init exists to satisfy the tea.Model interface for composability purposes.
Expand Down Expand Up @@ -99,10 +113,24 @@ func (m Model) ScrollPercent() float64 {
return math.Max(0.0, math.Min(1.0, v))
}

// HorizontalScrollPercent returns the amount horizontally scrolled as a float
// between 0 and 1.
func (m Model) HorizontalScrollPercent() float64 {
if m.xOffset >= m.longestLineWidth-m.Width {
return 1.0
}
y := float64(m.xOffset)
h := float64(m.Width)
t := float64(m.longestLineWidth)
v := y / (t - h)
return math.Max(0.0, math.Min(1.0, v))
}

// SetContent set the pager's text content.
func (m *Model) SetContent(s string) {
s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings
m.lines = strings.Split(s, "\n")
m.longestLineWidth = findLongestLineWidth(m.lines)

if m.YOffset > len(m.lines)-1 {
m.GotoBottom()
Expand All @@ -118,12 +146,24 @@ func (m Model) maxYOffset() int {
// visibleLines returns the lines that should currently be visible in the
// viewport.
func (m Model) visibleLines() (lines []string) {
h := m.Height - m.Style.GetVerticalFrameSize()
w := m.Width - m.Style.GetHorizontalFrameSize()

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

if (m.xOffset == 0 && m.longestLineWidth <= w) || w == 0 {
return lines
}

cutLines := make([]string, len(lines))
for i := range lines {
cutLines[i] = ansi.Cut(lines[i], m.xOffset, m.xOffset+w)
}
return cutLines
}

// scrollArea returns the scrollable boundaries for high performance rendering.
Expand Down Expand Up @@ -273,7 +313,7 @@ func ViewDown(m Model, lines []string) tea.Cmd {

// XXX: high performance rendering is deprecated in Bubble Tea. In a v2 we
// won't need to return a command here.
return tea.ScrollDown(lines, top, bottom)
return tea.ScrollDown(lines, top, bottom) //nolint:staticcheck
}

// ViewUp is a high performance command the moves the viewport down by a given
Expand All @@ -287,7 +327,40 @@ func ViewUp(m Model, lines []string) tea.Cmd {

// XXX: high performance rendering is deprecated in Bubble Tea. In a v2 we
// won't need to return a command here.
return tea.ScrollUp(lines, top, bottom)
return tea.ScrollUp(lines, top, bottom) //nolint:staticcheck
}

// 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
if m.xOffset >= m.longestLineWidth-m.Width {
return
}
m.xOffset += cols
}

// Resets lines indent to zero.
func (m *Model) ResetIndent() {
m.xOffset = 0
}

// Update handles standard message-based viewport updates.
Expand Down Expand Up @@ -344,6 +417,12 @@ func (m Model) updateAsModel(msg tea.Msg) (Model, tea.Cmd) {
if m.HighPerformanceRendering {
cmd = ViewUp(m, lines)
}

case key.Matches(msg, m.KeyMap.Left):
m.MoveLeft(m.horizontalStep)

case key.Matches(msg, m.KeyMap.Right):
m.MoveRight(m.horizontalStep)
}

case tea.MouseMsg:
Expand Down Expand Up @@ -418,3 +497,13 @@ func max(a, b int) int {
}
return b
}

func findLongestLineWidth(lines []string) int {
w := 0
for _, l := range lines {
if ww := ansi.StringWidth(l); ww > w {
w = ww
}
}
return w
}
Loading

0 comments on commit 2d53a61

Please sign in to comment.