diff --git a/demos/table/virtualtable/README.md b/demos/table/virtualtable/README.md new file mode 100644 index 0000000..2158c36 --- /dev/null +++ b/demos/table/virtualtable/README.md @@ -0,0 +1 @@ +![Screenshot](screenshot.png) \ No newline at end of file diff --git a/demos/table/virtualtable/main.go b/demos/table/virtualtable/main.go new file mode 100644 index 0000000..25dac72 --- /dev/null +++ b/demos/table/virtualtable/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "math" + + "github.com/rivo/tview" +) + +type TableData struct { + tview.TableContentReadOnly +} + +func (d *TableData) GetCell(row, column int) *tview.TableCell { + letters := [...]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'A' + byte(row%26)} // log(math.MaxInt64) / log(26) ~= 14 + start := len(letters) - 1 + row /= 26 + for row > 0 { + start-- + row-- + letters[start] = 'A' + byte(row%26) + row /= 26 + } + return tview.NewTableCell(fmt.Sprintf("[red]%s[green]%d", letters[start:], column)) +} + +func (d *TableData) GetRowCount() int { + return math.MaxInt64 +} + +func (d *TableData) GetColumnCount() int { + return math.MaxInt64 +} + +func main() { + data := &TableData{} + table := tview.NewTable(). + SetBorders(false). + SetSelectable(true, true). + SetContent(data) + if err := tview.NewApplication().SetRoot(table, true).EnableMouse(true).Run(); err != nil { + panic(err) + } +} diff --git a/demos/table/virtualtable/screenshot.png b/demos/table/virtualtable/screenshot.png new file mode 100644 index 0000000..edc6b89 Binary files /dev/null and b/demos/table/virtualtable/screenshot.png differ diff --git a/table.go b/table.go index 8395140..2e9bb49 100644 --- a/table.go +++ b/table.go @@ -187,6 +187,192 @@ func (c *TableCell) SetClickedFunc(clicked func() bool) *TableCell { return c } +// TableContent provides access to a Table's data. You may replace the Table +// class's default implementation with your own using the Table.SetContent() +// function. This will allow you to turn Table into a view of your own data +// structure. The Table.Draw() function which is called when the screen is +// updated, will then use the (read-only) functions of this interface to update +// the table. +// +// The interface's read-only functions are not called concurrently by the +// package (provided that users of the package don't call Table.Draw() in a +// separate goroutine, which would be uncommon and is not encouraged). +type TableContent interface { + // Return the cell at the given position or nil if there is no cell. The + // row and column arguments start at 0 and end at what GetRowCount() and + // GetColumnCount() return, minus 1. + GetCell(row, column int) *TableCell + + // Return the total number of rows in the table. + GetRowCount() int + + // Return the total number of columns in the table. + GetColumnCount() int + + // The following functions are provided for completeness reasons as the + // original Table implementation was not read-only. If you do not wish to + // forward modifying operations to your data, you may opt to leave these + // functions empty. To make this easier, you can include the + // TableContentReadOnly type in your struct. See also the demos/virtualtable + // example. + + // Set the cell at the given position to the provided cell. + SetCell(row, column int, cell *TableCell) + + // Remove the row at the given position by shifting all following rows up + // by one. Out of range positions may be ignored. + RemoveRow(row int) + + // Remove the column at the given position by shifting all following columns + // left by one. Out of range positions may be ignored. + RemoveColumn(column int) + + // Insert a new empty row at the given position by shifting all rows at that + // position and below down by one. Implementers may decide what to do with + // out of range positions. + InsertRow(row int) + + // Insert a new empty column at the given position by shifting all columns + // at that position and to the right by one to the right. Implementers may + // decide what to do with out of range positions. + InsertColumn(column int) + + // Remove all table data. + Clear() +} + +// TableContentReadOnly is an empty struct which implements the write operations +// if the TableContent interface. None of the implemented functions do anything. +// You can embed this struct into your own structs to free you from implementing +// the empty write functions of TableContent. See demos/virtualtable for an +// example. +type TableContentReadOnly struct{} + +// SetCell does not do anything. +func (t TableContentReadOnly) SetCell(row, column int, cell *TableCell) { + // nop. +} + +// RemoveRow does not do anything. +func (t TableContentReadOnly) RemoveRow(row int) { + // nop. +} + +// RemoveColumn does not do anything. +func (t TableContentReadOnly) RemoveColumn(column int) { + // nop. +} + +// InsertRow does not do anything. +func (t TableContentReadOnly) InsertRow(row int) { + // nop. +} + +// InsertColumn does not do anything. +func (t TableContentReadOnly) InsertColumn(column int) { + // nop. +} + +// Clear does not do anything. +func (t TableContentReadOnly) Clear() { + // nop. +} + +// tableDefaultContent implements the default TableContent interface for the +// Table class. +type tableDefaultContent struct { + // The cells of the table. Rows first, then columns. + cells [][]*TableCell + + // The rightmost column in the data set. + lastColumn int +} + +// Clear clears all data. +func (t *tableDefaultContent) Clear() { + t.cells = nil + t.lastColumn = -1 +} + +// SetCell sets a cell's content. +func (t *tableDefaultContent) SetCell(row, column int, cell *TableCell) { + 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 + } +} + +// RemoveRow removes a row from the data. +func (t *tableDefaultContent) RemoveRow(row int) { + if row < 0 || row >= len(t.cells) { + return + } + t.cells = append(t.cells[:row], t.cells[row+1:]...) +} + +// RemoveColumn removes a column from the data. +func (t *tableDefaultContent) RemoveColumn(column int) { + for row := range t.cells { + if column < 0 || column >= len(t.cells[row]) { + continue + } + t.cells[row] = append(t.cells[row][:column], t.cells[row][column+1:]...) + } +} + +// InsertRow inserts a new row at the given position. +func (t *tableDefaultContent) InsertRow(row int) { + if row >= len(t.cells) { + return + } + t.cells = append(t.cells, nil) // Extend by one. + copy(t.cells[row+1:], t.cells[row:]) // Shift down. + t.cells[row] = nil // New row is uninitialized. +} + +// InsertColumn inserts a new column at the given position. +func (t *tableDefaultContent) InsertColumn(column int) { + for row := range t.cells { + if column >= len(t.cells[row]) { + continue + } + t.cells[row] = append(t.cells[row], nil) // Extend by one. + copy(t.cells[row][column+1:], t.cells[row][column:]) // Shift to the right. + t.cells[row][column] = &TableCell{} // New element is an uninitialized table cell. + } +} + +// GetCell returns the cell at the given position. +func (t *tableDefaultContent) GetCell(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] +} + +// GetRowCount returns the number of rows in the data set. +func (t *tableDefaultContent) GetRowCount() int { + return len(t.cells) +} + +// GetColumnCount returns the number of columns in the data set. +func (t *tableDefaultContent) GetColumnCount() int { + if len(t.cells) == 0 { + return 0 + } + return t.lastColumn + 1 +} + // 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. @@ -248,11 +434,8 @@ type Table struct { // 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 table's data structure. + content TableContent // If true, when calculating the widths of the columns, all rows are evaluated // instead of only the visible ones. @@ -268,6 +451,10 @@ type Table struct { // The currently selected row and column. selectedRow, selectedColumn int + // A temporary flag which causes the next call to Draw() to force the + // current selection to remain visible. Set to false afterwards. + clampToSelection bool + // The number of rows/columns by which the table is scrolled down/to the // right. rowOffset, columnOffset int @@ -306,18 +493,36 @@ type Table struct { // NewTable returns a new table. func NewTable() *Table { - return &Table{ + t := &Table{ Box: NewBox(), bordersColor: Styles.GraphicsColor, separator: ' ', - lastColumn: -1, } + t.SetContent(nil) + return t +} + +// SetContent sets a new content type for this table. This allows you to back +// the table by a data structure of your own, for example one that cannot be +// fully held in memory. For details, see the TableContent interface +// documentation. +// +// A value of nil will return the table to its default implementation where all +// of its table cells are kept in memory. +func (t *Table) SetContent(content TableContent) *Table { + if content != nil { + t.content = content + } else { + t.content = &tableDefaultContent{ + lastColumn: -1, + } + } + return t } // Clear removes all table data. func (t *Table) Clear() *Table { - t.cells = nil - t.lastColumn = -1 + t.content.Clear() return t } @@ -427,6 +632,9 @@ func (t *Table) GetOffset() (row, column int) { // // Set this flag to true to avoid shifting column widths when the table is // scrolled. (May be slower for large tables.) +// +// Use with caution on very large tables, especially those not backed by the +// default TableContent data structure. func (t *Table) SetEvaluateAllRows(all bool) *Table { t.evaluateAllRows = all return t @@ -470,20 +678,7 @@ func (t *Table) SetDoneFunc(handler func(key tcell.Key)) *Table { // // 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 - } + t.content.SetCell(row, column, cell) return t } @@ -499,34 +694,24 @@ func (t *Table) SetCellSimple(row, column int, text string) *Table { // be inserted. Therefore, repeated calls to this function may return different // pointers for uninitialized cells. func (t *Table) GetCell(row, column int) *TableCell { - if row >= len(t.cells) || column >= len(t.cells[row]) { - return &TableCell{} + cell := t.content.GetCell(row, column) + if cell == nil { + cell = &TableCell{} } - return t.cells[row][column] + return cell } // RemoveRow removes the row at the given position from the table. If there is // no such row, this has no effect. func (t *Table) RemoveRow(row int) *Table { - if row < 0 || row >= len(t.cells) { - return t - } - - t.cells = append(t.cells[:row], t.cells[row+1:]...) - + t.content.RemoveRow(row) return t } // RemoveColumn removes the column at the given position from the table. If // there is no such column, this has no effect. func (t *Table) RemoveColumn(column int) *Table { - for row := range t.cells { - if column < 0 || column >= len(t.cells[row]) { - continue - } - t.cells[row] = append(t.cells[row][:column], t.cells[row][column+1:]...) - } - + t.content.RemoveColumn(column) return t } @@ -534,12 +719,7 @@ func (t *Table) RemoveColumn(column int) *Table { // given row and below will be shifted to the bottom by one row. If "row" is // equal or larger than the current number of rows, this function has no effect. func (t *Table) InsertRow(row int) *Table { - if row >= len(t.cells) { - return t - } - t.cells = append(t.cells, nil) // Extend by one. - copy(t.cells[row+1:], t.cells[row:]) // Shift down. - t.cells[row] = nil // New row is uninitialized. + t.content.InsertRow(row) return t } @@ -548,28 +728,18 @@ func (t *Table) InsertRow(row int) *Table { // column. Rows that have fewer initialized cells than "column" will remain // unchanged. func (t *Table) InsertColumn(column int) *Table { - for row := range t.cells { - if column >= len(t.cells[row]) { - continue - } - t.cells[row] = append(t.cells[row], nil) // Extend by one. - copy(t.cells[row][column+1:], t.cells[row][column:]) // Shift to the right. - t.cells[row][column] = &TableCell{} // New element is an uninitialized table cell. - } + t.content.InsertColumn(column) return t } // GetRowCount returns the number of rows in the table. func (t *Table) GetRowCount() int { - return len(t.cells) + return t.content.GetRowCount() } // 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 + return t.content.GetColumnCount() } // cellAt returns the row and column located at the given screen coordinates. @@ -591,7 +761,7 @@ func (t *Table) cellAt(x, y int) (row, column int) { if row >= t.fixedRows { row += t.rowOffset } - if row >= len(t.cells) { + if row >= t.content.GetRowCount() { row = -1 } } @@ -632,7 +802,7 @@ func (t *Table) ScrollToBeginning() *Table { func (t *Table) ScrollToEnd() *Table { t.trackEnd = true t.columnOffset = 0 - t.rowOffset = len(t.cells) + t.rowOffset = t.content.GetRowCount() return t } @@ -643,21 +813,16 @@ func (t *Table) Draw(screen tcell.Screen) { // What's our available screen space? _, totalHeight := screen.Size() x, y, width, height := t.GetInnerRect() + netWidth := width if t.borders { t.visibleRows = height / 2 + netWidth -= 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. + rowCount, columnCount := t.content.GetRowCount(), t.content.GetColumnCount() if t.rowsSelectable || t.columnsSelectable { if t.selectedColumn < 0 { t.selectedColumn = 0 @@ -665,27 +830,30 @@ func (t *Table) Draw(screen tcell.Screen) { if t.selectedRow < 0 { t.selectedRow = 0 } - for t.selectedRow < len(t.cells) { - cell := getCell(t.selectedRow, t.selectedColumn) + for t.selectedRow < rowCount { + cell := t.content.GetCell(t.selectedRow, t.selectedColumn) if cell != nil && !cell.NotSelectable { break } t.selectedColumn++ - if t.selectedColumn > t.lastColumn { + if t.selectedColumn > columnCount-1 { t.selectedColumn = 0 t.selectedRow++ } } } - // Clamp row offsets. - if t.rowsSelectable { + // Clamp row offsets if requested. + defer func() { + t.clampToSelection = false // Only once. + }() + if t.clampToSelection && 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 { + if t.selectedRow+1-t.rowOffset >= height/2 { t.rowOffset = t.selectedRow + 1 - height/2 t.trackEnd = false } @@ -696,52 +864,49 @@ func (t *Table) Draw(screen tcell.Screen) { } } } + if t.rowOffset < 0 { + t.rowOffset = 0 + } if t.borders { - if 2*(len(t.cells)-t.rowOffset) < height { + if rowCount-t.rowOffset < height/2 { t.trackEnd = true } } else { - if len(t.cells)-t.rowOffset < height { + if rowCount-t.rowOffset < height { t.trackEnd = true } } if t.trackEnd { if t.borders { - t.rowOffset = len(t.cells) - height/2 + t.rowOffset = rowCount - height/2 } else { - t.rowOffset = len(t.cells) - height + t.rowOffset = rowCount - 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 + // Avoid invalid column offsets. + if t.columnOffset >= columnCount-t.fixedColumns { + t.columnOffset = columnCount - t.fixedColumns - 1 } 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. + // Determine the indices of the rows which fit on the screen. var ( - columns, rows, allRows, widths []int - tableHeight, tableWidth int + rows, allRows []int + tableHeight 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. + rowStep = 2 // With borders, every table row takes two screen rows. } if t.evaluateAllRows { - allRows = make([]int, len(t.cells)) - for row := range t.cells { + allRows = make([]int, rowCount) + for row := 0; row < rowCount; row++ { allRows[row] = row } } @@ -753,60 +918,39 @@ func (t *Table) Draw(screen tcell.Screen) { tableHeight += rowStep return true } - for row := 0; row < t.fixedRows && row < len(t.cells); row++ { // Do the fixed rows first. + for row := 0; row < t.fixedRows && row < rowCount; 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. + for row := t.fixedRows + t.rowOffset; row < rowCount; row++ { // Then the remaining rows. if !indexRow(row) { break } } + + // Determine the columns' indices, widths, and expansion values that fit on + // the screen. var ( - skipped, lastTableWidth, expansionTotal int - expansions []int + tableWidth, expansionTotal int + columns, widths, 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. - } + includesSelection := !t.clampToSelection || !t.columnsSelectable - // 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:]...) + // Helper function that evaluates one column. Returns true if the column + // didn't fit at all. + indexColumn := func(column int) bool { + if netWidth == 0 || tableWidth >= netWidth { + return true } - // What's this column's width (without expansion)? - maxWidth := -1 - expansion := 0 + var maxWidth, expansion int evaluationRows := rows if t.evaluateAllRows { evaluationRows = allRows } for _, row := range evaluationRows { - if cell := getCell(row, column); cell != nil { + if cell := t.content.GetCell(row, column); cell != nil { _, _, _, _, _, _, cellWidth := decomposeString(cell.Text, true, false) if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth { cellWidth = cell.MaxWidth @@ -819,23 +963,107 @@ ColumnLoop: } } } - if maxWidth < 0 { - break // No more cells found in this column. + clampedMaxWidth := maxWidth + if tableWidth+maxWidth > netWidth { + clampedMaxWidth = netWidth - tableWidth } - - // Store new column info at the end. columns = append(columns, column) - widths = append(widths, maxWidth) - lastTableWidth = tableWidth - tableWidth += maxWidth + 1 + widths = append(widths, clampedMaxWidth) expansions = append(expansions, expansion) + tableWidth += clampedMaxWidth + 1 expansionTotal += expansion + if t.columnsSelectable && t.clampToSelection && column == t.selectedColumn { + // We want selections to appear fully. + includesSelection = clampedMaxWidth == maxWidth + } + + return false + } + + // Helper function that evaluates multiple columns, starting at "start" and + // at most ending at "maxEnd". Returns first column not included anymore (or + // -1 if all are included). + indexColumns := func(start, maxEnd int) int { + if start == maxEnd { + return -1 + } + + if start < maxEnd { + // Forward-evaluate columns. + for column := start; column < maxEnd; column++ { + if indexColumn(column) { + return column + } + } + return -1 + } + + // Backward-evaluate columns. + startLen := len(columns) + defer func() { + // Becaue we went backwards, we must reverse the partial slices. + for i, j := startLen, len(columns)-1; i < j; i, j = i+1, j-1 { + columns[i], columns[j] = columns[j], columns[i] + widths[i], widths[j] = widths[j], widths[i] + expansions[i], expansions[j] = expansions[j], expansions[i] + } + }() + for column := start; column >= maxEnd; column-- { + if indexColumn(column) { + return column + } + } + return -1 + } + + // Reset the table to only its fixed columns. + var fixedTableWidth, fixedExpansionTotal int + resetColumns := func() { + tableWidth = fixedTableWidth + expansionTotal = fixedExpansionTotal + columns = columns[:t.fixedColumns] + widths = widths[:t.fixedColumns] + expansions = expansions[:t.fixedColumns] + } + + // Add fixed columns. + if indexColumns(0, t.fixedColumns) < 0 { + fixedTableWidth = tableWidth + fixedExpansionTotal = expansionTotal + + // Add unclamped columns. + if column := indexColumns(t.fixedColumns+t.columnOffset, columnCount); !includesSelection || column < 0 && t.columnOffset > 0 { + // Offset is not optimal. Try again. + if !includesSelection { + // Clamp to selection. + resetColumns() + if t.selectedColumn <= t.fixedColumns+t.columnOffset { + // It's on the left. Start with the selection. + t.columnOffset = t.selectedColumn - t.fixedColumns + indexColumns(t.fixedColumns+t.columnOffset, columnCount) + } else { + // It's on the right. End with the selection. + if column := indexColumns(t.selectedColumn, t.fixedColumns); column >= 0 { + t.columnOffset = column + 1 - t.fixedColumns + } else { + t.columnOffset = 0 + } + } + } else if tableWidth < netWidth { + // Don't waste space. Try to fit as much on screen as possible. + resetColumns() + if column := indexColumns(columnCount-1, t.fixedColumns); column >= 0 { + t.columnOffset = column + 1 - t.fixedColumns + } else { + t.columnOffset = 0 + } + } + } } - t.columnOffset = skipped // If we have space left, distribute it. - if tableWidth < width { - toDistribute := width - tableWidth + if tableWidth < netWidth { + toDistribute := netWidth - tableWidth for index, expansion := range expansions { if expansionTotal <= 0 { break @@ -855,8 +1083,8 @@ ColumnLoop: // Draw the cells (and borders). var columnX int - if !t.borders { - columnX-- + if t.borders { + columnX++ } for columnIndex, column := range columns { columnWidth := widths[columnIndex] @@ -864,79 +1092,97 @@ ColumnLoop: if t.borders { // Draw borders. rowY *= 2 - for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ { - drawBorder(columnX+pos+1, rowY, Borders.Horizontal) + for pos := 0; pos < columnWidth && columnX+pos < width; pos++ { + drawBorder(columnX+pos, rowY, Borders.Horizontal) } ch := Borders.Cross - if columnIndex == 0 { - if rowY == 0 { + if row == 0 { + if column == 0 { ch = Borders.TopLeft } else { - ch = Borders.LeftT + ch = Borders.TopT } - } else if rowY == 0 { - ch = Borders.TopT + } else if column == 0 { + ch = Borders.LeftT } - drawBorder(columnX, rowY, ch) + drawBorder(columnX-1, rowY, ch) rowY++ if rowY >= height || y+rowY >= totalHeight { break // No space for the text anymore. } - drawBorder(columnX, rowY, Borders.Vertical) - } else if columnIndex > 0 { + drawBorder(columnX-1, rowY, Borders.Vertical) + } else if column < columnCount-1 { // Draw separator. - drawBorder(columnX, rowY, t.separator) + drawBorder(columnX+columnWidth, rowY, t.separator) } // Get the cell. - cell := getCell(row, column) + cell := t.content.GetCell(row, column) if cell == nil { continue } // Draw text. finalWidth := columnWidth - if columnX+1+columnWidth >= width { - finalWidth = width - columnX - 1 + if columnX+columnWidth >= width { + finalWidth = width - columnX } - cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth - _, printed, _, _ := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, 0, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color).Attributes(cell.Attributes), true) + cell.x, cell.y, cell.width = x+columnX, y+rowY, finalWidth + _, printed, _, _ := printWithStyle(screen, cell.Text, x+columnX, y+rowY, 0, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color).Attributes(cell.Attributes), true) if TaggedStringWidth(cell.Text)-printed > 0 && printed > 0 { - _, _, style, _ := screen.GetContent(x+columnX+finalWidth, y+rowY) - printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+finalWidth, 0, y+rowY, 1, AlignLeft, style, false) + _, _, style, _ := screen.GetContent(x+columnX+finalWidth-1, y+rowY) + printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+finalWidth-1, y+rowY, 0, 1, AlignLeft, style, false) } } // Draw bottom border. - if rowY := 2 * len(rows); t.borders && rowY < height { + if rowY := 2 * len(rows); t.borders && rowY > 0 && rowY < height { for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ { - drawBorder(columnX+pos+1, rowY, Borders.Horizontal) + drawBorder(columnX+pos, rowY, Borders.Horizontal) } - ch := Borders.BottomT - if columnIndex == 0 { + ch := Borders.Cross + if rows[len(rows)-1] == rowCount-1 { + if column == 0 { + ch = Borders.BottomLeft + } else { + ch = Borders.BottomT + } + } else if column == 0 { ch = Borders.BottomLeft } - drawBorder(columnX, rowY, ch) + drawBorder(columnX-1, rowY, ch) } columnX += columnWidth + 1 } // Draw right border. - if t.borders && len(t.cells) > 0 && columnX < width { + columnX-- + if t.borders && len(rows) > 0 && len(columns) > 0 && columnX < width { + lastColumn := columns[len(columns)-1] == columnCount-1 for rowY := range rows { rowY *= 2 if rowY+1 < height { drawBorder(columnX, rowY+1, Borders.Vertical) } - ch := Borders.RightT + ch := Borders.Cross if rowY == 0 { - ch = Borders.TopRight + if lastColumn { + ch = Borders.TopRight + } else { + ch = Borders.TopT + } + } else if lastColumn { + ch = Borders.RightT } drawBorder(columnX, rowY, ch) } if rowY := 2 * len(rows); rowY < height { - drawBorder(columnX, rowY, Borders.BottomRight) + ch := Borders.BottomT + if lastColumn { + ch = Borders.BottomRight + } + drawBorder(columnX, rowY, ch) } } @@ -984,7 +1230,7 @@ ColumnLoop: rowSelected := t.rowsSelectable && !t.columnsSelectable && row == t.selectedRow for columnIndex, column := range columns { columnWidth := widths[columnIndex] - cell := getCell(row, column) + cell := t.content.GetCell(row, column) if cell == nil { continue } @@ -1058,56 +1304,65 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi // Movement functions. previouslySelectedRow, previouslySelectedColumn := t.selectedRow, t.selectedColumn + lastColumn := t.content.GetColumnCount() - 1 + rowCount := t.content.GetRowCount() 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) + startRow := t.selectedRow + startColumn := t.selectedColumn + for { + cell := t.content.GetCell(t.selectedRow, t.selectedColumn) if cell != nil && !cell.NotSelectable { return } t.selectedColumn-- if t.selectedColumn < 0 { - t.selectedColumn = t.lastColumn t.selectedRow-- + if t.selectedRow < 0 { + t.selectedRow = rowCount - 1 + } + } + if t.selectedColumn == startColumn && t.selectedRow == startRow { + t.selectedColumn = -1 + t.selectedRow = -1 + return } } } 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 + startRow := t.selectedRow + startColumn := t.selectedColumn + for { + if t.selectedColumn <= lastColumn { + cell := t.content.GetCell(t.selectedRow, t.selectedColumn) + if cell != nil && !cell.NotSelectable { + return + } } - t.selectedColumn++ - if t.selectedColumn > t.lastColumn { + if t.selectedColumn >= lastColumn { t.selectedColumn = 0 - t.selectedRow++ + if t.selectedRow >= rowCount-1 { + t.selectedRow = 0 + } else { + t.selectedRow++ + } + } else { + t.selectedColumn++ + } + if t.selectedColumn == startColumn && t.selectedRow == startRow { + t.selectedColumn = -1 + t.selectedRow = -1 + return } } - t.selectedColumn = t.lastColumn - t.selectedRow = len(t.cells) - 1 - previous() } home = func() { if t.rowsSelectable { t.selectedRow = 0 t.selectedColumn = 0 + t.clampToSelection = true next() } else { t.trackEnd = false @@ -1118,8 +1373,9 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi end = func() { if t.rowsSelectable { - t.selectedRow = len(t.cells) - 1 - t.selectedColumn = t.lastColumn + t.selectedRow = rowCount - 1 + t.selectedColumn = lastColumn + t.clampToSelection = true previous() } else { t.trackEnd = true @@ -1130,9 +1386,10 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi down = func() { if t.rowsSelectable { t.selectedRow++ - if t.selectedRow >= len(t.cells) { - t.selectedRow = len(t.cells) - 1 + if t.selectedRow >= rowCount { + t.selectedRow = rowCount - 1 } + t.clampToSelection = true next() } else { t.rowOffset++ @@ -1145,6 +1402,7 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi if t.selectedRow < 0 { t.selectedRow = 0 } + t.clampToSelection = true previous() } else { t.trackEnd = false @@ -1156,8 +1414,13 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi if t.columnsSelectable { t.selectedColumn-- if t.selectedColumn < 0 { - t.selectedColumn = 0 + t.selectedColumn = lastColumn + t.selectedRow-- + if t.selectedRow < 0 { + t.selectedRow = rowCount - 1 + } } + t.clampToSelection = true previous() } else { t.columnOffset-- @@ -1167,6 +1430,7 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi right = func() { if t.columnsSelectable { t.selectedColumn++ + t.clampToSelection = true next() } else { t.columnOffset++ @@ -1181,9 +1445,10 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi if t.rowsSelectable { t.selectedRow += offsetAmount - if t.selectedRow >= len(t.cells) { - t.selectedRow = len(t.cells) - 1 + if t.selectedRow >= rowCount { + t.selectedRow = rowCount - 1 } + t.clampToSelection = true next() } else { t.rowOffset += offsetAmount @@ -1201,6 +1466,7 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi if t.selectedRow < 0 { t.selectedRow = 0 } + t.clampToSelection = true previous() } else { t.trackEnd = false @@ -1268,15 +1534,10 @@ func (t *Table) MouseHandler() func(action MouseAction, event *tcell.EventMouse, case MouseLeftClick: selectEvent := true row, column := t.cellAt(x, y) - if row >= 0 && row < len(t.cells) && column >= 0 { - cells := t.cells[row] - if column < len(cells) { - cell := cells[column] - if cell != nil && cell.Clicked != nil { - if noSelect := cell.Clicked(); noSelect { - selectEvent = false - } - } + cell := t.content.GetCell(row, column) + if cell != nil && cell.Clicked != nil { + if noSelect := cell.Clicked(); noSelect { + selectEvent = false } } if selectEvent && (t.rowsSelectable || t.columnsSelectable) {