charmbracelet / bubbles

TUI components for Bubble Tea 🫧
MIT License
5.49k stars 265 forks source link

Table is scrolling before selected row reaches the top #428

Closed dzeleniak closed 7 months ago

dzeleniak commented 11 months ago

This is a copy of an issue created on bubbletea.

"Here is a code sample that uses the table component. Something I noticed is that when I press the down arrow key, going from the first item to the last, everything works as expected. Going in the reverse direction, that is, going from the last item to the first one (via up arrow) the viewport is scrolled down before the selected line matches the top of the viewport. Check the animated gif on the entry labeled as Poland. What can be done so that going from the first item to last item and the opposite way (last to first) behaves exactly the same way?"

package main

import (
    "os"
    "fmt"
        "strings"

    "github.com/charmbracelet/bubbles/table"
    tea "github.com/charmbracelet/bubbletea"
    "github.com/charmbracelet/lipgloss"
)

var selectedRepo string = ""

var baseStyle = lipgloss.NewStyle().
    BorderStyle(lipgloss.DoubleBorder()).
    BorderForeground(lipgloss.Color("56"))

type model struct {
    table  table.Model
    width  int
    height int
}

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

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    var cmd tea.Cmd
    switch msg := msg.(type) {
    case tea.WindowSizeMsg:
        m.width = msg.Width
        m.height = msg.Height   
    case tea.KeyMsg:
        switch msg.String() {
        case "esc", "q":
            return m, tea.Quit
        case "enter":
            selectedRepo = m.table.SelectedRow()[0]
            return m, tea.Quit
                }
    }
    m.table, cmd = m.table.Update(msg)
    return m, cmd
}

func (m model) View() string {
    if m.width == 0 { return "" }
    table := baseStyle.Render(m.table.View())
    return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, table)
}

func center(s string, w int) string {
    if len(s) >= w { return s }
    n := w - len(s)
    div := n / 2
    return strings.Repeat(" ", div) + s + strings.Repeat(" ", div)
}

func main() {
    centeredTitle := center("Press [ENTER] to choose repository or [ESC]/[Q] to quit", 71)

    columns := []table.Column{
        {Title: centeredTitle, Width: 71},
    }

    rows := []table.Row{
            {"                    Brazil  devuan.c3sl.ufpr.br"},
            {"                  Bulgaria  dev1.ipacct.com"},
            {"                  Bulgaria  devuan.ipacct.com/devuan"},
            {"                    Canada  mishka.snork.ca/devuan"},
            {"                     Chile  devuan.dcc.uchile.cl"},
            {"                   Denmark  mirrors.dotsrc.org/devuan"},
            {"                   England  devuan-mirror.thorcom.net"},
            {"                   Finland  devuan.packet-gain.de"},
            {"                    France  pkgmaster.devuan.org"},
            {"                   Germany  devuan.bio.lmu.de"},
            {"                   Germany  devuan.sedf.de"},
            {"                   Germany  dist-mirror.fem.tu-ilmenau.de/devuan"},
            {"                   Germany  ftp.fau.de/devuan"},
            {"                   Germany  mirror.checkdomain.de/devuan"},
            {"                   Germany  mirror.stinpriza.org/devuan"},
            {"                   Hungary  quantum-mirror.hu/mirrors/pub/devuan"},
            {"                     India  dev1.ipacct.in"},
            {"                     India  devuan.ipacct.in/devuan"},
            {"                     Japan  devuan.m10k.jp"},
            {"               Netherlands  mirror.koddos.net/devuan"},
            {"               Netherlands  mirror.vpgrp.io/devuan"},
            {"               Netherlands  sledjhamr.org/devuan"},
            {"               New Zealand  deb.devuan.nz"},
            {"                    Poland  devuan.krypto-it.pl/devuan/devuan"},
            {"                    Poland  devuan.sakamoto.pl/packages"},
            {"                     Spain  repo.ifca.es/devuan"},
            {"                    Sweden  devuan.keff.org"},
            {"               Switzerland  devuan.planetcobalt.net"},
            {"               Switzerland  mirror.ungleich.ch/mirror/packages/devuan"},
            {"                    Taiwan  tw1.mirror.blendbyte.net/devuan"},
            {"                   Ukraine  mirror.mirohost.net/devuan"},
            {"  United States of America  dev.beard.ly/devuan"},
            {"  United States of America  devuan.slipfox.xyz"},
            {"  United States of America  mirrors.ocf.berkeley.edu/devuan"},
            {"                   Uruguay  espejito.fder.edu.uy/devuan"},
    }

    t := table.New(
        table.WithColumns(columns),
        table.WithRows(rows),
        table.WithFocused(true),
        table.WithHeight(13),
    )

    s := table.DefaultStyles()
    s.Header = s.Header.
        BorderStyle(lipgloss.NormalBorder()).
        BorderForeground(lipgloss.Color("56")).
        BorderBottom(true).
        Bold(true)
    s.Selected = s.Selected.
        Foreground(lipgloss.Color("229")).
        Background(lipgloss.Color("13")).
        Bold(false)
    t.SetStyles(s)

    m := model{t, 0, 0}
    if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil {
        fmt.Println("Error running program:", err)
        os.Exit(1)
    }

    if selectedRepo == "" {
        fmt.Printf("No repository was chosen\n")
        os.Exit(0)
    }

    var repo  string
    var parts []string
    repo  = strings.TrimLeft(selectedRepo, " ")
    parts = strings.Split(repo, "  ")
    repo  = parts[1]
    fmt.Printf("%s\n", repo)
}

266444965-fa022f25-e8de-481a-8f6d-e5067b2072fb

dzeleniak commented 11 months ago

I have done a little digging on this issue.

It looks like the problem is in the bubbles package on the move-up command. Upon investigation it looks like the error is in the MoveUp function.

The following code fixes the issue.

bubbles/table/table.go

// MoveUp moves the selection up by any number of rows.
// 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)
    switch {
    case m.start == 0:
        m.viewport.SetYOffset(clamp(m.viewport.YOffset, 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)
    }
    m.UpdateViewport()
}
maxatome commented 11 months ago

Just as a note, #363 reports a similar problem but when scrolling down. I don't know if it is the same bug behind the scene, so just in case...

dzeleniak commented 11 months ago

Thanks for the heads up! I'll take a look at this tomorrow.