Files
abra/vendor/github.com/evertras/bubble-table/table/row.go

253 lines
6.2 KiB
Go

package table
import (
"fmt"
"sync/atomic"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/wordwrap"
)
// RowData is a map of string column keys to arbitrary data. Data with a key
// that matches a column key will be displayed. Data with a key that does not
// match a column key will not be displayed, but will remain attached to the Row.
// This can be useful for attaching hidden metadata for future reference when
// retrieving rows.
type RowData map[string]any
// Row represents a row in the table with some data keyed to the table columns>
// Can have a style applied to it such as color/bold. Create using NewRow().
type Row struct {
Style lipgloss.Style
Data RowData
selected bool
// id is an internal unique ID to match rows after they're copied
id uint32
}
var lastRowID uint32 = 1
// NewRow creates a new row and copies the given row data.
func NewRow(data RowData) Row {
row := Row{
Data: make(map[string]any),
id: lastRowID,
}
atomic.AddUint32(&lastRowID, 1)
for key, val := range data {
// Doesn't deep copy val, but close enough for now...
row.Data[key] = val
}
return row
}
// WithStyle uses the given style for the text in the row.
func (r Row) WithStyle(style lipgloss.Style) Row {
r.Style = style.Copy()
return r
}
//nolint:cyclop,funlen // Breaking this up will be more complicated than it's worth for now
func (m Model) renderRowColumnData(row Row, column Column, rowStyle lipgloss.Style, borderStyle lipgloss.Style) string {
cellStyle := rowStyle.Copy().Inherit(column.style).Inherit(m.baseStyle)
var str string
switch column.key {
case columnKeySelect:
if row.selected {
str = m.selectedText
} else {
str = m.unselectedText
}
case columnKeyOverflowRight:
cellStyle = cellStyle.Align(lipgloss.Right)
str = ">"
case columnKeyOverflowLeft:
str = "<"
default:
fmtString := "%v"
var data any
if entry, exists := row.Data[column.key]; exists {
data = entry
if column.fmtString != "" {
fmtString = column.fmtString
}
} else if m.missingDataIndicator != nil {
data = m.missingDataIndicator
} else {
data = ""
}
switch entry := data.(type) {
case StyledCell:
str = fmt.Sprintf(fmtString, entry.Data)
if entry.StyleFunc != nil {
cellStyle = entry.StyleFunc(StyledCellFuncInput{
Column: column,
Data: entry.Data,
Row: row,
GlobalMetadata: m.metadata,
}).Copy().Inherit(cellStyle)
} else {
cellStyle = entry.Style.Copy().Inherit(cellStyle)
}
default:
str = fmt.Sprintf(fmtString, entry)
}
}
if m.multiline {
str = wordwrap.String(str, column.width)
cellStyle = cellStyle.Align(lipgloss.Top)
} else {
str = limitStr(str, column.width)
}
cellStyle = cellStyle.Inherit(borderStyle)
cellStr := cellStyle.Render(str)
return cellStr
}
func (m Model) renderRow(rowIndex int, last bool) string {
row := m.GetVisibleRows()[rowIndex]
highlighted := rowIndex == m.rowCursorIndex
rowStyle := row.Style.Copy()
if m.rowStyleFunc != nil {
styleResult := m.rowStyleFunc(RowStyleFuncInput{
Index: rowIndex,
Row: row,
IsHighlighted: m.focused && highlighted,
})
rowStyle = rowStyle.Inherit(styleResult)
} else if m.focused && highlighted {
rowStyle = rowStyle.Inherit(m.highlightStyle)
}
return m.renderRowData(row, rowStyle, last)
}
func (m Model) renderBlankRow(last bool) string {
return m.renderRowData(NewRow(nil), lipgloss.NewStyle(), last)
}
// This is long and could use some refactoring in the future, but not quite sure
// how to pick it apart yet.
//
//nolint:funlen, cyclop
func (m Model) renderRowData(row Row, rowStyle lipgloss.Style, last bool) string {
numColumns := len(m.columns)
columnStrings := []string{}
totalRenderedWidth := 0
stylesInner, stylesLast := m.styleRows()
maxCellHeight := 1
if m.multiline {
for _, column := range m.columns {
cellStr := m.renderRowColumnData(row, column, rowStyle, lipgloss.NewStyle())
maxCellHeight = max(maxCellHeight, lipgloss.Height(cellStr))
}
}
for columnIndex, column := range m.columns {
var borderStyle lipgloss.Style
var rowStyles borderStyleRow
if !last {
rowStyles = stylesInner
} else {
rowStyles = stylesLast
}
rowStyle = rowStyle.Copy().Height(maxCellHeight)
if m.horizontalScrollOffsetCol > 0 && columnIndex == m.horizontalScrollFreezeColumnsCount {
var borderStyle lipgloss.Style
if columnIndex == 0 {
borderStyle = rowStyles.left.Copy()
} else {
borderStyle = rowStyles.inner.Copy()
}
rendered := m.renderRowColumnData(row, genOverflowColumnLeft(1), rowStyle, borderStyle)
totalRenderedWidth += lipgloss.Width(rendered)
columnStrings = append(columnStrings, rendered)
}
if columnIndex >= m.horizontalScrollFreezeColumnsCount &&
columnIndex < m.horizontalScrollOffsetCol+m.horizontalScrollFreezeColumnsCount {
continue
}
if len(columnStrings) == 0 {
borderStyle = rowStyles.left
} else if columnIndex < numColumns-1 {
borderStyle = rowStyles.inner
} else {
borderStyle = rowStyles.right
}
cellStr := m.renderRowColumnData(row, column, rowStyle, borderStyle)
if m.maxTotalWidth != 0 {
renderedWidth := lipgloss.Width(cellStr)
const (
borderAdjustment = 1
overflowColWidth = 2
)
targetWidth := m.maxTotalWidth - overflowColWidth
if columnIndex == len(m.columns)-1 {
// If this is the last header, we don't need to account for the
// overflow arrow column
targetWidth = m.maxTotalWidth
}
if totalRenderedWidth+renderedWidth > targetWidth {
overflowWidth := m.maxTotalWidth - totalRenderedWidth - borderAdjustment
overflowStyle := genOverflowStyle(rowStyles.right, overflowWidth)
overflowColumn := genOverflowColumnRight(overflowWidth)
overflowStr := m.renderRowColumnData(row, overflowColumn, rowStyle, overflowStyle)
columnStrings = append(columnStrings, overflowStr)
break
}
totalRenderedWidth += renderedWidth
}
columnStrings = append(columnStrings, cellStr)
}
return lipgloss.JoinHorizontal(lipgloss.Bottom, columnStrings...)
}
// Selected returns a copy of the row that's set to be selected or deselected.
// The old row is not changed in-place.
func (r Row) Selected(selected bool) Row {
r.selected = selected
return r
}