mirror of
https://github.com/rivo/tview.git
synced 2024-11-15 06:12:46 +00:00
911 lines
26 KiB
Go
911 lines
26 KiB
Go
package tview
|
|
|
|
import (
|
|
"github.com/gdamore/tcell"
|
|
runewidth "github.com/mattn/go-runewidth"
|
|
)
|
|
|
|
// TableCell represents one cell inside a Table. You can instantiate this type
|
|
// directly but all colors (background and text) will be set to their default
|
|
// which is black.
|
|
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
|
|
|
|
// The background color of the cell.
|
|
BackgroundColor 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
|
|
}
|
|
|
|
// 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 {
|
|
return &TableCell{
|
|
Text: text,
|
|
Align: AlignLeft,
|
|
Color: Styles.PrimaryTextColor,
|
|
BackgroundColor: tcell.ColorDefault,
|
|
}
|
|
}
|
|
|
|
// SetText sets the cell's text.
|
|
func (c *TableCell) SetText(text string) *TableCell {
|
|
c.Text = text
|
|
return c
|
|
}
|
|
|
|
// SetAlign sets the cell's text alignment, one of AlignLeft, AlignCenter, or
|
|
// AlignRight.
|
|
func (c *TableCell) SetAlign(align int) *TableCell {
|
|
c.Align = align
|
|
return c
|
|
}
|
|
|
|
// SetMaxWidth sets 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.
|
|
func (c *TableCell) SetMaxWidth(maxWidth int) *TableCell {
|
|
c.MaxWidth = maxWidth
|
|
return c
|
|
}
|
|
|
|
// SetTextColor sets the cell's text color.
|
|
func (c *TableCell) SetTextColor(color tcell.Color) *TableCell {
|
|
c.Color = color
|
|
return c
|
|
}
|
|
|
|
// SetBackgroundColor sets the cell's background color. Set to
|
|
// tcell.ColorDefault to use the table's background color.
|
|
func (c *TableCell) SetBackgroundColor(color tcell.Color) *TableCell {
|
|
c.BackgroundColor = color
|
|
return c
|
|
}
|
|
|
|
// SetSelectable sets whether or not this cell can be selected by the user.
|
|
func (c *TableCell) SetSelectable(selectable bool) *TableCell {
|
|
c.NotSelectable = !selectable
|
|
return c
|
|
}
|
|
|
|
// 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()) or a "selectionChanged" event (see
|
|
// SetSelectionChangedFunc()).
|
|
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 changes the selection.
|
|
// If entire rows selected, the column value is undefined.
|
|
// Likewise for entire columns.
|
|
selectionChanged 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
|
|
}
|
|
|
|
// GetSelectable returns what can be selected in a table. Refer to
|
|
// SetSelectable() for details.
|
|
func (t *Table) GetSelectable() (rows, columns bool) {
|
|
return t.rowsSelectable, t.columnsSelectable
|
|
}
|
|
|
|
// GetSelection returns the position of the current selection.
|
|
// If entire rows are selected, the column index is undefined.
|
|
// Likewise for entire columns.
|
|
func (t *Table) GetSelection() (row, column int) {
|
|
return t.selectedRow, t.selectedColumn
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// SetSelectionChangedFunc sets a handler which is called whenever the user
|
|
// navigates to a new selection. The handler receives the position of the new
|
|
// selection. If entire rows are selected, the column index is undefined.
|
|
// Likewise for entire columns.
|
|
func (t *Table) SetSelectionChangedFunc(handler func(row, column int)) *Table {
|
|
t.selectionChanged = 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)
|
|
if cell == nil {
|
|
continue
|
|
}
|
|
|
|
// 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.
|
|
previouslySelectedRow, previouslySelectedColumn := t.selectedRow, t.selectedColumn
|
|
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)
|
|
}
|
|
}
|
|
|
|
// If the selection has changed, notify the handler.
|
|
if t.selectionChanged != nil &&
|
|
(t.rowsSelectable && previouslySelectedRow != t.selectedRow ||
|
|
t.columnsSelectable && previouslySelectedColumn != t.selectedColumn) {
|
|
t.selectionChanged(t.selectedRow, t.selectedColumn)
|
|
}
|
|
}
|
|
}
|