package tview import ( "github.com/gdamore/tcell" runewidth "github.com/mattn/go-runewidth" ) // TableCell represents one cell inside a Table. type TableCell struct { // The text to be displayed in the table cell. Text string // The alignment of the cell text. One of AlignLeft (default), AlignCenter, // or AlignRight. Align int // The maximum width of the cell in screen space. This is used to give a // column a maximum width. Any cell text whose screen width exceeds this width // is cut off. Set to 0 if there is no maximum width. MaxWidth int // The color of the cell text. Color tcell.Color // If set to true, this cell cannot be selected. NotSelectable bool // The position and width of the cell the last time table was drawn. x, y, width int } // GetLastPosition returns the position of the table cell the last time it was // drawn on screen. If the cell is not on screen, the return values are // undefined. // // Because the Table class will attempt to keep selected cells on screen, this // function is most useful in response to a "selected" event (see // SetSelectedFunc()). func (c *TableCell) GetLastPosition() (x, y, width int) { return c.x, c.y, c.width } // Table visualizes two-dimensional data consisting of rows and columns. Each // Table cell is defined via SetCell() by the TableCell type. They can be added // dynamically to the table and changed any time. // // The most compact display of a table is without borders. Each row will then // occupy one row on screen and columns are separated by the rune defined via // SetSeparator() (a space character by default). // // When borders are turned on (via SetBorders()), each table cell is surrounded // by lines. Therefore one table row will require two rows on screen. // // Columns will use as much horizontal space as they need. You can constrain // their size with the MaxWidth parameter of the TableCell type. // // Fixed Columns // // You can define fixed rows and rolumns via SetFixed(). They will always stay // in their place, even when the table is scrolled. Fixed rows are always the // top rows. Fixed columns are always the leftmost columns. // // Selections // // You can call SetSelectable() to set columns and/or rows to "selectable". If // the flag is set only for columns, entire columns can be selected by the user. // If it is set only for rows, entire rows can be selected. If both flags are // set, individual cells can be selected. The "selected" handler set via // SetSelectedFunc() is invoked when the user presses Enter on a selection. // // Navigation // // If the table extends beyond the available space, it can be navigated with // key bindings similar to Vim: // // - h, left arrow: Move left by one column. // - l, right arrow: Move right by one column. // - j, down arrow: Move down by one row. // - k, up arrow: Move up by one row. // - g, home: Move to the top. // - G, end: Move to the bottom. // - Ctrl-F, page down: Move down by one page. // - Ctrl-B, page up: Move up by one page. // // When there is no selection, this affects the entire table (except for fixed // rows and columns). When there is a selection, the user moves the selection. // The class will attempt to keep the selection from moving out of the screen. // // See https://github.com/rivo/tview/wiki/Table for an example. type Table struct { *Box // Whether or not this table has borders around each cell. borders bool // The color of the borders or the separator. bordersColor tcell.Color // If there are no borders, the column separator. separator rune // The cells of the table. Rows first, then columns. cells [][]*TableCell // The rightmost column in the data set. lastColumn int // The number of fixed rows / columns. fixedRows, fixedColumns int // Whether or not rows or columns can be selected. If both are set to true, // cells can be selected. rowsSelectable, columnsSelectable bool // The currently selected row and column. selectedRow, selectedColumn int // The number of rows/columns by which the table is scrolled down/to the // right. rowOffset, columnOffset int // If set to true, the table's last row will always be visible. trackEnd bool // The number of visible rows the last time the table was drawn. visibleRows int // An optional function which gets called when the user presses Enter on a // selected cell. If entire rows selected, the column value is undefined. // Likewise for entire columns. selected func(row, column int) // An optional function which gets called when the user presses Escape, Tab, // or Backtab. Also when the user presses Enter if nothing is selectable. done func(key tcell.Key) } // NewTable returns a new table. func NewTable() *Table { return &Table{ Box: NewBox(), bordersColor: Styles.GraphicsColor, separator: ' ', lastColumn: -1, } } // Clear removes all table data. func (t *Table) Clear() *Table { t.cells = nil t.lastColumn = -1 return t } // SetBorders sets whether or not each cell in the table is surrounded by a // border. func (t *Table) SetBorders(show bool) *Table { t.borders = show return t } // SetBordersColor sets the color of the cell borders. func (t *Table) SetBordersColor(color tcell.Color) *Table { t.bordersColor = color return t } // SetSeparator sets the character used to fill the space between two // neighboring cells. This is a space character ' ' per default but you may // want to set it to GraphicsVertBar (or any other rune) if the column // separation should be more visible. If cell borders are activated, this is // ignored. // // Separators have the same color as borders. func (t *Table) SetSeparator(separator rune) *Table { t.separator = separator return t } // SetFixed sets the number of fixed rows and columns which are always visible // even when the rest of the cells are scrolled out of view. Rows are always the // top-most ones. Columns are always the left-most ones. func (t *Table) SetFixed(rows, columns int) *Table { t.fixedRows, t.fixedColumns = rows, columns return t } // SetSelectable sets the flags which determine what can be selected in a table. // There are three selection modi: // // - rows = false, columns = false: Nothing can be selected. // - rows = true, columns = false: Rows can be selected. // - rows = false, columns = true: Columns can be selected. // - rows = true, columns = true: Individual cells can be selected. func (t *Table) SetSelectable(rows, columns bool) *Table { t.rowsSelectable, t.columnsSelectable = rows, columns return t } // Select sets the selected cell. Depending on the selection settings // specified via SetSelectable(), this may be an entire row or column, or even // ignored completely. func (t *Table) Select(row, column int) *Table { t.selectedRow, t.selectedColumn = row, column return t } // SetOffset sets how many rows and columns should be skipped when drawing the // table. This is useful for large tables that do not fit on the screen. // Navigating a selection can change these values. // // Fixed rows and columns are never skipped. func (t *Table) SetOffset(row, column int) *Table { t.rowOffset, t.columnOffset = row, column return t } // SetSelectedFunc sets a handler which is called whenever the user presses the // Enter key on a selected cell/row/column. The handler receives the position of // the selection and its cell contents. If entire rows are selected, the column // index is undefined. Likewise for entire columns. func (t *Table) SetSelectedFunc(handler func(row, column int)) *Table { t.selected = handler return t } // SetDoneFunc sets a handler which is called whenever the user presses the // Escape, Tab, or Backtab key. If nothing is selected, it is also called when // user presses the Enter key (because pressing Enter on a selection triggers // the "selected" handler set via SetSelectedFunc()). func (t *Table) SetDoneFunc(handler func(key tcell.Key)) *Table { t.done = handler return t } // SetCell sets the content of a cell the specified position. It is ok to // directly instantiate a TableCell object. If the cell has contain, at least // the Text and Color fields should be set. // // Note that setting cells in previously unknown rows and columns will // automatically extend the internal table representation, e.g. starting with // a row of 100,000 will immediately create 100,000 empty rows. // // To avoid unnecessary garbage collection, fill columns from left to right. func (t *Table) SetCell(row, column int, cell *TableCell) *Table { if row >= len(t.cells) { t.cells = append(t.cells, make([][]*TableCell, row-len(t.cells)+1)...) } rowLen := len(t.cells[row]) if column >= rowLen { t.cells[row] = append(t.cells[row], make([]*TableCell, column-rowLen+1)...) for c := rowLen; c < column; c++ { t.cells[row][c] = &TableCell{} } } t.cells[row][column] = cell if column > t.lastColumn { t.lastColumn = column } return t } // SetCellSimple calls SetCell() with the given text, left-aligned, in white. func (t *Table) SetCellSimple(row, column int, text string) *Table { t.SetCell(row, column, &TableCell{Text: text, Align: AlignLeft, Color: Styles.PrimaryTextColor}) return t } // GetCell returns the contents of the cell at the specified position. A valid // TableCell object is always returns but it will be uninitialized if the cell // was not previously set. func (t *Table) GetCell(row, column int) *TableCell { if row >= len(t.cells) || column >= len(t.cells[row]) { return &TableCell{} } return t.cells[row][column] } // GetRowCount returns the number of rows in the table. func (t *Table) GetRowCount() int { return len(t.cells) } // GetColumnCount returns the (maximum) number of columns in the table. func (t *Table) GetColumnCount() int { if len(t.cells) == 0 { return 0 } return t.lastColumn + 1 } // ScrollToBeginning scrolls the table to the beginning to that the top left // corner of the table is shown. Note that this position may be corrected if // there is a selection. func (t *Table) ScrollToBeginning() *Table { t.trackEnd = false t.columnOffset = 0 t.rowOffset = 0 return t } // ScrollToEnd scrolls the table to the beginning to that the bottom left corner // of the table is shown. Adding more rows to the table will cause it to // automatically scroll with the new data. Note that this position may be // corrected if there is a selection. func (t *Table) ScrollToEnd() *Table { t.trackEnd = true t.columnOffset = 0 t.rowOffset = len(t.cells) return t } // Draw draws this primitive onto the screen. func (t *Table) Draw(screen tcell.Screen) { t.Box.Draw(screen) // What's our available screen space? x, y, width, height := t.GetInnerRect() if t.borders { t.visibleRows = height / 2 } else { t.visibleRows = height } // Return the cell at the specified position (nil if it doesn't exist). getCell := func(row, column int) *TableCell { if row < 0 || column < 0 || row >= len(t.cells) || column >= len(t.cells[row]) { return nil } return t.cells[row][column] } // If this cell is not selectable, find the next one. if t.rowsSelectable || t.columnsSelectable { if t.selectedColumn < 0 { t.selectedColumn = 0 } if t.selectedRow < 0 { t.selectedRow = 0 } for t.selectedRow < len(t.cells) { cell := getCell(t.selectedRow, t.selectedColumn) if cell == nil || !cell.NotSelectable { break } t.selectedColumn++ if t.selectedColumn > t.lastColumn { t.selectedColumn = 0 t.selectedRow++ } } } // Clamp row offsets. if t.rowsSelectable { if t.selectedRow >= t.fixedRows && t.selectedRow < t.fixedRows+t.rowOffset { t.rowOffset = t.selectedRow - t.fixedRows t.trackEnd = false } if t.borders { if 2*(t.selectedRow+1-t.rowOffset) >= height { t.rowOffset = t.selectedRow + 1 - height/2 t.trackEnd = false } } else { if t.selectedRow+1-t.rowOffset >= height { t.rowOffset = t.selectedRow + 1 - height t.trackEnd = false } } } if t.borders { if 2*(len(t.cells)-t.rowOffset) < height { t.trackEnd = true } } else { if len(t.cells)-t.rowOffset < height { t.trackEnd = true } } if t.trackEnd { if t.borders { t.rowOffset = len(t.cells) - height/2 } else { t.rowOffset = len(t.cells) - height } } if t.rowOffset < 0 { t.rowOffset = 0 } // Clamp column offset. (Only left side here. The right side is more // difficult and we'll do it below.) if t.columnsSelectable && t.selectedColumn >= t.fixedColumns && t.selectedColumn < t.fixedColumns+t.columnOffset { t.columnOffset = t.selectedColumn - t.fixedColumns } if t.columnOffset < 0 { t.columnOffset = 0 } if t.selectedColumn < 0 { t.selectedColumn = 0 } // Determine the indices and widths of the columns and rows which fit on the // screen. var ( columns, rows, widths []int tableHeight, tableWidth int ) rowStep := 1 if t.borders { rowStep = 2 // With borders, every table row takes two screen rows. tableWidth = 1 // We start at the second character because of the left table border. } indexRow := func(row int) bool { // Determine if this row is visible, store its index. if tableHeight >= height { return false } rows = append(rows, row) tableHeight += rowStep return true } for row := 0; row < t.fixedRows && row < len(t.cells); row++ { // Do the fixed rows first. if !indexRow(row) { break } } for row := t.fixedRows + t.rowOffset; row < len(t.cells); row++ { // Then the remaining rows. if !indexRow(row) { break } } var skipped, lastTableWidth int ColumnLoop: for column := 0; ; column++ { // If we've moved beyond the right border, we stop or skip a column. for tableWidth-1 >= width { // -1 because we include one extra column if the separator falls on the right end of the box. // We've moved beyond the available space. if column < t.fixedColumns { break ColumnLoop // We're in the fixed area. We're done. } if !t.columnsSelectable && skipped >= t.columnOffset { break ColumnLoop // There is no selection and we've already reached the offset. } if t.columnsSelectable && t.selectedColumn-skipped == t.fixedColumns { break ColumnLoop // The selected column reached the leftmost point before disappearing. } if t.columnsSelectable && skipped >= t.columnOffset && (t.selectedColumn < column && lastTableWidth < width-1 && tableWidth < width-1 || t.selectedColumn < column-1) { break ColumnLoop // We've skipped as many as requested and the selection is visible. } if len(columns) <= t.fixedColumns { break // Nothing to skip. } // We need to skip a column. skipped++ lastTableWidth -= widths[t.fixedColumns] + 1 tableWidth -= widths[t.fixedColumns] + 1 columns = append(columns[:t.fixedColumns], columns[t.fixedColumns+1:]...) widths = append(widths[:t.fixedColumns], widths[t.fixedColumns+1:]...) } // What's this column's width? maxWidth := -1 for _, row := range rows { if cell := getCell(row, column); cell != nil { cellWidth := runewidth.StringWidth(cell.Text) if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth { cellWidth = cell.MaxWidth } if cellWidth > maxWidth { maxWidth = cellWidth } } } if maxWidth < 0 { break // No more cells found in this column. } // Store new column info at the end. columns = append(columns, column) widths = append(widths, maxWidth) lastTableWidth = tableWidth tableWidth += maxWidth + 1 } t.columnOffset = skipped // Helper function which draws border runes. borderStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.bordersColor) selectedBorderStyle := tcell.StyleDefault.Background(t.bordersColor).Foreground(t.backgroundColor) drawBorder := func(colX, rowY int, ch rune, selected bool) { style := borderStyle if selected { style = selectedBorderStyle } screen.SetContent(x+colX, y+rowY, ch, nil, style) } // Draw the cells (and borders). var columnX int if !t.borders { columnX-- } for columnIndex, column := range columns { columnWidth := widths[columnIndex] columnSelected := t.columnsSelectable && !t.rowsSelectable && column == t.selectedColumn for rowY, row := range rows { // Is this row/cell selected? rowSelected := t.rowsSelectable && !t.columnsSelectable && row == t.selectedRow cellSelected := columnSelected || rowSelected || t.rowsSelectable && t.columnsSelectable && column == t.selectedColumn && row == t.selectedRow if t.borders { // Draw borders. rowY *= 2 for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ { drawBorder(columnX+pos+1, rowY, GraphicsHoriBar, columnSelected) } ch := GraphicsCross if columnIndex == 0 { if rowY == 0 { ch = GraphicsTopLeftCorner } else { ch = GraphicsLeftT } } else if rowY == 0 { ch = GraphicsTopT } drawBorder(columnX, rowY, ch, false) rowY++ if rowY >= height { break // No space for the text anymore. } drawBorder(columnX, rowY, GraphicsVertBar, rowSelected) } else if columnIndex > 0 { // Draw separator. drawBorder(columnX, rowY, t.separator, rowSelected) } // Get the cell. cell := getCell(row, column) // Determine colors. bgColor := t.backgroundColor textColor := cell.Color if cellSelected && !cell.NotSelectable { bgColor = cell.Color textColor = t.backgroundColor } // Draw cell background. bgStyle := tcell.StyleDefault.Background(bgColor) finalWidth := columnWidth if columnX+1+columnWidth >= width { finalWidth = width - columnX - 1 } for pos := 0; pos < finalWidth; pos++ { screen.SetContent(x+columnX+1+pos, y+rowY, ' ', nil, bgStyle) } cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth // Draw text. text := cell.Text textWidth := runewidth.StringWidth(text) if finalWidth < textWidth && finalWidth > 0 { // Grow title until we hit the end. abbrWidth := runewidth.RuneWidth(GraphicsEllipsis) abbrPos := 0 for pos, ch := range text { if abbrWidth >= finalWidth { text = text[:abbrPos] + string(GraphicsEllipsis) break } abbrWidth += runewidth.RuneWidth(ch) abbrPos = pos } } Print(screen, text, x+columnX+1, y+rowY, finalWidth, cell.Align, textColor) } // Draw bottom border. if rowY := 2 * len(rows); t.borders && rowY < height { for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ { drawBorder(columnX+pos+1, rowY, GraphicsHoriBar, columnSelected) } ch := GraphicsBottomT if columnIndex == 0 { ch = GraphicsBottomLeftCorner } drawBorder(columnX, rowY, ch, false) } columnX += columnWidth + 1 } // Draw right border. if t.borders && len(t.cells) > 0 && columnX < width { for rowY, row := range rows { rowSelected := t.rowsSelectable && !t.columnsSelectable && row == t.selectedRow rowY *= 2 if rowY+1 < height { drawBorder(columnX, rowY+1, GraphicsVertBar, rowSelected) } ch := GraphicsRightT if rowY == 0 { ch = GraphicsTopRightCorner } drawBorder(columnX, rowY, ch, false) } if rowY := 2 * len(rows); rowY < height { drawBorder(columnX, rowY, GraphicsBottomRightCorner, false) } } } // InputHandler returns the handler for this primitive. func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return func(event *tcell.EventKey, setFocus func(p Primitive)) { key := event.Key() if (!t.rowsSelectable && !t.columnsSelectable && key == tcell.KeyEnter) || key == tcell.KeyEscape || key == tcell.KeyTab || key == tcell.KeyBacktab { if t.done != nil { t.done(key) } return } // Movement functions. var ( getCell = func(row, column int) *TableCell { if row < 0 || column < 0 || row >= len(t.cells) || column >= len(t.cells[row]) { return nil } return t.cells[row][column] } previous = func() { for t.selectedRow >= 0 { cell := getCell(t.selectedRow, t.selectedColumn) if cell == nil || !cell.NotSelectable { return } t.selectedColumn-- if t.selectedColumn < 0 { t.selectedColumn = t.lastColumn t.selectedRow-- } } } next = func() { if t.selectedColumn > t.lastColumn { t.selectedColumn = 0 t.selectedRow++ if t.selectedRow >= len(t.cells) { t.selectedRow = len(t.cells) - 1 } } for t.selectedRow < len(t.cells) { cell := getCell(t.selectedRow, t.selectedColumn) if cell == nil || !cell.NotSelectable { return } t.selectedColumn++ if t.selectedColumn > t.lastColumn { t.selectedColumn = 0 t.selectedRow++ } } t.selectedColumn = t.lastColumn t.selectedRow = len(t.cells) - 1 previous() } home = func() { if t.rowsSelectable { t.selectedRow = 0 t.selectedColumn = 0 next() } else { t.trackEnd = false t.rowOffset = 0 t.columnOffset = 0 } } end = func() { if t.rowsSelectable { t.selectedRow = len(t.cells) - 1 t.selectedColumn = t.lastColumn previous() } else { t.trackEnd = true t.columnOffset = 0 } } down = func() { if t.rowsSelectable { t.selectedRow++ if t.selectedRow >= len(t.cells) { t.selectedRow = len(t.cells) - 1 } next() } else { t.rowOffset++ } } up = func() { if t.rowsSelectable { t.selectedRow-- if t.selectedRow < 0 { t.selectedRow = 0 } previous() } else { t.trackEnd = false t.rowOffset-- } } left = func() { if t.columnsSelectable { t.selectedColumn-- if t.selectedColumn < 0 { t.selectedColumn = 0 } previous() } else { t.columnOffset-- } } right = func() { if t.columnsSelectable { t.selectedColumn++ if t.selectedColumn > t.lastColumn { t.selectedColumn = t.lastColumn } next() } else { t.columnOffset++ } } pageDown = func() { if t.rowsSelectable { t.selectedRow += t.visibleRows if t.selectedRow >= len(t.cells) { t.selectedRow = len(t.cells) - 1 } next() } else { t.rowOffset += t.visibleRows } } pageUp = func() { if t.rowsSelectable { t.selectedRow -= t.visibleRows if t.selectedRow < 0 { t.selectedRow = 0 } previous() } else { t.trackEnd = false t.rowOffset -= t.visibleRows } } ) switch key { case tcell.KeyRune: switch event.Rune() { case 'g': home() case 'G': end() case 'j': down() case 'k': up() case 'h': left() case 'l': right() } case tcell.KeyHome: home() case tcell.KeyEnd: end() case tcell.KeyUp: up() case tcell.KeyDown: down() case tcell.KeyLeft: left() case tcell.KeyRight: right() case tcell.KeyPgDn, tcell.KeyCtrlF: pageDown() case tcell.KeyPgUp, tcell.KeyCtrlB: pageUp() case tcell.KeyEnter: if (t.rowsSelectable || t.columnsSelectable) && t.selected != nil { t.selected(t.selectedRow, t.selectedColumn) } } } }