Skip to content

Commit

Permalink
Multiline cells (#159)
Browse files Browse the repository at this point in the history
* chore: add hardcoded cells wrap with reflow.wordwrap

* feat: add multiline support

* fix: remove unncessery align

* chore: restore a new line

* chore: restore align

* refactor the multilinelogic

* feat: add example-multiline

* feat: add multiline tests

* chore: append one more test
  • Loading branch information
Maciej Więcek authored Nov 26, 2023
1 parent d6ce119 commit a875259
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 4 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ example-events:
example-features:
@go run ./examples/features/main.go

.PHONY: example-multiline
example-multiline:
@go run ./examples/multiline/main.go

.PHONY: example-filter
example-filter:
@go run ./examples/filter/*.go
Expand Down
5 changes: 5 additions & 0 deletions examples/multiline/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Multiline Example

This example code showcases the implementation of a multiline feature. The feature enables users to input and display content spanning multiple lines within the row. The provided code allows you to integrate the multiline feature seamlessly into your project. Feel free to experiment and adapt the code based on your specific requirements.

<img width="593" alt="image" src="https://github.com/Evertras/bubble-table/assets/23465248/3092b6f2-1e75-4c11-85f6-fcbea249d509">
166 changes: 166 additions & 0 deletions examples/multiline/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package main

import (
"log"
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/evertras/bubble-table/table"
)

const (
columnKeyName = "name"
columnKeyCountry = "country"
columnKeyCurrency = "crurrency"
)

type Model struct {
tableModel table.Model
}

func NewModel() Model {
columns := []table.Column{
table.NewColumn(columnKeyName, "Name", 10).WithStyle(
lipgloss.NewStyle().
Foreground(lipgloss.Color("#88f")),
),
table.NewColumn(columnKeyCountry, "Country", 20),
table.NewColumn(columnKeyCurrency, "Currency", 10),
}

rows := []table.Row{
table.NewRow(
table.RowData{
columnKeyName: "Talon Stokes",
columnKeyCountry: "Mexico",
columnKeyCurrency: "$23.17",
}),
table.NewRow(
table.RowData{
columnKeyName: "Sonia Shepard",
columnKeyCountry: "United States",
columnKeyCurrency: "$76.47",
}),
table.NewRow(
table.RowData{
columnKeyName: "Shad Reed",
columnKeyCountry: "Turkey",
columnKeyCurrency: "$62.99",
}),
table.NewRow(
table.RowData{
columnKeyName: "Kibo Clay",
columnKeyCountry: "Philippines",
columnKeyCurrency: "$29.82",
}),
table.NewRow(
table.RowData{

columnKeyName: "Leslie Kerr",
columnKeyCountry: "Singapore",
columnKeyCurrency: "$70.54",
}),
table.NewRow(
table.RowData{
columnKeyName: "Micah Hurst",
columnKeyCountry: "Pakistan",
columnKeyCurrency: "$80.84",
}),
table.NewRow(
table.RowData{
columnKeyName: "Dora Miranda",
columnKeyCountry: "Colombia",
columnKeyCurrency: "$34.75",
}),
table.NewRow(
table.RowData{
columnKeyName: "Keefe Walters",
columnKeyCountry: "China",
columnKeyCurrency: "$56.82",
}),
table.NewRow(
table.RowData{
columnKeyName: "Fujimoto Tarokizaemon no shoutokinori",
columnKeyCountry: "Japan",
columnKeyCurrency: "$89.31",
}),
table.NewRow(
table.RowData{
columnKeyName: "Keefe Walters",
columnKeyCountry: "China",
columnKeyCurrency: "$56.82",
}),
table.NewRow(
table.RowData{
columnKeyName: "Vincent Sanchez",
columnKeyCountry: "Peru",
columnKeyCurrency: "$71.60",
}),
table.NewRow(
table.RowData{
columnKeyName: "Lani Figueroa",
columnKeyCountry: "United Kingdom",
columnKeyCurrency: "$90.67",
}),
}

model := Model{
tableModel: table.New(columns).
WithRows(rows).
HeaderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true)).
Focused(true).
WithBaseStyle(
lipgloss.NewStyle().
BorderForeground(lipgloss.Color("#a38")).
Foreground(lipgloss.Color("#a7a")).
Align(lipgloss.Left),
).
WithMultiline(true),
}

return 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
)

m.tableModel, cmd = m.tableModel.Update(msg)
cmds = append(cmds, cmd)

switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)
}
}

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

func (m Model) View() string {
body := strings.Builder{}

body.WriteString("A table demo with multiline feature enabled!\n")
body.WriteString("Press up/down or j/k to move around\n")
body.WriteString(m.tableModel.View())
body.WriteString("\n")

return body.String()
}

func main() {
p := tea.NewProgram(NewModel())

if err := p.Start(); err != nil {
log.Fatal(err)
}
}
2 changes: 1 addition & 1 deletion table/border.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ func (b *borderStyleRow) inherit(s lipgloss.Style) {
}

// There's a lot of branches here, but splitting it up further would make it
// harder to follow. So just be careful with comments and make sure it's tested!
// harder to follow. So just be careful with comments and make sure it's tested!
//
//nolint:nestif
func (m Model) styleHeaders() borderStyleRow {
Expand Down
3 changes: 3 additions & 0 deletions table/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ type Model struct {
// Internal cached calculation, the height of the header and footer
// including borders. Used to determine how many padding rows to add.
metaHeight int

// If true, the table will be multiline
multiline bool
}

// New creates a new table ready for further modifications.
Expand Down
7 changes: 7 additions & 0 deletions table/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,10 @@ func (m Model) WithAllRowsDeselected() Model {

return m
}

// WithMultiline sets whether or not to wrap text in cells to multiple lines.
func (m Model) WithMultiline(multiline bool) Model {
m.multiline = multiline

return m
}
21 changes: 19 additions & 2 deletions table/row.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/wordwrap"
)

// RowData is a map of string column keys to interface{} data. Data with a key
Expand Down Expand Up @@ -43,7 +44,7 @@ func (r Row) WithStyle(style lipgloss.Style) Row {
return r
}

//nolint:nestif // This has many ifs, but they're short
//nolint:nestif,cyclop // This has many ifs, but they're short
func (m Model) renderRowColumnData(row Row, column Column, rowStyle lipgloss.Style, borderStyle lipgloss.Style) string {
cellStyle := rowStyle.Copy().Inherit(column.style).Inherit(m.baseStyle)

Expand Down Expand Up @@ -86,8 +87,15 @@ func (m Model) renderRowColumnData(row Row, column Column, rowStyle lipgloss.Sty
}
}

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(limitStr(str, column.width))
cellStr := cellStyle.Render(str)

return cellStr
}
Expand Down Expand Up @@ -121,6 +129,14 @@ func (m Model) renderRowData(row Row, rowStyle lipgloss.Style, last bool) string

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
Expand All @@ -130,6 +146,7 @@ func (m Model) renderRowData(row Row, rowStyle lipgloss.Style, last bool) string
} else {
rowStyles = stylesLast
}
rowStyle = rowStyle.Copy().Height(maxCellHeight)

if m.horizontalScrollOffsetCol > 0 && columnIndex == m.horizontalScrollFreezeColumnsCount {
var borderStyle lipgloss.Style
Expand Down
2 changes: 1 addition & 1 deletion table/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/charmbracelet/lipgloss"
)

// View renders the table. It does not end in a newline, so that it can be
// View renders the table. It does not end in a newline, so that it can be
// composed with other elements more consistently.
//
//nolint:cyclop
Expand Down
73 changes: 73 additions & 0 deletions table/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1515,3 +1515,76 @@ func TestMinimumHeightSmallerThanTable(t *testing.T) {

assert.Equal(t, expectedTable, rendered)
}

func TestMultilineEnabled(t *testing.T) {
model := New([]Column{
NewColumn("name", "Name", 4),
}).
WithRows([]Row{
NewRow(RowData{"name": "AAAAAAAAAAAAAAAAAA"}),
NewRow(RowData{"name": "BBB"}),
}).
WithMultiline(true)

assert.True(t, model.multiline)

const expectedTable = `┏━━━━┓
┃Name┃
┣━━━━┫
┃AAAA┃
┃AAAA┃
┃AAAA┃
┃AAAA┃
┃AA ┃
┃BBB ┃
┗━━━━┛`

rendered := model.View()
assert.Equal(t, expectedTable, rendered)
}

func TestMultilineDisabledByDefault(t *testing.T) {
model := New([]Column{
NewColumn("name", "Name", 4),
}).
WithRows([]Row{
NewRow(RowData{"name": "AAAAAAAAAAAAAAAAAA"}),
NewRow(RowData{"name": "BBB"}),
})
// WithMultiline(false)

assert.False(t, model.multiline)

const expectedTable = `┏━━━━┓
┃Name┃
┣━━━━┫
┃AAA…┃
┃ BBB┃
┗━━━━┛`

rendered := model.View()
assert.Equal(t, expectedTable, rendered)
}

func TestMultilineDisabledExplicite(t *testing.T) {
model := New([]Column{
NewColumn("name", "Name", 4),
}).
WithRows([]Row{
NewRow(RowData{"name": "AAAAAAAAAAAAAAAAAA"}),
NewRow(RowData{"name": "BBB"}),
}).
WithMultiline(false)

assert.False(t, model.multiline)

const expectedTable = `┏━━━━┓
┃Name┃
┣━━━━┫
┃AAA…┃
┃ BBB┃
┗━━━━┛`

rendered := model.View()
assert.Equal(t, expectedTable, rendered)
}

0 comments on commit a875259

Please sign in to comment.