aboutsummaryrefslogtreecommitdiff
path: root/vendor/maunium.net/go/tview/table.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/maunium.net/go/tview/table.go')
-rw-r--r--vendor/maunium.net/go/tview/table.go1025
1 files changed, 1025 insertions, 0 deletions
diff --git a/vendor/maunium.net/go/tview/table.go b/vendor/maunium.net/go/tview/table.go
new file mode 100644
index 0000000..4f2336f
--- /dev/null
+++ b/vendor/maunium.net/go/tview/table.go
@@ -0,0 +1,1025 @@
+package tview
+
+import (
+ "sort"
+
+ "maunium.net/go/tcell"
+ colorful "github.com/lucasb-eyer/go-colorful"
+)
+
+// 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
+
+ // If the total table width is less than the available width, this value is
+ // used to add extra width to a column. See SetExpansion() for details.
+ Expansion 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
+}
+
+// SetExpansion sets the value by which the column of this cell expands if the
+// available width for the table is more than the table width (prior to applying
+// this expansion value). This is a proportional value. The amount of unused
+// horizontal space is divided into widths to be added to each column. How much
+// extra width a column receives depends on the expansion value: A value of 0
+// (the default) will not cause the column to increase in width. Other values
+// are proportional, e.g. a value of 2 will cause a column to grow by twice
+// the amount of a column with a value of 1.
+//
+// Since this value affects an entire column, the maximum over all visible cells
+// in that column is used.
+//
+// This function panics if a negative value is provided.
+func (c *TableCell) SetExpansion(expansion int) *TableCell {
+ if expansion < 0 {
+ panic("Table cell expansion values may not be negative")
+ }
+ c.Expansion = expansion
+ 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.
+//
+// Use SetInputCapture() to override or modify keyboard input.
+//
+// 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
+}
+
+// GetOffset returns the current row and column offset. This indicates how many
+// rows and columns the table is scrolled down and to the right.
+func (t *Table) GetOffset() (row, column int) {
+ return t.rowOffset, t.columnOffset
+}
+
+// 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, NewTableCell(text))
+ 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, expansionTotal int
+ expansions []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:]...)
+ expansions = append(expansions[:t.fixedColumns], expansions[t.fixedColumns+1:]...)
+ }
+
+ // What's this column's width (without expansion)?
+ maxWidth := -1
+ expansion := 0
+ for _, row := range rows {
+ if cell := getCell(row, column); cell != nil {
+ cellWidth := StringWidth(cell.Text)
+ if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth {
+ cellWidth = cell.MaxWidth
+ }
+ if cellWidth > maxWidth {
+ maxWidth = cellWidth
+ }
+ if cell.Expansion > expansion {
+ expansion = cell.Expansion
+ }
+ }
+ }
+ 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
+ expansions = append(expansions, expansion)
+ expansionTotal += expansion
+ }
+ t.columnOffset = skipped
+
+ // If we have space left, distribute it.
+ if tableWidth < width {
+ toDistribute := width - tableWidth
+ for index, expansion := range expansions {
+ if expansionTotal <= 0 {
+ break
+ }
+ expWidth := toDistribute * expansion / expansionTotal
+ widths[index] += expWidth
+ tableWidth += expWidth
+ toDistribute -= expWidth
+ expansionTotal -= expansion
+ }
+ }
+
+ // Helper function which draws border runes.
+ borderStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.bordersColor)
+ drawBorder := func(colX, rowY int, ch rune) {
+ screen.SetContent(x+colX, y+rowY, ch, nil, borderStyle)
+ }
+
+ // Draw the cells (and borders).
+ var columnX int
+ if !t.borders {
+ columnX--
+ }
+ for columnIndex, column := range columns {
+ columnWidth := widths[columnIndex]
+ for rowY, row := range rows {
+ if t.borders {
+ // Draw borders.
+ rowY *= 2
+ for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ {
+ drawBorder(columnX+pos+1, rowY, GraphicsHoriBar)
+ }
+ ch := GraphicsCross
+ if columnIndex == 0 {
+ if rowY == 0 {
+ ch = GraphicsTopLeftCorner
+ } else {
+ ch = GraphicsLeftT
+ }
+ } else if rowY == 0 {
+ ch = GraphicsTopT
+ }
+ drawBorder(columnX, rowY, ch)
+ rowY++
+ if rowY >= height {
+ break // No space for the text anymore.
+ }
+ drawBorder(columnX, rowY, GraphicsVertBar)
+ } else if columnIndex > 0 {
+ // Draw separator.
+ drawBorder(columnX, rowY, t.separator)
+ }
+
+ // Get the cell.
+ cell := getCell(row, column)
+ if cell == nil {
+ continue
+ }
+
+ // Draw text.
+ finalWidth := columnWidth
+ if columnX+1+columnWidth >= width {
+ finalWidth = width - columnX - 1
+ }
+ cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth
+ _, printed := Print(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, cell.Color)
+ if StringWidth(cell.Text)-printed > 0 && printed > 0 {
+ _, _, style, _ := screen.GetContent(x+columnX+1+finalWidth-1, y+rowY)
+ fg, _, _ := style.Decompose()
+ Print(screen, string(GraphicsEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, fg)
+ }
+ }
+
+ // 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)
+ }
+ ch := GraphicsBottomT
+ if columnIndex == 0 {
+ ch = GraphicsBottomLeftCorner
+ }
+ drawBorder(columnX, rowY, ch)
+ }
+
+ columnX += columnWidth + 1
+ }
+
+ // Draw right border.
+ if t.borders && len(t.cells) > 0 && columnX < width {
+ for rowY := range rows {
+ rowY *= 2
+ if rowY+1 < height {
+ drawBorder(columnX, rowY+1, GraphicsVertBar)
+ }
+ ch := GraphicsRightT
+ if rowY == 0 {
+ ch = GraphicsTopRightCorner
+ }
+ drawBorder(columnX, rowY, ch)
+ }
+ if rowY := 2 * len(rows); rowY < height {
+ drawBorder(columnX, rowY, GraphicsBottomRightCorner)
+ }
+ }
+
+ // Helper function which colors the background of a box.
+ colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, selected bool) {
+ for by := 0; by < h && fromY+by < y+height; by++ {
+ for bx := 0; bx < w && fromX+bx < x+width; bx++ {
+ m, c, style, _ := screen.GetContent(fromX+bx, fromY+by)
+ if selected {
+ fg, _, _ := style.Decompose()
+ if fg == textColor || fg == t.bordersColor {
+ fg = backgroundColor
+ }
+ if fg == tcell.ColorDefault {
+ fg = t.backgroundColor
+ }
+ style = style.Background(textColor).Foreground(fg)
+ } else {
+ if backgroundColor == tcell.ColorDefault {
+ continue
+ }
+ style = style.Background(backgroundColor)
+ }
+ screen.SetContent(fromX+bx, fromY+by, m, c, style)
+ }
+ }
+ }
+
+ // Color the cell backgrounds. To avoid undesirable artefacts, we combine
+ // the drawing of a cell by background color, selected cells last.
+ cellsByBackgroundColor := make(map[tcell.Color][]*struct {
+ x, y, w, h int
+ text tcell.Color
+ selected bool
+ })
+ var backgroundColors []tcell.Color
+ for rowY, row := range rows {
+ columnX := 0
+ rowSelected := t.rowsSelectable && !t.columnsSelectable && row == t.selectedRow
+ for columnIndex, column := range columns {
+ columnWidth := widths[columnIndex]
+ cell := getCell(row, column)
+ if cell == nil {
+ continue
+ }
+ bx, by, bw, bh := x+columnX, y+rowY, columnWidth+1, 1
+ if t.borders {
+ by = y + rowY*2
+ bw++
+ bh = 3
+ }
+ columnSelected := t.columnsSelectable && !t.rowsSelectable && column == t.selectedColumn
+ cellSelected := !cell.NotSelectable && (columnSelected || rowSelected || t.rowsSelectable && t.columnsSelectable && column == t.selectedColumn && row == t.selectedRow)
+ entries, ok := cellsByBackgroundColor[cell.BackgroundColor]
+ cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &struct {
+ x, y, w, h int
+ text tcell.Color
+ selected bool
+ }{
+ x: bx,
+ y: by,
+ w: bw,
+ h: bh,
+ text: cell.Color,
+ selected: cellSelected,
+ })
+ if !ok {
+ backgroundColors = append(backgroundColors, cell.BackgroundColor)
+ }
+ columnX += columnWidth + 1
+ }
+ }
+ sort.Slice(backgroundColors, func(i int, j int) bool {
+ // Draw brightest colors last (i.e. on top).
+ r, g, b := backgroundColors[i].RGB()
+ c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255}
+ _, _, li := c.Hcl()
+ r, g, b = backgroundColors[j].RGB()
+ c = colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255}
+ _, _, lj := c.Hcl()
+ return li < lj
+ })
+ for _, bgColor := range backgroundColors {
+ entries := cellsByBackgroundColor[bgColor]
+ for _, cell := range entries {
+ if cell.selected {
+ defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, true)
+ } else {
+ colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, false)
+ }
+ }
+ }
+}
+
+// InputHandler returns the handler for this primitive.
+func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ return t.WrapInputHandler(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)
+ }
+ })
+}