Lysa UI  0.0
Lysa UI —UI components for the Lysa Engine
Grid and Table

Grid lays out its children in rows and columns of independent sizes each position being a GridCell container. Table builds on it by adding a row of column headers that sort and resize the columns.

Neither widget owns the data it displays: a cell is only a container and the value are stored in the user data of the widgets.

Table of contents


1. Creating a Grid

A Grid is a Widget drawn with a border:

// A grid of 3 rows and 4 columns, every row and column sharing the space
const auto grid = panel->create<lysa::ui::Grid>(
// OR an empty grid filled afterwards
const auto header = grid->addRow(24.0f); // returns the index of the row
const auto body = grid->addRow(); // shares the remaining height
grid->addColumn(120.0f);
grid->addColumn(); // shares the remaining width

The rows and the columns of the constructor are created when the grid is added to a Window, so the grid must be attached before addRow() or addColumn() is called.

Row 0 is the top row and column 0 the leftmost one, even though the coordinate space is Y-up:

column 0 column 1 column 2
┌────────────┬────────────┬────────────┐
row 0 │ cell │ cell │ cell │
├────────────┼────────────┼────────────┤
row 1 │ cell │ cell │ cell │
├────────────┼────────────┼────────────┤
row 2 │ cell │ cell │ cell │
└────────────┴────────────┴────────────┘

Rows and columns are inserted and removed at any position :

grid->insertRow(1, 24.0f); // the rows below are shifted down
grid->insertColumn(0); // the columns at its right are shifted right
grid->removeRow(2);
grid->removeColumn(0);
grid->removeAllRows(); // the columns are kept
grid->clear(); // rows, columns and cells

2. Row heights and column widths

Every row has a height and every column a width, expressed in UI units. A size of Grid::AUTO_SIZE (zero) means that the row or the column shares what is left :

grid->setRowHeight(0, 24.0f); // fixed height
grid->setRowHeight(1, lysa::ui::Grid::AUTO_SIZE);
grid->setColumnWidth(0, 150.0f);
grid->setColumnWidth(1, lysa::ui::Grid::AUTO_SIZE);
grid->setColumnWidth(2, lysa::ui::Grid::AUTO_SIZE); // same width as column 1

getRowHeight() and getColumnWidth() return the requested size, AUTO_SIZE included :

const auto height = grid->getRowRealHeight(1); // never AUTO_SIZE
const auto width = grid->getColumnRealWidth(1);
const auto bottom = grid->getRowPosition(1); // Y of the bottom border of the row
const auto left = grid->getColumnPosition(1); // X of the left border of the column

The cells are laid out inside the client rectangle of the grid (minus its borders and its padding). Sizes are clamped to that rectangle.


3. Cells and user data

A GridCell is a Widget : add the widgets to display into it with the usual create() / add() and alignments :

const auto cell = grid->getCell(1, 0);

A cell can be given a background of its own:

cell->setDrawBackground(true);
cell->setResource(std::make_shared<lysa::ui::StyleResource>(
"style=FLAT;color=0.2,0.2,0.3,1.0"));

The grid never owns a value, you have to use the user data:

cell->setUserData(entity.getId());
const auto entityId = cell->getUserData<lysa::unique_id>();

getCell() returns the cell displaying a position :

const auto cell = grid->getCell(row, column); // nullptr for an invalid position
const auto below = grid->getCellAt(x, y); // cell at a position of the UI, or nullptr
for (const auto& c : grid->getCells()) { /* ... */ }

4. Spanning cells

merge() turns a rectangle of positions into a single cell with the one at its top-left corner :

// The cell at (0, 0) now covers 1 row and 3 columns
grid->merge(0, 0, 1, 3);
// The cell at (1, 2) now covers 2 rows and 2 columns
grid->merge(1, 2, 2, 2);
┌──────────────────────────────────────┐
row 0 │ merge(0, 0, 1, 3) │
├────────────┬─────────────────────────┤
row 1 │ cell │ │
├────────────┤ merge(1, 2, 2, 2) │
row 2 │ cell │ │
└────────────┴─────────────────────────┘

merge() returns false when the rectangle is not entirely inside the grid.

split() removes the span of the cell displaying a position :

grid->split(0, 0);

A cell knows the rectangle it covers:

if (cell->isSpanning()) {
const auto rows = cell->getRowSpan();
const auto columns = cell->getColumnSpan();
}
const auto covered = cell->covers(2, 3);

5. Reordering rows

Rows are moved with the widgets they display and nothing is re-created. This is what makes a sort possible without the grid knowing anything about the data.

grid->swapRows(0, 3);
grid->moveRow(4, 0); // the row 4 becomes the top row
// order[0] is the row to display at the top, must be a permutation of [0, getRowCount()[
grid->reorderRows({2, 0, 1, 3});

6. Appearance of the grid

The lines between the cells are drawn above them so a cell with a background never hides them :

grid->setGridColor({0.4f, 0.4f, 0.4f, 1.0f});
grid->resetGridColor(); // back to the color of the style
grid->setGridVisible(false); // no lines at all

The style provides the default :

// Color of the lines between the cells, defaults to color_shadow_dark
style.setOption("color_grid_lines", "0.25,0.25,0.25,1.0");

7. Creating a Table

Table is a Grid with a row of column headers displayed above it and kept aligned with the columns. A header is a Button holding the title of the column and an Arrow showing the sort direction:

// addColumn(title, width, sortable, resizable) returns the index of the column
table->addColumn("Name", lysa::ui::Grid::AUTO_SIZE);
table->addColumn("Type", 120.0f);
table->addColumn("Size", 80.0f, true, false); // sortable, not resizable
table->addColumn("Lock", 30.0f, false, false); // neither
// A column can be titled with any widget, an icon for example
table->addColumn(std::make_shared<lysa::ui::IconSVG>("app://res/icons/lock.svg"), 30.0f);

Rows are added to the table itself :

const auto row = table->addRow(); // Table::DEFAULT_ROW_HEIGHT
table->addRow(32.0f); // an explicit height
table->setDefaultRowHeight(18.0f); // for the next rows added without one
const auto cell = table->getCell(row, 0);
cell->create<lysa::ui::Text>(lysa::ui::Alignment::LEFTCENTER, node.getName());
cell->setUserData(node.getId());
table->removeRow(row);
table->removeAllRows(); // the columns are kept

Set setDefaultRowHeight(Grid::AUTO_SIZE) to get back the behaviour of the grid, the rows sharing the height of the table.


8. Sorting the columns

Clicking the header of a sortable column changes the sorted column and the sort direction and emits UIEvent::OnSortColumn.

The table does not reorder the rows by itself**: it does not know what a cell displays. React to the signal and reorder the rows yourself or let sortRows() do it :

lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnSortColumn, table->id,
[table](const lysa::Event& e) {
const auto& payload = std::any_cast<lysa::ui::UIEventSort>(e.payload);
if (payload.column == lysa::ui::Table::NO_COLUMN) { return; }
table->sortRows(payload.column,
[](const lysa::ui::GridCell& a, const lysa::ui::GridCell& b) {
return a.getUserData<std::string>() < b.getUserData<std::string>();
});
});

The comparator returns true when the first cell must be displayed above the second one for the SortOrder::ASCENDING direction. sortRows() reverses the result when the table is currently sorted in SortOrder::DESCENDING.

getUserData<T>() throws when the cell has no user data of that type: give every cell of a sorted column its value or compare through the widgets of the cells instead.

The sort is also driven from the code, which emits the signal exactly like a click on a header:

const auto column = table->getSortedColumn(); // NO_COLUMN when not sorted
const auto order = table->getSortOrder();

9. Resizing the columns

Dragging the right border of the header of a resizable column changes the width of that column :

table->setGripWidth(8.0f);
table->setColumnResizable(2, false);
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnResizeColumn, table->id,
[](const lysa::Event& e) {
const auto& payload = std::any_cast<lysa::ui::UIEventColumn>(e.payload);
saveColumnWidth(payload.column, payload.width);
});
table->setColumnWidth(0, 200.0f);
table->setColumnWidth(0, lysa::ui::Grid::AUTO_SIZE);
const auto width = table->getColumnWidth(0); // the width currently used

10. Parts and resources of a Table

The two widgets built by the table are reachable :

const auto& grid = table->getGrid(); // the Grid displaying the content
const auto& header = table->getHeader(); // the widget holding the headers
// Merging cells, hiding the lines, ... go through the grid
grid->setGridVisible(false);
grid->merge(0, 0, 1, 2);

Each column keeps its header, its title and its sort indicator:

const auto button = table->getColumnHeader(0);
const auto title = table->getColumnTitle<lysa::ui::Text>(0);
const auto& columns = table->getColumns(); // header, title, indicator, sortable, resizable

The height of the row of headers is computed from the height of the titles unless it is set explicitly:

table->setHeaderHeight(24.0f);
table->setHeaderHeight(0.0f); // back to the automatic height

The resources of the headers of the grid and of the sort indicators are assigned by the style :

table->setResources("style=RAISED", "style=LOWERED", "width=8;height=8");
style.setOption("size_sort_arrow", "10");