From 96b2dd552306aab9ebc7ac1f8d038db872c6b4b8 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Tue, 6 Oct 2020 17:17:43 -0700 Subject: [PATCH] Add Table test and benchmark, store TableCell as []byte instead of string --- CHANGELOG | 1 + demos/presentation/table.go | 6 +- demos/table/main.go | 6 +- table.go | 45 ++++++++++----- table_test.go | 106 ++++++++++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 18 deletions(-) create mode 100644 table_test.go diff --git a/CHANGELOG b/CHANGELOG index 4d0ba79..0b9ebf8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ v1.5.1 (WIP) - Store TextView buffer as [][]byte instead of []string - Add TextView.SetBytes and TextView.GetBytes +- Add TableCell.SetBytes, TableCell.GetBytes and TableCell.GetText - Allow modification of scroll bar render text - Optimize TextView (writing is 90% faster, drawing is 50% faster) diff --git a/demos/presentation/table.go b/demos/presentation/table.go index 91fb99d..78af2f8 100644 --- a/demos/presentation/table.go +++ b/demos/presentation/table.go @@ -1,8 +1,8 @@ package main import ( + "bytes" "fmt" - "strings" "github.com/gdamore/tcell/v2" "gitlab.com/tslocum/cview" @@ -251,8 +251,8 @@ const tableSelectCell = `[green]func[white] [yellow]main[white]() { func Table(nextSlide func()) (title string, content cview.Primitive) { table := cview.NewTable(). SetFixed(1, 1) - for row, line := range strings.Split(tableData, "\n") { - for column, cell := range strings.Split(line, "|") { + for row, line := range bytes.Split([]byte(tableData), []byte("\n")) { + for column, cell := range bytes.Split(line, []byte("|")) { color := tcell.ColorWhite.TrueColor() if row == 0 { color = tcell.ColorYellow.TrueColor() diff --git a/demos/table/main.go b/demos/table/main.go index 2c2cfad..e17724e 100644 --- a/demos/table/main.go +++ b/demos/table/main.go @@ -2,17 +2,19 @@ package main import ( - "strings" + "bytes" "github.com/gdamore/tcell/v2" "gitlab.com/tslocum/cview" ) +const loremIpsumText = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." + func main() { app := cview.NewApplication() table := cview.NewTable(). SetBorders(true) - lorem := strings.Split("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", " ") + lorem := bytes.Split([]byte(loremIpsumText), []byte(" ")) cols, rows := 10, 40 word := 0 for r := 0; r < rows; r++ { diff --git a/table.go b/table.go index d1042e3..7a93d1d 100644 --- a/table.go +++ b/table.go @@ -1,6 +1,7 @@ package cview import ( + "bytes" "sort" "sync" @@ -16,7 +17,7 @@ type TableCell struct { Reference interface{} // The text to be displayed in the table cell. - Text string + Text []byte // The alignment of the cell text. One of AlignLeft (default), AlignCenter, // or AlignRight. @@ -52,7 +53,7 @@ type TableCell struct { // NewTableCell returns a new table cell with sensible defaults. That is, left // aligned text with the primary text color (see Styles) and a transparent // background (using the background of the Table). -func NewTableCell(text string) *TableCell { +func NewTableCell(text []byte) *TableCell { return &TableCell{ Text: text, Align: AlignLeft, @@ -61,8 +62,8 @@ func NewTableCell(text string) *TableCell { } } -// SetText sets the cell's text. -func (c *TableCell) SetText(text string) *TableCell { +// SetBytes sets the cell's text. +func (c *TableCell) SetBytes(text []byte) *TableCell { c.Lock() defer c.Unlock() @@ -70,6 +71,24 @@ func (c *TableCell) SetText(text string) *TableCell { return c } +// SetText sets the cell's text. +func (c *TableCell) SetText(text string) *TableCell { + return c.SetBytes([]byte(text)) +} + +// GetBytes returns the cell's text. +func (c *TableCell) GetBytes() []byte { + c.RLock() + defer c.RUnlock() + + return c.Text +} + +// GetText returns the cell's text. +func (c *TableCell) GetText() string { + return string(c.GetBytes()) +} + // SetAlign sets the cell's text alignment, one of AlignLeft, AlignCenter, or // AlignRight. func (c *TableCell) SetAlign(align int) *TableCell { @@ -285,7 +304,7 @@ type Table struct { // If set to true, the table's last row will always be visible. trackEnd bool - // The sort function of the table. Defaults to a case-sensitive string comparison. + // The sort function of the table. Defaults to a case-sensitive comparison. sortFunc func(column, i, j int) bool // Whether or not the table should be sorted when a fixed row is clicked. @@ -591,7 +610,7 @@ func (t *Table) SetCell(row, column int, cell *TableCell) *Table { } // SetCellSimple calls SetCell() with the given text, left-aligned, in white. -func (t *Table) SetCellSimple(row, column int, text string) *Table { +func (t *Table) SetCellSimple(row, column int, text []byte) *Table { return t.SetCell(row, column, NewTableCell(text)) } @@ -797,7 +816,7 @@ func (t *Table) Sort(column int, descending bool) *Table { if t.sortFunc == nil { t.sortFunc = func(column, i, j int) bool { - return t.cells[i][column].Text < t.cells[j][column].Text + return bytes.Compare(t.cells[i][column].Text, t.cells[j][column].Text) == -1 } } @@ -995,7 +1014,7 @@ ColumnLoop: } for _, row := range evaluationRows { if cell := getCell(row, column); cell != nil { - _, _, _, _, _, _, cellWidth := decomposeText([]byte(cell.Text), true, false) + _, _, _, _, _, _, cellWidth := decomposeText(cell.Text, true, false) if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth { cellWidth = cell.MaxWidth } @@ -1089,8 +1108,8 @@ ColumnLoop: finalWidth = width - columnX - 1 } cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth - _, printed := printWithStyle(screen, []byte(cell.Text), x+columnX+1, y+rowY, finalWidth, cell.Align, SetAttributes(tcell.StyleDefault.Foreground(cell.Color), cell.Attributes)) - if TaggedStringWidth(cell.Text)-printed > 0 && printed > 0 { + _, printed := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, SetAttributes(tcell.StyleDefault.Foreground(cell.Color), cell.Attributes)) + if TaggedTextWidth(cell.Text)-printed > 0 && printed > 0 { _, _, style, _ := screen.GetContent(x+columnX+finalWidth, y+rowY) printWithStyle(screen, []byte(string(SemigraphicsHorizontalEllipsis)), x+columnX+finalWidth, y+rowY, 1, AlignLeft, style) } @@ -1201,7 +1220,7 @@ ColumnLoop: // the drawing of a cell by background color, selected cells last. type cellInfo struct { x, y, w, h int - text tcell.Color + color tcell.Color selected bool } cellsByBackgroundColor := make(map[tcell.Color][]*cellInfo) @@ -1229,7 +1248,7 @@ ColumnLoop: y: by, w: bw, h: bh, - text: cell.Color, + color: cell.Color, selected: cellSelected, }) if !ok { @@ -1256,7 +1275,7 @@ ColumnLoop: if t.selectedStyle != tcell.StyleDefault { defer colorBackground(cell.x, cell.y, cell.w, cell.h, selBg, selFg, selAttr, false) } else { - defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, 0, true) + defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.color, 0, true) } } else { colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, tcell.ColorDefault, 0, false) diff --git a/table_test.go b/table_test.go new file mode 100644 index 0000000..9165e67 --- /dev/null +++ b/table_test.go @@ -0,0 +1,106 @@ +package cview + +import ( + "fmt" + "testing" +) + +var tableTestCases = generateTableTestCases() + +type tableTestCase struct { + rows int + columns int + fixedRows int + fixedColumns int +} + +func (c *tableTestCase) String() string { + return fmt.Sprintf("Rows=%d/Cols=%d/FixedRows=%d/FixedCols=%d", c.rows, c.columns, c.fixedRows, c.fixedColumns) +} + +func TestTable(t *testing.T) { + t.Parallel() + + for _, c := range tableTestCases { + c := c // Capture + + t.Run(c.String(), func(t *testing.T) { + t.Parallel() + + table := tc(c) + + app, err := newTestApp(table) + if err != nil { + t.Errorf("failed to initialize Application: %s", err) + } + + for row := 0; row < c.rows; row++ { + for column := 0; column < c.columns; column++ { + contents := table.GetCell(row, column).GetText() + expected := fmt.Sprintf("%d,%d", column, row) + if contents != expected { + t.Errorf("failed to either get or set TableCell text: expected %s, got %s", expected, contents) + } + } + } + + table.Draw(app.screen) + + table.Clear() + }) + } +} + +func BenchmarkTableDraw(b *testing.B) { + for _, c := range tableTestCases { + c := c // Capture + + b.Run(c.String(), func(b *testing.B) { + table := tc(c) + + app, err := newTestApp(table) + if err != nil { + b.Errorf("failed to initialize Application: %s", err) + } + + table.Draw(app.screen) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + table.Draw(app.screen) + } + }) + } +} + +func generateTableTestCases() []*tableTestCase { + var cases []*tableTestCase + for i := 1; i < 3; i++ { + rows := i * 5 + for i := 1; i < 3; i++ { + columns := i * 7 + for fixedRows := 0; fixedRows < 3; fixedRows++ { + for fixedColumns := 0; fixedColumns < 3; fixedColumns++ { + cases = append(cases, &tableTestCase{rows, columns, fixedRows, fixedColumns}) + } + } + } + } + return cases +} + +func tc(c *tableTestCase) *Table { + table := NewTable() + + for row := 0; row < c.rows; row++ { + for column := 0; column < c.columns; column++ { + table.SetCellSimple(row, column, []byte(fmt.Sprintf("%d,%d", column, row))) + } + } + + table.SetFixed(c.fixedRows, c.fixedColumns) + + return table +}