Skip to content

Commit

Permalink
feat: introduce vertical scrollbar
Browse files Browse the repository at this point in the history
  • Loading branch information
nervo committed Jun 7, 2024
1 parent 0b15a9f commit a0b5dfa
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 0 deletions.
Binary file added scrollbar/example/example.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions scrollbar/example/example.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Output example.gif

Require go

Hide

Type@0 "go run main.go" Enter
Sleep 3s

Show

Space@100ms 100
Up@100ms 50
Down@100ms 30

Sleep 3s
95 changes: 95 additions & 0 deletions scrollbar/example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package main

import (
"fmt"
"github.com/charmbracelet/bubbles/scrollbar"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"os"
)

func newModel() model {
// Viewport
vp := viewport.New(0, 0)

// Scrollbar
sb := scrollbar.NewVertical()
sb.Style = sb.Style.Border(lipgloss.RoundedBorder(), true)

return model{
viewport: vp,
scrollbar: sb,
}
}

type model struct {
content string
viewport viewport.Model
scrollbar tea.Model
}

func (m model) Init() tea.Cmd {
return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)

switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q", "esc":
return m, tea.Quit
case " ":
if m.content != "" {
m.content += "\n"
}
m.content += fmt.Sprintf("%02d: Lorem ipsum dolor sit amet, consectetur adipiscing elit.", lipgloss.Height(m.content)-1)
}
case tea.WindowSizeMsg:
// Update viewport size
m.viewport.Width = msg.Width - 3

Check failure on line 55 in scrollbar/example/main.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 3, in <operation> detected (gomnd)
m.viewport.Height = msg.Height

// Update scrollbar height
m.scrollbar, cmd = m.scrollbar.Update(scrollbar.HeightMsg(msg.Height))
cmds = append(cmds, cmd)
}

m.viewport.SetContent(m.content)
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)

// Update scrollbar viewport
m.scrollbar, cmd = m.scrollbar.Update(m.viewport)

Check failure on line 68 in scrollbar/example/main.go

View workflow job for this annotation

GitHub Actions / lint

ineffectual assignment to cmd (ineffassign)

return m, tea.Batch(cmds...)
}

func (m model) View() string {
if m.viewport.TotalLineCount() > m.viewport.VisibleLineCount() {
return lipgloss.JoinHorizontal(lipgloss.Left,
m.viewport.View(),
m.scrollbar.View(),
)
}

return m.viewport.View()
}

func main() {
p := tea.NewProgram(
newModel(),
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
)

if _, err := p.Run(); err != nil {
fmt.Println("could not run program:", err)
os.Exit(1)
}
}
23 changes: 23 additions & 0 deletions scrollbar/scrollbar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package scrollbar

type Msg struct {

Check failure on line 3 in scrollbar/scrollbar.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported type Msg should have comment or be unexported (revive)
Total int
Visible int
Offset int
}

type HeightMsg int

Check failure on line 9 in scrollbar/scrollbar.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported type HeightMsg should have comment or be unexported (revive)

func min(a, b int) int {
if a < b {
return a
}
return b
}

func max(a, b int) int {
if a > b {
return a
}
return b
}
67 changes: 67 additions & 0 deletions scrollbar/vertical.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package scrollbar

import (
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"math"
"strings"
)

func NewVertical() Vertical {

Check failure on line 11 in scrollbar/vertical.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported function NewVertical should have comment or be unexported (revive)
return Vertical{
Style: lipgloss.NewStyle().Width(1),
ThumbStyle: lipgloss.NewStyle().SetString("β–ˆ"),
TrackStyle: lipgloss.NewStyle().SetString("β–‘"),
}
}

type Vertical struct {

Check failure on line 19 in scrollbar/vertical.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported type Vertical should have comment or be unexported (revive)
Style lipgloss.Style
ThumbStyle lipgloss.Style
TrackStyle lipgloss.Style
height int
thumbHeight int
thumbOffset int
}

func (m Vertical) Init() tea.Cmd {

Check failure on line 28 in scrollbar/vertical.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported method Vertical.Init should have comment or be unexported (revive)
return nil
}

func (m Vertical) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

Check failure on line 32 in scrollbar/vertical.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported method Vertical.Update should have comment or be unexported (revive)
switch msg := msg.(type) {
case Msg:
m.thumbHeight, m.thumbOffset = m.computeThumb(msg.Total, msg.Visible, msg.Offset)
case HeightMsg:
m.height = m.computeHeight(int(msg))
case viewport.Model:
m.thumbHeight, m.thumbOffset = m.computeThumb(msg.TotalLineCount(), msg.VisibleLineCount(), msg.YOffset)
}

return m, nil
}

func (m Vertical) computeHeight(height int) int {
return height - m.Style.GetVerticalFrameSize()
}

func (m Vertical) computeThumb(total, visible, offset int) (int, int) {
ratio := float64(m.height) / float64(total)

thumbHeight := max(1, int(math.Round(float64(visible)*ratio)))
thumbOffset := max(0, min(m.height-thumbHeight, int(math.Round(float64(offset)*ratio))))

return thumbHeight, thumbOffset
}

func (m Vertical) View() string {

Check failure on line 58 in scrollbar/vertical.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported method Vertical.View should have comment or be unexported (revive)
bar := strings.TrimRight(
strings.Repeat(m.TrackStyle.String()+"\n", m.thumbOffset)+
strings.Repeat(m.ThumbStyle.String()+"\n", m.thumbHeight)+
strings.Repeat(m.TrackStyle.String()+"\n", max(0, m.height-m.thumbOffset-m.thumbHeight)),
"\n",
)

return m.Style.Render(bar)
}
55 changes: 55 additions & 0 deletions scrollbar/vertical_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package scrollbar

import (
tea "github.com/charmbracelet/bubbletea"
"testing"
)

func TestVerticalView(t *testing.T) {
tests := []struct {
name string
total int
visible int
offset int
view string
}{
{
name: "ThirdTop",
total: 9,
visible: 3,
offset: 0,
view: "β–ˆ\nβ–‘\nβ–‘",
},
{
name: "ThirdMiddle",
total: 9,
visible: 3,
offset: 3,
view: "β–‘\nβ–ˆ\nβ–‘",
},
{
name: "ThirdBottom",
total: 9,
visible: 3,
offset: 6,
view: "β–‘\nβ–‘\nβ–ˆ",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var scrollbar tea.Model
scrollbar = NewVertical()
scrollbar, _ = scrollbar.Update(HeightMsg(test.visible))
scrollbar, _ = scrollbar.Update(Msg{
Total: test.total,
Visible: test.visible,
Offset: test.offset,
})
view := scrollbar.View()

if view != test.view {
t.Errorf("expected:\n%s\ngot:\n%s", test.view, view)
}
})
}
}

0 comments on commit a0b5dfa

Please sign in to comment.