Collections
Table
Rows in columns that the user sorts, resizes and moves.
ui.Table puts a list's rows in columns, under a header of
ui.TableColumns, each with a title, a width (or 0 to share the room the
others leave) and an alignment; cell builds the content of a row's
column, usually a Text. It builds only the rows in view, as a list does,
and rows are as high as their tallest cell, so text that wraps makes its
row taller.
cols := []ui.TableColumn{{Title: "Name"}, {Title: "Size", Width: 90, Align: ui.End}}
app.files.Selected = &app.file
if ui.Table(c, &app.files, cols, len(files), func(row, col int) {
switch col {
case 0:
ui.Text(c, files[row].Name).SingleLine()
case 1:
ui.Text(c, files[row].Size())
}
}).Grow(1).Submitted() {
app.open(files[app.file])
}Choosing rows
The table takes the same ListState as a list, or nil, and takes the
keyboard focus for it: with Selected, a click or the keys choose a row,
with Selection several, and Submitted reports a double click or Enter;
Key, Label, Reorder, DragRows and the rest work as for a list. A row Header
names spans every column, which cell builds as column 0, and stays at the
top while its section's rows scroll under it.
Chosen and hovered rows are square from edge to edge, as in Finder's list
view; RoundRows rounds their corners at the theme's Radius, as a list's
rows are, for an app whose other lists are rounded.
Choosing cells
With Selection, CellSelect makes the table choose a block of cells, as a
spreadsheet does: the rows chosen, times the columns from Cells.Anchor to
Cells.Cursor in the order shown (Cells.All for every column). It is one
block: separate ranges of cells, as Excel's Cmd-drag adds, are not chosen.
- Clicks. A click on a cell chooses its row and its column; a Shift-click extends the rows and the columns to it; a Cmd-click (Ctrl-click on Linux and Windows) adds its row or takes it out and keeps the columns. A press on a cell outside the block chooses as the pointer goes down; a press inside the block waits for the release.
- Dragging over cells chooses those it goes over, and scrolls the table near its edges, down and sideways.
- Keys. Left and Right move
Cells.Cursor, Shift with them extends the columns; the keys that move the rows leave one column unless with Shift; Cmd+A chooses every row and every column. Right to left, Left goes to the column on the left of the screen. In anOutlineTable, Left and Right still open and close the rows, and the columns are chosen with the pointer. - Right-click. On a cell outside the block, it chooses that cell before
the context menu opens; inside, the block stays.
MenuKeepsSelectionleaves the block as it is. - Row headers. A
RowHeadercolumn, as a spreadsheet's row numbers, chooses whole rows: a click or a drag on its cells chooses rows with every column, and a click on its header every row. It does not sort.
The cells of the block show as a chosen row does, in the accent color with
the accent's text color, as do the row headers of whole rows chosen, and the
cell at Cells.Cursor in the row the keys move from has a border while the
table has the focus. A cell holding the focus, as an editor in it, shows as
it is. Chosen rows are not tinted whole. SelectedColumns returns the
block's column IDs in the order shown, and SelectColumns chooses every row
and a range of columns, for the app's own commands. CellSelected(row, col)
tells a cell builder whether its cell is chosen, for text of its own color
to show on the accent. A cell that reads its
row's click with RowClicked, or its double click with RowDoubleClicked,
leaves the click to the row, so the block follows it.
CellsStyle paints the block in colors of its own rather than the accent,
as a spreadsheet's two levels and stripes: Cell fills the block's cells,
CellText colors their text, Row fills the rest of the rows chosen, and
Stripe fills every other row. A zero color paints none. The cursor's
border takes CellText in the block and Cell out of it, or the accent
and its text where those are zero.
t := c.Theme()
app.grid.CellSelect = true
app.grid.CellsStyle = ui.CellsStyle{
Cell: t.Accent,
CellText: t.AccentText,
Row: t.Accent.Alpha(0.12),
Stripe: t.Text.Alpha(0.04),
}cols := []ui.TableColumn{{Title: "#", RowHeader: true, Width: 48, Fixed: true}, {Title: "Name"}, {Title: "Size", Width: 90}}
app.grid.Selection, app.grid.CellSelect = &app.chosen, true
ui.Table(c, &app.grid, cols, len(rows), func(row, col int) {
text, color := rows[row][col], c.Theme().Text
if text == "" {
text, color = "empty", c.Theme().TextMuted
}
if app.grid.CellSelected(row, col) {
color = c.Theme().AccentText // muted does not show on the accent
}
ui.Text(c, text).TextColor(color).SingleLine()
}).Grow(1)Arranging columns
The user arranges the columns, as in Finder:
- Resizing. Dragging the right edge of a column's header resizes the
column, between its
MinWidthandMaxWidth, and a double click there fits it to its cells. The columns before it that share the room left keep their widths, for the edge to follow the pointer. - Moving. Dragging a header moves its column among the others.
- Scrolling sideways. Columns wider than the table scroll sideways, the header with them.
- Fixed columns. A
Fixedcolumn stays where it is, as wide as it is.
ListState.Columns keeps the order and the widths, by the columns' IDs
(their titles by default): set it from saved settings to restore them, and
save it when the table's Changed reports it. Columns changes as the
pointer moves, but Changed reports it once, as the user lets go of a header
or an edge, when the order or a width is new, and once a double click fits a
column to its cells, so the settings are written once per gesture:
if ui.Table(c, &app.files, cols, len(files), cell).Grow(1).Changed() {
app.settings.Columns = app.files.Columns
app.saveSettings()
}With Sort or SortBy set too, Changed also reports a new sort: save
both when it reports either.
Sorting
With ListState.Sort, a click on the header of a Sortable column sorts
the rows by it, ascending, and a second click reverses the order: the header
shows an arrow, assistive technology reads the order, and the table's
Changed reports it. The table shows the rows in the order cell gets
them, so sort them as it says, keyed by Key for the choice to follow its
rows:
cols := []ui.TableColumn{{Title: "Name", Sortable: true}, {Title: "Size", Width: 90, Align: ui.End, Sortable: true}}
app.files.Sort = &app.sort
sorted := sortFiles(app.files, app.sort) // by app.sort.Column, Descending or not
app.files.Key = func(i int) any { return sorted[i].ID }To sort by several columns, set ListState.SortBy. A click still sorts by
one column, and reverses it when it was first; a Cmd-click (Ctrl-click on
Linux and Windows) or a Shift-click, as in web data grids, adds the column
after those sorted by, ascending; another makes it descending, and a third
takes it out. The headers show their direction and, when there are several,
their place. SortBy gets a new slice as it changes, so one the app kept
stays as it was. With Sort set too, the table keeps it the first of
SortBy, the zero SortOrder when it sorts by none, so code that reads the
column sorting first goes on reading Sort:
app.files.SortBy = &app.sortBy // []ui.SortOrder, the first first
sorted := sortFiles(app.files, app.sortBy...)Header menu
ListState.HeaderMenu gives the header a context menu, built as
ContextMenu's is. col is the index of the column
clicked in the columns given to Table, whatever order the user moved them
into, or -1 for the part of the header past the columns, as AppKit's
clickedColumn. A menu built without items does not open:
app.files.HeaderMenu = func(m *ui.Menu, col int) {
if col >= 0 && m.Item("Hide Column").Chosen() {
app.hide(cols[col].ID)
}
if m.Item("Show All Columns").Chosen() {
app.showAll()
}
}Header cells
ListState.HeaderCell is called as the table builds each column's header
cell, with the cell and the column's index, as HeaderMenu's: what it
builds goes in the cell after the title and the sort arrow, and it may
give the cell a tooltip or a card of its own:
app.files.HeaderCell = func(cell ui.Element, col int) {
cell.Tooltip(app.columnTypes[col])
}Cell menu
ListState.CellMenu gives the cells a context menu over the whole cell,
padding included, built as ContextMenu's is. row and
col are as the cell builder receives them. A menu built without items does
not open, and a context menu of what a cell holds takes the right-click over
it instead:
app.grid.CellMenu = func(m *ui.Menu, row, col int) {
if m.Item("Copy").Chosen() {
app.copy(row, col)
}
}Renaming in place
An editable text in a cell is renamed in place, as Finder's file names: Return on macOS and F2 elsewhere edit the chosen row's text, as does a click on it once the row is chosen.
ui.Table(c, &app.files, cols, len(files), func(row, col int) {
if col == 0 && ui.EditableText(c, &files[row].Name).Changed() {
app.rename(files[row])
}
})Accessibility
Assistive technology sees a table, named by its Label, of rows named by
the text inside them, each saying which of all the rows it is, with column
headers that say the order the rows are sorted in, and the cells of a
RowHeader column naming their rows as row headers. With CellSelect,
the cells of the block say they are chosen.
For a tree of rows in columns, see Outline.