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

Improper rendering of lipgloss.Table #381

Open
siennathesane opened this issue Sep 25, 2024 · 1 comment
Open

Improper rendering of lipgloss.Table #381

siennathesane opened this issue Sep 25, 2024 · 1 comment

Comments

@siennathesane
Copy link

Describe the bug

The lipgloss.Table rendering provides improper rendering in a few ways:

  • Headers are inconsistently rendered outside of the size of the window
  • Multiple rows are rendered with the same style during an update
  • The bottom of the table will scroll to the top of the window

Setup
Please complete the following information along with version numbers, if applicable.

  • macOS Sequoia
  • ZSH, default from macOS
  • Observed in both rio & iterm
  • No multiplexer
  • en_US.UTF-8, default for macOS.

To Reproduce
Steps to reproduce the behavior:

  1. Render the table with more rows than the height of the window.

Unfortunately, I can't share the code that's rendering the table, only the table component itself. You can render the table as the only thing rendered and still observe this behavior

Source Code

Here is the table component I built. It's pulled from github.com/charmbracelet/bubbles.Table, but modified to use lipgloss.Table instead.

package table

import (
	"github.com/charmbracelet/bubbles/help"
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/charmbracelet/lipgloss/table"
	"github.com/jamf/k8s-hermes-cli/internal/styles"
	"github.com/jamf/k8s-hermes-cli/internal/utils"
	jamfcolor "github.com/jamf/pkg/styles"
)

// table constants
const (
	headerRow int = 0
	firstRow  int = 1
)

type Data interface {
	table.Data
	Headers() []string
}

var _ tea.Model = (*Model)(nil)

// KeyMap defines keybindings for the Model.
type KeyMap struct {
	LineUp       key.Binding
	LineDown     key.Binding
	PageUp       key.Binding
	PageDown     key.Binding
	HalfPageUp   key.Binding
	HalfPageDown key.Binding
	GotoTop      key.Binding
	GotoBottom   key.Binding
}

// ShortHelp implements the KeyMap interface.
func (km KeyMap) ShortHelp() []key.Binding {
	return []key.Binding{km.LineUp, km.LineDown}
}

// FullHelp implements the KeyMap interface.
func (km KeyMap) FullHelp() [][]key.Binding {
	return [][]key.Binding{
		{km.LineUp, km.LineDown, km.GotoTop, km.GotoBottom},
		{km.PageUp, km.PageDown, km.HalfPageUp, km.HalfPageDown},
	}
}

// DefaultKeyMap returns a default set of keybindings.
func DefaultKeyMap() KeyMap {
	const spacebar = " "
	return KeyMap{
		LineUp: key.NewBinding(
			key.WithKeys("up", "k"),
			key.WithHelp("↑/k", "up"),
		),
		LineDown: key.NewBinding(
			key.WithKeys("down", "j"),
			key.WithHelp("↓/j", "down"),
		),
		PageUp: key.NewBinding(
			key.WithKeys("b", "pgup"),
			key.WithHelp("b/pgup", "page up"),
		),
		PageDown: key.NewBinding(
			key.WithKeys("f", "pgdown", spacebar),
			key.WithHelp("f/pgdn", "page down"),
		),
		HalfPageUp: key.NewBinding(
			key.WithKeys("u", "ctrl+u"),
			key.WithHelp("u", "½ page up"),
		),
		HalfPageDown: key.NewBinding(
			key.WithKeys("d", "ctrl+d"),
			key.WithHelp("d", "½ page down"),
		),
		GotoTop: key.NewBinding(
			key.WithKeys("home", "g"),
			key.WithHelp("g/home", "go to start"),
		),
		GotoBottom: key.NewBinding(
			key.WithKeys("end", "G"),
			key.WithHelp("G/end", "go to end"),
		),
	}
}

type Styles struct {
	Border       lipgloss.Border
	BorderStyle  lipgloss.Style
	BorderHeader bool
	Header       lipgloss.Style
	Cell         lipgloss.Style
	Selected     lipgloss.Style
}

func DefaultStyles() Styles {
	return Styles{
		Border:       lipgloss.RoundedBorder(),
		BorderHeader: true,
		BorderStyle:  styles.BaseStyle().BorderForeground(lipgloss.Color(jamfcolor.AthensGrey)),
		Selected:     styles.BaseStyle().Bold(true).Background(lipgloss.Color(jamfcolor.Havelock)),
		Header:       styles.BaseStyle().Bold(true).Padding(1).AlignHorizontal(lipgloss.Center),
		Cell:         styles.BaseStyle().Padding(1),
	}
}

type Option func(*Model)

type Model struct {
	KeyMap KeyMap
	Help   help.Model

	yoffset int
	height  int
	data    Data
	cursor  int
	focus   bool
	styles  Styles

	table *table.Table
	start int
	end   int
}

func New(opts ...Option) *Model {
	m := &Model{
		cursor: firstRow,
		table:  table.New(),
		KeyMap: DefaultKeyMap(),
		Help:   help.New(),
		styles: DefaultStyles(),
	}
	m.Help.ShowAll = true
	for _, o := range opts {
		o(m)
	}
	return m
}

// WithHeaders sets the headers for the table.
func WithHeaders(headers ...string) Option {
	return func(m *Model) {
		m.table.Headers(headers...)
	}
}

// WithRows sets the rows for the table.
func WithRows(rows ...[]string) Option {
	return func(m *Model) {
		m.table.Rows(rows...)
	}
}

// WithData sets the table data.
func WithData(data table.Data) Option {
	return func(m *Model) {
		m.table = m.table.Data(data)
	}
}

func WithFocus() Option {
	return func(m *Model) {
		m.focus = true
	}
}

func WithStyles(styles Styles) Option {
	return func(m *Model) {
		m.styles = styles
	}
}

func WithKeyMap(km KeyMap) Option {
	return func(m *Model) {
		m.KeyMap = km
	}
}

func WithHeight(h int) Option {
	return func(m *Model) {
		m.height = h
		m.table.Height(h)
	}
}

func WithWidth(w int) Option {
	return func(m *Model) {
		m.table.Width(w)
	}
}

// SetData sets the table data.
func (m *Model) SetData(data Data) {
	m.data = data
	m.table = m.table.Data(data)
}

// SetHeaders sets the table headers.
func (m *Model) SetHeaders(headers ...string) {
	m.table = m.table.Headers(headers...)
}

func (m *Model) Cursor() int {
	return m.cursor
}

func (m *Model) SetCursor(n int) {
	m.cursor = utils.Clamp(n, 0, m.data.Rows())
}

// MoveUp moves the cursor up n rows, up to the first row.
func (m *Model) MoveUp(n int) {
	m.SetCursor(m.cursor - n)

	m.setYOffset(m.yoffset - n)
	m.table.Offset(m.yoffset)
}

// MoveDown moves the cursor down n rows, up to the last row.
func (m *Model) MoveDown(n int) {
	m.SetCursor(m.cursor + n)

	m.setYOffset(m.yoffset + n)
	m.table.Offset(m.yoffset)
}

func (m *Model) GoToBottom() {
	m.MoveDown(m.data.Rows())
}

func (m *Model) GoToTop() {
	// todo (sienna): this feels buggy
	m.MoveUp(firstRow)
}

func (m *Model) Height() int {
	return m.height
}

func (m *Model) SetHeight(h int) {
	m.height = h
	m.table = m.table.Height(h)
}

// SetWidth sets the width of the table.
func (m *Model) SetWidth(w int) {
	m.table = m.table.Width(w)
}

// Focused returns true if the table is focused.
func (m *Model) Focused() bool {
	return m.focus
}

// Focus focuses the table, allowing the user to move around the rows and
// interact.
func (m *Model) Focus() {
	m.focus = true
}

// Blur blurs the table, preventing selection or movement.
func (m *Model) Blur() {
	m.focus = false
}

func (m *Model) HelpView() string {
	return m.Help.View(m.KeyMap)
}

func (m *Model) setYOffset(n int) {
	m.yoffset = utils.Clamp(n, 0, m.data.Rows())
}

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

func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	if !m.focus {
		return m, nil
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, m.KeyMap.LineUp):
			m.MoveUp(1)
		case key.Matches(msg, m.KeyMap.LineDown):
			m.MoveDown(1)
		case key.Matches(msg, m.KeyMap.PageUp):
			m.MoveUp(m.height)
		case key.Matches(msg, m.KeyMap.PageDown):
			m.MoveDown(m.height)
		case key.Matches(msg, m.KeyMap.HalfPageUp):
			m.MoveUp(m.height / 2)
		case key.Matches(msg, m.KeyMap.HalfPageDown):
			m.MoveDown(m.height / 2)
		case key.Matches(msg, m.KeyMap.LineDown):
			m.MoveDown(1)
		case key.Matches(msg, m.KeyMap.GotoTop):
			m.GoToTop()
		case key.Matches(msg, m.KeyMap.GotoBottom):
			m.GoToBottom()
		}
	case tea.MouseMsg:
		if msg.Button == tea.MouseButtonWheelUp {
			m.MoveUp(1)
		}
		if msg.Button == tea.MouseButtonWheelDown {
			m.MoveDown(1)
		}
	}

	return m, nil
}

func (m *Model) View() string {
	m.table.StyleFunc(func(row int, col int) lipgloss.Style {
		if row == headerRow {
			return m.styles.Header
		}
		if row == m.cursor {
			return m.styles.Selected
		}
		return m.styles.Cell
	})

	return m.table.String()
}

Expected behavior

  • Headers should render at the top of the window.
  • StyleFunc should consistently highlight rows
  • The table should not scroll up to the top of the window but remain locked to the bottom.

Screenshots

Screen.Recording.2024-09-25.at.1.45.04.PM.mov

Additional context

I've tested several different iterations and still end up with the same generalized results.

@bashbunni
Copy link
Member

Hey, I think this should be fixed by #373
We have an open PR in bubbles as well to make it render using Lip Gloss table as well if that's of interest! charmbracelet/bubbles#617

If you have any feedback on that one, please don't hesitate to let us know

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants