1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267 |
- package tview
- import (
- "sort"
- "github.com/gdamore/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 reference object.
- Reference interface{}
- // 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
- // The style attributes of the cell.
- Attributes tcell.AttrMask
- // 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
- }
- // SetAttributes sets the cell's text attributes. You can combine different
- // attributes using bitmask operations:
- //
- // cell.SetAttributes(tcell.AttrUnderline | tcell.AttrBold)
- func (c *TableCell) SetAttributes(attr tcell.AttrMask) *TableCell {
- c.Attributes = attr
- return c
- }
- // SetStyle sets the cell's style (foreground color, background color, and
- // attributes) all at once.
- func (c *TableCell) SetStyle(style tcell.Style) *TableCell {
- c.Color, c.BackgroundColor, c.Attributes = style.Decompose()
- 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
- }
- // SetReference allows you to store a reference of any type in this cell. This
- // will allow you to establish a mapping between the cell and your
- // actual data.
- func (c *TableCell) SetReference(reference interface{}) *TableCell {
- c.Reference = reference
- return c
- }
- // GetReference returns this cell's reference object.
- func (c *TableCell) GetReference() interface{} {
- return c.Reference
- }
- // 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
- // If true, when calculating the widths of the columns, all rows are evaluated
- // instead of only the visible ones.
- evaluateAllRows bool
- // 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
- // The indices of the visible columns as of the last time the table was drawn.
- visibleColumnIndices []int
- // The net widths of the visible columns as of the last time the table was
- // drawn.
- visibleColumnWidths []int
- // The style of the selected rows. If this value is 0, selected rows are
- // simply inverted.
- selectedStyle tcell.Style
- // 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
- }
- // SetSelectedStyle sets a specific style for selected cells. If no such style
- // is set, per default, selected cells are inverted (i.e. their foreground and
- // background colors are swapped).
- //
- // To reset a previous setting to its default, make the following call:
- //
- // table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, 0)
- func (t *Table) SetSelectedStyle(foregroundColor, backgroundColor tcell.Color, attributes tcell.AttrMask) *Table {
- t.selectedStyle = tcell.StyleDefault.Foreground(foregroundColor).Background(backgroundColor) | tcell.Style(attributes)
- 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 Borders.Vertical (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. The "selection changed" event is fired if such a callback
- // is available (even if the selection ends up being the same as before and even
- // if cells are not selectable).
- func (t *Table) Select(row, column int) *Table {
- t.selectedRow, t.selectedColumn = row, column
- if t.selectionChanged != nil {
- t.selectionChanged(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
- t.trackEnd = false
- 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
- }
- // SetEvaluateAllRows sets a flag which determines the rows to be evaluated when
- // calculating the widths of the table's columns. When false, only visible rows
- // are evaluated. When true, all rows in the table are evaluated.
- //
- // Set this flag to true to avoid shifting column widths when the table is
- // scrolled. (May be slower for large tables.)
- func (t *Table) SetEvaluateAllRows(all bool) *Table {
- t.evaluateAllRows = all
- 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 current
- // selection changes. 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 content, 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 returned but it will be uninitialized if the cell
- // was not previously set. Such an uninitialized object will not automatically
- // 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{}
- }
- return t.cells[row][column]
- }
- // 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:]...)
- 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:]...)
- }
- return t
- }
- // InsertRow inserts a row before the row with the given index. Cells on the
- // 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.
- return t
- }
- // InsertColumn inserts a column before the column with the given index. Cells
- // in the given column and to its right will be shifted to the right by one
- // 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.
- }
- return t
- }
- // 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
- }
- // cellAt returns the row and column located at the given screen coordinates.
- // Each returned value may be negative if there is no row and/or cell. This
- // function will also process coordinates outside the table's inner rectangle so
- // callers will need to check for bounds themselves.
- func (t *Table) cellAt(x, y int) (row, column int) {
- rectX, rectY, _, _ := t.GetInnerRect()
- // Determine row as seen on screen.
- if t.borders {
- row = (y - rectY - 1) / 2
- } else {
- row = y - rectY
- }
- // Respect fixed rows and row offset.
- if row >= 0 {
- if row >= t.fixedRows {
- row += t.rowOffset
- }
- if row >= len(t.cells) {
- row = -1
- }
- }
- // Saerch for the clicked column.
- column = -1
- if x >= rectX {
- columnX := rectX
- if t.borders {
- columnX++
- }
- for index, width := range t.visibleColumnWidths {
- columnX += width + 1
- if x < columnX {
- column = t.visibleColumnIndices[index]
- break
- }
- }
- }
- return
- }
- // 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?
- _, totalHeight := screen.Size()
- 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, allRows, 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.
- }
- if t.evaluateAllRows {
- allRows = make([]int, len(t.cells))
- for row := range t.cells {
- allRows[row] = row
- }
- }
- 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
- evaluationRows := rows
- if t.evaluateAllRows {
- evaluationRows = allRows
- }
- for _, row := range evaluationRows {
- if cell := getCell(row, column); cell != nil {
- _, _, _, _, _, _, cellWidth := decomposeString(cell.Text, true, false)
- 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
- 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, Borders.Horizontal)
- }
- ch := Borders.Cross
- if columnIndex == 0 {
- if rowY == 0 {
- ch = Borders.TopLeft
- } else {
- ch = Borders.LeftT
- }
- } else if rowY == 0 {
- ch = Borders.TopT
- }
- drawBorder(columnX, 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 {
- // 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 := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color)|tcell.Style(cell.Attributes))
- if TaggedStringWidth(cell.Text)-printed > 0 && printed > 0 {
- _, _, style, _ := screen.GetContent(x+columnX+finalWidth, y+rowY)
- printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+finalWidth, y+rowY, 1, AlignLeft, style)
- }
- }
- // 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, Borders.Horizontal)
- }
- ch := Borders.BottomT
- if columnIndex == 0 {
- ch = Borders.BottomLeft
- }
- 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, Borders.Vertical)
- }
- ch := Borders.RightT
- if rowY == 0 {
- ch = Borders.TopRight
- }
- drawBorder(columnX, rowY, ch)
- }
- if rowY := 2 * len(rows); rowY < height {
- drawBorder(columnX, rowY, Borders.BottomRight)
- }
- }
- // Helper function which colors the background of a box.
- // backgroundColor == tcell.ColorDefault => Don't color the background.
- // textColor == tcell.ColorDefault => Don't change the text color.
- // attr == 0 => Don't change attributes.
- // invert == true => Ignore attr, set text to backgroundColor or t.backgroundColor;
- // set background to textColor.
- colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, attr tcell.AttrMask, invert 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)
- fg, bg, a := style.Decompose()
- if invert {
- 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 {
- bg = backgroundColor
- }
- if textColor != tcell.ColorDefault {
- fg = textColor
- }
- if attr != 0 {
- a = attr
- }
- style = style.Background(bg).Foreground(fg) | tcell.Style(a)
- }
- 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.
- type cellInfo struct {
- x, y, w, h int
- text tcell.Color
- selected bool
- }
- cellsByBackgroundColor := make(map[tcell.Color][]*cellInfo)
- 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, &cellInfo{
- 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
- })
- selFg, selBg, selAttr := t.selectedStyle.Decompose()
- for _, bgColor := range backgroundColors {
- entries := cellsByBackgroundColor[bgColor]
- for _, cell := range entries {
- if cell.selected {
- if t.selectedStyle != 0 {
- defer colorBackground(cell.x, cell.y, cell.w, cell.h, selBg, selFg, selAttr, false)
- } else {
- defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, 0, true)
- }
- } else {
- colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, tcell.ColorDefault, 0, false)
- }
- }
- }
- // Remember column infos.
- t.visibleColumnIndices, t.visibleColumnWidths = columns, widths
- }
- // 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() {
- offsetAmount := t.visibleRows - t.fixedRows
- if offsetAmount < 0 {
- offsetAmount = 0
- }
- if t.rowsSelectable {
- t.selectedRow += offsetAmount
- if t.selectedRow >= len(t.cells) {
- t.selectedRow = len(t.cells) - 1
- }
- next()
- } else {
- t.rowOffset += offsetAmount
- }
- }
- pageUp = func() {
- offsetAmount := t.visibleRows - t.fixedRows
- if offsetAmount < 0 {
- offsetAmount = 0
- }
- if t.rowsSelectable {
- t.selectedRow -= offsetAmount
- if t.selectedRow < 0 {
- t.selectedRow = 0
- }
- previous()
- } else {
- t.trackEnd = false
- t.rowOffset -= offsetAmount
- }
- }
- )
- 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)
- }
- })
- }
- // MouseHandler returns the mouse handler for this primitive.
- func (t *Table) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
- return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
- x, y := event.Position()
- if !t.InRect(x, y) {
- return false, nil
- }
- switch action {
- case MouseLeftClick:
- if t.rowsSelectable || t.columnsSelectable {
- t.Select(t.cellAt(x, y))
- }
- consumed = true
- setFocus(t)
- case MouseScrollUp:
- t.trackEnd = false
- t.rowOffset--
- consumed = true
- case MouseScrollDown:
- t.rowOffset++
- consumed = true
- }
- return
- })
- }
|