ClaFi Grid

Building grids was never that simple

Introduction

Device Name Channel Level Range Mode Amount Outputs Mixer Input Output Monitor 0.80 0.65 1.00 Scale Offset No change Master bus 0.50 Set Inputs 1 1 2 3 4 5 5 1 Device and Range have sub-columns, so the header calculates two rows of cells. 2 Level has no sub-columns, so its header cell fills the full header height. 3 A group's span row fills one column across all of its rows. 4 This row fills Device itself, so one cell covers Name and Channel. 5 Expanders open and close a whole section.

The structure you see in that picture — different column levels, visual cell spans and merges, groups — falls out of two hierarchies, a tree of columns and a tree of rows, plus one fact per cell: whether that row fills that column.

You describe all of it — columns, rows, and the contents of every cell — as a single nested declaration:

Grid& m_grid{ add<Grid>(
    Columns{ /* the column tree */ },
    Header{}, // The grid header is optional, this line creates one
    Rows{ /* the rows, and the cells in them */ }
) };

The grid builds itself from that list and, for each row, connects a pair of handlers to that row: one answering which columns the row fills, one supplying the text of its cells. The grid asks the row while it lays out, paints and hit-tests, so there is no second place to keep in sync. The row owns those connections, so nothing outside the grid has to stay alive.

The one idea to carry through this document: a grid is a sparse matrix. Which cells exist is the most important thing about it — it drives layout, painting, merging, hit testing and keyboard navigation. With the Blueprint, a row's cells are exactly the ones you list, and nothing else.

Quick Start

A grid with two columns and two rows. This is the whole thing:

import ClaFi.Controls.Grids_Dt;
import ClaFi.Controls.Grids;
import ClaFi.Controls.Stack;
import ClaFi.Core.TextEngine.Text;
import ClaFi.Core.Foundation;
import ClaFi.Core.System.UiTypes;

namespace MyApp
{
    using namespace ::ClaFi;
    using namespace ::ClaFi::Controls;
    using namespace ::ClaFi::Controls::Grids::Dt;

    enum class Col
    {
        Name,
        Size
    };

    class FilesPage : public Stack
    {
    public:
        explicit FilesPage(const CreateParams&);
    private:
        Grid& m_grid{ add<Grid>(
            Columns{
                Column{ Tag{ Col::Name }, Text{ L"Name" } },
                Column{ Tag{ Col::Size }, Text{ L"Size" } }
            },
            Header{},
            Rows{
                Row{
                    Cell{ Tag{ Col::Name }, [](Grids::GetCellTextEvent& event) {
                        event.text() << L"readme.txt";
                    } },
                    Cell{ Tag{ Col::Size }, [](Grids::GetCellTextEvent& event) {
                        event.text() << L"1.2 KB";
                    } }
                },
                Row{
                    Cell{ Tag{ Col::Name }, [](Grids::GetCellTextEvent& event) {
                        event.text() << L"notes.md";
                    } },
                    Cell{ Tag{ Col::Size }, [](Grids::GetCellTextEvent& event) {
                        event.text() << L"640 B";
                    } }
                }
            }
        ) };
    };

    FilesPage::FilesPage(const CreateParams& params)
        :
        Stack{ params, Orientation::Vertical }
    {
    }
}
Three namespaces. The grid's runtime classes and its events live in ClaFi::Controls::Grids — plural, so that Grid stays free as a class name. The blueprint layer is ClaFi::Controls::Grids::Dt. The machinery both of the framework's blueprint layers share — OnEvent, Init, the node bases, the pack routing — lives in ClaFi::Dt, and Grids::Dt re-exports the parts you name directly.

Pull the blueprint layer in once, in the namespace your page lives in, and write every node unqualified after that:
namespace MyApp
{
    using namespace ::ClaFi;
    using namespace ::ClaFi::Controls;
    using namespace ::ClaFi::Controls::Grids::Dt;
Two nodes are written in full, Grids::Dt::Expander and Grids::Dt::Divider. Wherever Controls::Expander or Controls::Divider is visible, the bare name is ambiguous. The other nodes are written bare.

The events a provider or a handler takes are not nodes, and they stay in Grids:: - Grids::GetCellTextEvent, Grids::CellContentEvent, Grids::NewItemEvent. So do Grids::GridLines and Grids::ViewMode. Spell a node Grids::Dt:: in full rather than Dt::. Under using namespace ::ClaFi, Dt:: names ClaFi::Dt, which holds none of them.

Grids_Dt also publishes Grids::Rt::, aliases for the runtime types a blueprint mentions: Rt::Column, Rt::RowBase, Rt::RowContainer. The same classes, spelled so that a blueprint reads visibly differently from the live objects it describes.
The imports. import ClaFi.Controls.Grids_Dt; brings the blueprint layer, with OnEvent and Init re-exported from ClaFi::Dt. import ClaFi.Controls.Grids; brings the runtime classes and the events. The properties come from their own modules: Tag from ClaFi.Core.System.UiTypes, Text from ClaFi.Core.TextEngine.Text, CreateParams from ClaFi.Core.Foundation. clang wants each one imported by the file that names it, even where MSVC lets the name through from another import.

Four things to notice, because they apply everywhere:

Text usually comes from your data rather than a literal. Point the cell at a member function and it will be called whenever that cell needs drawing:

void FilesPage::nameCell(Grids::GetCellTextEvent& event) const
{
    event.text() << m_files[event.row().tag<std::size_t>()].name;
}

Row FilesPage::fileRow(std::size_t index) const
{
    return Row{
        Tag{ index },
        Cell{ Tag{ Col::Name }, this, &FilesPage::nameCell }
    };
}

Tag is yours to use however you like — here it carries the row's index into the model. The grid never reads it.

Core Concepts

Columns

Columns form a tree. Any column can have sub-columns:

Column{ Tag{ Col::Range }, Text{ L"Range" },
    Column{ Tag{ Col::RangeMode },   Text{ L"Mode" } },
    Column{ Tag{ Col::RangeAmount }, Text{ L"Amount" } }
}

Anything in a Column that is not another Column is a property. The useful ones:

PropertyEffect
Text{ ... }The header label, and the default text of the header cell.
ShowInHeader::Yes / NoWhether the header row draws a cell here. Default Yes.
ColumnWidthMode::FitContentAs wide as its widest cell. The default.
ColumnWidthMode::FixedAs wide as the float you also pass.
ColumnWidthMode::FillTakes a share of the leftover width. With several Fill siblings the float acts as a ratio.
TextAlign::...Alignment for every cell in the column, the header's included.
TextFormat{ ... }What every data cell of the column is laid out over - a style, a colour, an alignment. The header takes the alignment alone. See Cells and content.
Tag{ ... }Names the column, so a cell can refer to it and columnByTag can find it afterwards. Unique within the grid. Reaches the column like any other property.

Rows

Rows appear in the order you declare them. Besides plain Row there are containers that hold other rows:

NodeProduces
Header{}The header row. Draws a cell in every column whose ShowInHeader is Yes, using that column's Text.
Row{ ... }An ordinary row. Can hold text cells and live controls.
Divider{}A thin band between rows, closed by a grid line.
NewItem{ PlaceHolderText{...} }A muted line kept below the last row. Pressing it asks you for a new item - see Adding items from the grid.
Expander{ Header{...}, rows... }A collapsible header with rows beneath it.
Group{ Span{...}, rows... }A span row laid over its rows, so its cells span them vertically.
Rows{ ... }Grouping only — adds nesting in your source, not in the grid.

Cells and content

The Text handed to a provider is a stream the grid clears once per query, and providers append to it. Several levels can contribute to one cell — the cell's own provider, then a grid-wide OnGetCellText handler. One that wants to replace what an earlier one wrote calls event.text().clear() first.

A row's cells are exactly the ones you declare. Four forms, in one row:

Row{
    // The cell exists and is empty. It still draws a border and can be hovered.
    Cell{ Tag{ Col::Amount } },

    // Text from a lambda.
    Cell{ Tag{ Col::Name }, [](Grids::GetCellTextEvent& event) {
        event.text() << L"...";
    } },

    // Text from a member function - the usual choice.
    Cell{ Tag{ Col::Channel }, this, &Page::channelCell },

    // A live control living in the cell. Its constructor arguments follow the Tag.
    CellWith<CheckBox>{ Tag{ Col::Mute } }
}

A text provider receives the grid's Grids::GetCellTextEvent - the same event a grid-wide OnGetCellText handler takes, so one method serves at either level:

MemberWhat it gives
event.row()const Rt::RowBase& - which row. Read its Tag to find your data.
event.column()const Rt::Column& - which column.
event.text()Text& - write here, appending.
event.stopPropagation()Keeps the handlers behind this one from seeing the cell.
Not declaring a cell is different from declaring an empty one. A column you skip has no cell in that row: nothing is drawn, and either a neighbouring cell covers the space or it stays blank. A bare Cell{ Tag{ ... } } gives you a real, bordered, hoverable cell that happens to be empty. Both are useful; pick deliberately.

A look every cell of a column shares belongs to the column, not to each provider. Stated as the column's TextFormat - see the Text in Controls manual - it is laid out under the text every cell writes, and it reaches a cell's tooltip and its in-place editor the way it reaches the paint:

Column{ Tag{ Reading::Last }, Text{ L"Last" }, TextAlign::Right, TextFormat{ TextStyleId::Code } }

void FpsPage::writeReading(Grids::GetCellTextEvent& event) const
{
    // The value alone - the column states the style.
    event.text() << Fmt{ L"{}[color muted]{}[/color]", number, unit };
}

A TextAlign the column takes goes in ahead of its TextFormat, so an alignment the format states wins. The header cell is laid out over the alignment alone: it names the column and holds none of its values. None of the format is written into the text a provider receives, so a value accepted from the editor holds what was typed and nothing else.

Merging

Cells grow to cover their neighbours in two ways. Neither is configured — both follow from which cells you declared.

A column whose cell covers its sub-columns

Device Name Channel Level Range Mode Amount header Master bus no cells in Name or Channel 0.50 Scale Offset 0.25 one row level 0 level 1

If a row fills a column but none of that column's sub-columns, the cell spans downward over them. The result is always a plain rectangle, because sub-columns exactly cover their parent's width.

This is how one row shows a single wide label while its neighbour shows two separate values under the same parent column.

A group's span row, whose cell covers all of its rows

Device Name Channel Level Range Mode Amount header Mixer group span row Input Output Monitor Aux the span row fills Name, the rows fill Channel and the value columns

A Group's span row is stretched to the full height of the rows inside it and drawn across them. They do not collide because the span row and its rows fill different columns: the span row owns the columns that should read as one tall cell, the rows own the rest.

Design rule for a group: the span row and its rows must declare disjoint sets of columns whose union covers everything the group should show. A column claimed by neither leaves a gap; a column claimed by both is drawn twice.

Widths

Each column's width comes from its ColumnWidthMode:

A parent column is at least as wide as its sub-columns combined, and the leftover width is distributed among the Fill ones.

Give at least one sub-column in each group ColumnWidthMode::Fill. If none of them can stretch, they keep their natural widths and may not reach their parent's right edge, leaving a narrow unpainted strip there.

Tags

A Tag is the one identifier in play. On a column it is the name cells use to find it; on a row or a control it is yours to use however you like.

On a columnOn a row or a control
Read bythe blueprint, and columnByTagnobody but you
Purposelets a cell name the columnwhatever you want to attach
Unique?yes, within one gridno requirement at all
Reaches the object?yes, forwarded like any propertyyes, like any property
Column{ Tag{ Col::Level },   // how cells name this column
    Text{ L"Level" }
}

Row{ Tag{ Kind::Channel, channel },   // yours - the grid never reads it
    Cell{ Tag{ Col::Level }, this, &Page::levelCell }
}

Tag packs several values into one, so a column name can be compound: Tag{ SectionId::Audio, Col::Level }. Reading a segment back is tag.get<index, count, Enum>(), or row.tag<index, count, Enum>() on a row.

Because one type serves both jobs, nothing stops a cell from naming a row's tag by mistake. The blueprint cannot catch that at compile time; it shows up as an unresolved cell, which a debug build throws on. If a column is meant to be nameable, give it a Tag drawn from the enum your cells use, and keep that enum separate from your row tags.
A column declared without a Tag is legal — it simply cannot be named by a cell. Note that Tag{} holding zero is a perfectly ordinary tag: the first enumerator of a plain enum is zero, so absence is tracked by whether you passed the property at all, not by its value.

Blueprint Reference

import ClaFi.Controls.Grids_Dt;
import ClaFi.Controls.Grids;

namespace MyApp
{
    using namespace ::ClaFi;
    using namespace ::ClaFi::Controls;
    using namespace ::ClaFi::Controls::Grids::Dt;
}

Grid

template <typename... Args>
explicit Grid(const CreateParams& params, Args&&... args);

Grid is the grid itself: an Rt::Grid whose constructor builds the columns, the header and the rows it is handed. The grid's own properties and handlers go in the same list. Add it where any control is added:

Grid& m_grid{ add<Grid>(
    Grids::GridLines::Horizontal,
    OnEvent{ [this](Grids::GetCellTooltipEvent& event) {
        writeHint(event);
    } },
    Columns{ /* ... */ },
    Header{},
    Rows{ /* ... */ }
) };
Columns are always applied before rows, whatever order you write them in, because cells resolve their tags by lookup. Header{} is added ahead of the first row wherever it is written, and NewItem{} stays after the last one. The rows keep the order they are written in.
The description is not kept. The nodes are spent in the constructor, and what stands afterwards is an ordinary grid. Columns carry their tags and rows own their cell connections, both reached through the grid - see Finding things later. The constructor throws std::logic_error if two columns share a tag, or if a cell names a tag no column carries. It checks both only where _DEBUG is defined, which is the Debug configuration under Visual Studio. Other builds skip them.

Column and Columns

Column{ Tag{ ... }, properties..., Column{ ... }, Column{ ... } }

The Tag names the column and may sit anywhere in the argument list — it is an ordinary property, read by the blueprint on its way through. Everything that is not a nested Column is a property, passed straight through to the column. Nested Column nodes become sub-columns.

A column without a Tag is allowed — it simply cannot be named by a cell:

Column{ Text{ L"decorative" }, ShowInHeader::Yes }

Columns{ ... } is a grouping node with no effect on the grid; use it as the outer wrapper.

Row, Expander, Group, Header, Span, Divider, NewItem

Row{ properties..., Cell{ ... }, CellWith<T>{ ... }, aCellSet }

A Row takes no leading key. Properties — anything that is not a cell — go to the row control, so this is where your Tag goes:

Row{
    Tag{ Kind::Device, deviceIndex },
    Cell{ Tag{ Col::Name }, this, &Page::nameCell },
    m_valueCells
}

Every blueprint row can host controls, so nothing special is needed to put one in a cell.

NodeChildren
Row{ props..., cells... }cells
Expander{ props..., rows... }rows, inside the collapsible body
Group{ props..., rows... }rows, overlapped by the span row
Header{ ... }the header of the container it sits in — see below
Span{ ... }the span row of the group it sits in — see below
Divider{}—
NewItem{ PlaceHolderText{...} }—
Rows{ rows... }spliced into the enclosing grid
Rows{
    Grids::Dt::Expander{
        Header{ Text{ TextStyleId::SubHeading, L"Outputs" } },
        Group{
            Span{ Tag{ Kind::Group, Device::Mixer },
                Cell{ Tag{ Col::Name }, this, &Page::groupNameCell } },
            channelRow(Channel::Input),
            channelRow(Channel::Output)
        }
    }
}
Why the node names are unprefixed. The node names are unprefixed because their namespace already says grid: inside Grids::Dt, Row, Cell and Header cannot be mistaken for anything else, and GridRow would stutter. CellWith<T> is a cell as well, named for what the cell holds rather than Control<T>, which would collide with the framework's own control base.

Tag, Text, ShowInHeader and ColumnWidthMode are not grid types at all. They are shared control properties, understood by anything with a column or a header. Header itself is shared too: it lives in ClaFi::Dt alongside OnEvent and Init, because a container having a header is not a grid idea. Span is declared in Grids::Dt, because a row spanning the rows beside it is.

Header

Header{ ... } is the header of the container it sits directly inside. It is not a row and not a node of any hierarchy: a container looks for one in its own argument list, so it can appear anywhere in that list, and a Header nested a level deeper — inside Rows{}, a Row, a Column — is a compile error rather than a silent no-op.

What it accepts depends on the container, because the two headers are different objects:

InThe header isTakes
Gridthe column-title rownothing — the columns already say what belongs in it, through ShowInHeader
Expanderone text controlText. A cell here is a compile error

Span

Span{ ... } is a group's span row: a row of the group's own, sectioned by the same columns as every other row and laid over the group's rows, so its cells span them. It is a part of the container it sits directly inside, the way a Header is, and only a Group takes one — a Span anywhere else, or a Header in a Group, is a compile error. It takes a Tag, cells and CellWiths, exactly as a Row does.

A span being a row means it goes through the same path as every other row: its declared cells answer content authoritatively and stop propagation. Nothing at grid level has to answer for it. It is a RowContainer, so a CellWith lands in one of its cells the way it lands in any row's.
In a blueprint the span's Tag goes inside Span{ ... } and an expander's text inside its Header{ ... } — the nesting says whose each one is. Through the direct API, grid.addGroup takes the span's tag as SpanTag and grid.addExpander the header's text as HeaderText. The standalone Controls::Expander is a different control and takes HeaderText too.

NewItem

NewItem{ PlaceHolderText{ ... } } is the grid's new-item row. It takes the text its one cell shows and nothing else. Put it in the grid's own list: wherever it is written among the rows, it ends up last, and it stays last as rows are added. What pressing it does is in Adding items from the grid.

Cell, CellWith, CellSet, Init

Cell{ Tag{ ... } }                        // empty but present
Cell{ Tag{ ... }, lambda }                // void(Grids::GetCellTextEvent&)
Cell{ Tag{ ... }, this, &Page::method }   // bound member function
CellWith<T>{ Tag{ ... }, ctorArgs... }    // a live control
CellSet{ cells... }                       // a reusable bundle

The leading Tag on a cell is positional — unlike a column's, it is consumed rather than forwarded, so it has to come first. Everything after it belongs to the control.

CellWith<T> forwards its arguments to the control's constructor. Because the call has to be replayed when the blueprint is applied, those arguments are copied into the node:

Every argument to CellWith<T> must be copy-constructible, and anything large or shared will be duplicated. Wrap it: std::ref(items) passes a reference through instead.

For settings that have no constructor property, use Init<T>. It runs against the control immediately after it is created and can appear anywhere in the argument list:

CellWith<Slider>{ Tag{ Col::Amount },
    ScrollButtons::No,
    Init<Slider>{ [](Slider& slider) {
        slider.setMaxPosition(1.0f);
        slider.setRelativePosition(0.5f, false);
    } }
}

Finding things later

The grid answers for everything the blueprint built. The lookups are members of GridBase, and a sub-grid answers them the same as its root:

MemberPurpose
columnByTag(Tag)The Column& carrying that tag. Throws std::logic_error if there is none, in every build.
findColumnByTag(Tag)Same, but returns nullptr if absent.
forEachRow(func)Calls func(RowContainer&) for every row in the grid and its sub-grids.
ComboBox& mode = row.controlAtColumnAs<ComboBox>(m_grid.columnByTag(Tag{ Col::Mode }));

m_grid.forEachRow([this](Grids::Rt::RowContainer& row) {
    refresh(row);
});

Inside a lambda that runs later, capture the tag and resolve it when the event fires. The columns do not exist yet while the blueprint is being built:

OnEvent{ [this, amountTag](ComboBoxChangeEvent& event) {
    auto& row = *static_cast<Grids::Rt::RowContainer*>(event.comboBox().parent());
    Control* slider = row.controlAtColumn(m_grid.columnByTag(amountTag));
    slider->invalidateState();
} }
columnByTag walks the column tree; there is no index. That is fine once per event, and wrong in a handler the grid calls per row and per column — an OnCellContent handler, for instance. Resolve the column once, keep the reference, and compare by address there.

Recipes

A label that spans sub-columns

Some rows want one wide label where others want two separate values. Declare the parent column's cell and skip the children:

// Device has two sub-columns.
Column{ Tag{ Col::Device }, Text{ L"Device" },
    Column{ Tag{ Col::Name },    ShowInHeader::No },
    Column{ Tag{ Col::Channel }, ShowInHeader::No }
}

// This row fills the parent, so its cell spans both children: one wide label.
Row{ Cell{ Tag{ Col::Device }, this, &Page::deviceCell } }

// This one fills a child instead, so the two sit side by side.
Row{ Cell{ Tag{ Col::Channel }, this, &Page::channelCell } }

Grouping rows beside a span

A Group's span row stretches down its rows. It is a row, so it is described as one:

// The rows fill Channel; the span row fills Name. Together they cover both.
Group{
    Span{
        Tag{ Kind::Group, Device::Mixer },
        Cell{ Tag{ Col::Name }, this, &Page::deviceCell } },
    channelRow(Channel::Input),
    channelRow(Channel::Output)
}

That is the whole recipe. The span's cells connect to the span row exactly as any row's do, so it answers for itself and nothing at grid level is involved.

A control that belongs to the group rather than to a row

The span's cells are stretched over the rows beside them, so a control in one of them reads as the group's own. That is where a choice made once per group goes:

Group{
    Span{
        Tag{ Kind::Group, Device::Mixer },
        Cell{ Tag{ Col::Name }, this, &Page::deviceCell },
        CellWith<CheckBox>{ Tag{ Col::Mute } } },
    channelRow(Channel::Input),
    channelRow(Channel::Output)
}

The control is placed at the top of the merged cell, at the height of one row, and the column it names counts as the span's under the disjointness rule — no row of the group may declare it.

forEachRow walks the rows of a grid and its sub-grids. A group's span is not one of them, so a control there is not reached that way. Reach it through the group, or keep a reference to it from Init<T>.
Set the span's Tag even where the grid never needs it. A provider shared by the span and the rows receives either one as event.row(), and the tag is how it tells the span from an element row.

Live controls in cells

A CellWith<T> puts a live control in a cell. The row keeps the keyboard focus for it: the grid keeps the arrows, Home, End, the page keys and Tab, and every other key goes to the control in the selected cell. A key held with Alt goes to the control too, and on to the application's shortcuts when the control has no use for it: Alt+Down drops a combo box's list, and Alt+Left in a browser goes back.

The first click picks the cell. A control that opens something over itself - a combo box's list, an editor typed in place - opens it only from a click on the cell that is already selected. The first click selects the cell and its row, and the next one opens. The dropdown arrow opens on the first click too, because opening the list is its one job. Picking a row to delete it opens nothing.

GestureOn a cell not selectedOn the selected cell
Click on the controlSelects the cell and its rowOpens the list or the editor
Click on the dropdown arrowSelects and opensOpens
Double clickSelects and opensOpens
Ctrl+click, Shift+clickIn a SelectionMode::Multi grid, changes the selected rows and opens nothing, the arrow included
Return, Space, F4, Alt+Down-Opens

A check box or a button in a cell acts on the first click. It opens nothing over the cell, so the grid has nothing to hold back.

A click on a control in a cell reaches the grid as a click on the row holding it, which is how the grid moves its selection. A click handler on the grid reads the row in event.control, not the control.

A cell control of your own that drops a popup on a click asks first, and the question it asks says what dropping the popup is to that click. When the answer is no, it leaves the click alone - the click goes on to the row, which picks the cell.

QuestionDropping the popup isOn the click that picks the cell
mayDropPopupImplicitlyA meaning a click on the control is given - the list a combo box drops from its main area, an editor over a captionRefused
mayDropPopupThe one job of the part that was clicked - a dropdown arrowAllowed

Ctrl+click and Shift+click in a SelectionMode::Multi grid refuse both. A swatch that opens a colour picker when clicked is given that meaning, so it asks the first question:

void SwatchControl::nestedClick(ClickEvent& event)
{
    if (!mayDropPopupImplicitly(event))
        return;
    event.stopPropagation();
    openSwatchPopup();
}
A control built on DropdownControlBase or WithInPlaceEdit asks already and needs nothing more. Both questions walk nestedControlDroppingPopup from the control up to the root, and the row holding the cell answers them for the control, reading DropPopupEvent::implicit to tell them apart.

Rows that differ from their neighbours

Build rows with a function and let it choose:

Row Page::channelRow(Channel channel) const
{
    const bool readOnly = channel == Channel::Monitor;
    return Row{
        Tag{ Kind::Channel, channel },
        Cell{ Tag{ Col::Channel }, this, &Page::channelCell },
        readOnly ? m_readOnlyCells : m_valueCells
    };
}

A row that should leave a slot visibly blank rather than let a neighbour cover it declares the cell with no content:

const CellSet m_readOnlyCells{
    Cell{ Tag{ Col::Mode } },      // present, empty, still bordered
    Cell{ Tag{ Col::Amount } }
};

Reaching a control later

void Page::refreshAll()
{
    Grids::Rt::Column& modeColumn = m_grid.columnByTag(Tag{ Col::Mode });
    Grids::Rt::Column& amountColumn = m_grid.columnByTag(Tag{ Col::Amount });
    m_grid.forEachRow([&](Grids::Rt::RowContainer& row) {
        if (row.tag<0, 2, Kind>() != Kind::Channel)
            return;
        ComboBox& mode = row.controlAtColumnAs<ComboBox>(modeColumn);
        Slider& amount = row.controlAtColumnAs<Slider>(amountColumn);
        mode.invalidateState();
        amount.invalidateState();
    });
}
forEachRow visits every RowContainer standing as a row of the grid, including ones inside expanders and groups. Expanders and groups are not containers themselves, so they are stepped over and descended into. A group's span is a container but not a row of the grid, so it is not visited. If some rows do not have the control you are asking for, check the row's Tag first — controlAtColumnAs does not check.

Adding items from the grid

A NewItem row lets the grid ask for an item itself: a muted line under the last row, led by a plus, that reads the text you give it. The grid does the asking and leaves the adding to you:

Grid& m_grid{ add<Grid>(
    Columns{ /* ... */ },
    Header{},
    NewItem{ PlaceHolderText{ L"Add rule" } }
) };

The handler goes in the constructor, once the grid is built:

m_grid.onNewItem([this](Grids::NewItemEvent&) {
    m_rules.push_back({});
    addRuleRow(m_rules.size() - 1);
});

The row's one cell spans every column and behaves like any other cell: the pointer lights it and the arrow keys reach it. A click on it, or Return or Space while it is selected, raises NewItemEvent on the grid. When your handler returns, the grid selects the first cell of the row the handler added, gives it the focus, and scrolls it into view once the row has been laid out. A handler that adds nothing leaves the selection where it was.

A row added after the new-item row is put ahead of it, so it stays last however the rows arrive - appended one at a time or rebuilt wholesale.

Add the row inside the handler. The grid selects a row added while the event runs; one added later - on the next tick, say - arrives unselected.
The new-item row never joins the selection. It is a command rather than an item, so a Delete handler that walks selection() never meets it. It opens no in-place editor either, so F2 on it does nothing.

To ask for an item from somewhere else - a toolbar button, a shortcut - call grid.requestNewItem(). It raises the same event and selects the same way.

Troubleshooting

Build errors

SymptomCauseFix
Unqualified grid names fail to resolve in an implementation unit A module implementation unit does not inherit everything its interface unit had in scope. Give the .cpp its own imports and using-directives: import ClaFi.Controls.Grids_Dt;, import ClaFi.Controls.Grids;, using namespace ::ClaFi;, using namespace ::ClaFi::Controls;, using namespace ::ClaFi::Controls::Grids::Dt;. Spell a node Grids::Dt:: in full - Dt:: names ClaFi::Dt here.
"deleted function" or "no copy constructor", pointing into std::function or std::tuple An argument to CellWith<T> or Column is move-only. The call is stored so it can be replayed, which requires copyable arguments. Wrap it in std::ref. If it truly must be moved, create that control through the direct API instead.
No matching constructor for Cell, with a note that a constraint was not satisfied The provider cannot be called with a Grids::GetCellTextEvent&. A handler written for another event is the usual cause. Take Grids::GetCellTextEvent&, the parameter a grid-wide OnGetCellText handler takes.
text() called on a const event, inside a provider The provider takes const Grids::GetCellTextEvent&. That passes the constraint, and text() is not const. Drop the const.
A call through a member pointer drops const, inside Cell's constructor instantiated for const Page The function building the row is const but the cell provider is not. Make the provider const too.
Errors listing a control's constructor, from inside CellWith<T> The argument pack does not match that control's constructor. Write one direct row.addControl<T>(column, args...) to confirm the arguments, then move them into CellWith<T> unchanged.

Nothing appears, or it looks wrong

SymptomCause
Debug build throws from Grid's constructor: "Grid design: cell Tags matched no column" Exactly what it says, and the message lists the numeric tag values. Some cell names a column that was never declared with that Tag. A build without _DEBUG skips the check and drops those cells.
A row you declared never appears at all None of its cells resolved, so the row has no content, therefore no height. Same cause as above — run a debug build.
A cell is missing but the row is fine That one tag did not match a column. A debug build throws from the constructor and lists the numeric tag values.
A group's span row has no label Its Span{ ... } declares no cell in that column. See the recipe.
Every cell is filled in a row you expected to be sparse That row declared no cells, so no handler was connected to it and the question fell through to the grid's event, where hasContent defaults to true. The span of a Group whose Span declares no cells does this too.
A parent cell is short, with a gap beneath it Something in its sub-columns has content in that row, so it is not merging. Declare the empty sibling as a bare Cell{ Tag{ ... } } if you want the space filled.
A narrow unpainted strip at the right edge of a column group None of that group's sub-columns is ColumnWidthMode::Fill, so they do not reach their parent's edge.
Controls appear in the wrong cells A shared CellSet was built with the wrong tags, or a lambda captured a column reference instead of a tag.
A handler that picks out one column is slow It resolves a tag on every call. Resolve the column once with columnByTag after the grid is built, keep the Rt::Column&, and compare by address: &event.column() == &m_someColumn.
A crash in Grid's constructor, or a row without the cells a shared set holds A CellSet the grid's argument list names is declared after the grid. Members are built in declaration order, so the grid read the set before it was built.

Do / Don't

Do

  • Declare cell sets, and anything else the grid's argument list names, before the grid.
  • Give every column a cell names a unique Tag.
  • Capture tags in lambdas and resolve them through the grid when the event fires.
  • Resolve a column once and compare by address in anything the grid calls per cell.
  • Use std::ref for anything shared or expensive.
  • Use a bare Cell{ Tag{ ... } } when you want a present-but-empty cell.
  • State a look every cell of a column shares as the column's TextFormat, not in each provider.
  • Add an item's row inside the NewItemEvent handler, so the grid can select it.
  • Ask mayDropPopupImplicitly before a cell control of your own drops a popup on a click, or mayDropPopup from a part whose one job is that popup.
  • Give at least one sub-column in each group Fill.
  • Run a debug build after changing a blueprint.

Don't

  • Don't draw column tags from the same enum as your row tags — one type serves both, so nothing catches the mix-up at compile time.
  • Don't capture a Column& in a cell set — the columns do not exist yet.
  • Don't call columnByTag inside OnCellContent.
  • Don't leave a Group's Span without cells. A span that declares none answers through the grid's handlers, where every cell exists.
  • Don't read a missing cell as an empty one; it means no cell at all.
  • Don't rely on release builds to tell you a tag was wrong.

Appendix · Direct API

The blueprint is a convenience, not a wall. Everything below works on a grid built from a blueprint as well, which is useful when one row needs something the blueprint does not express.

CallReturns
grid.columns()the root ColumnCollection&
columns.add(props...)Column&
column.createSubColumns()ColumnCollection&, creating on first use
column.subColumns()ColumnCollection*, null if none
column.cellTextFormat()const TextFormat& - what the column's data cells are laid out over, its alignment included
column.headerTextFormat()const TextFormat& - the alignment alone, which the header cell is laid out over
grid.addHeader()Grids::GridHeader&
grid.addRow(props...)Rt::Row&
grid.addControlRow(props...)Rt::RowContainer&
grid.addDivider(props...)Grids::RowDivider&
grid.addExpander(props...)Rt::RowExpander&, body via .body()
grid.addGroup(props...)Rt::RowGroup&, span via .span(), body via .body()
grid.addNewItemRow(PlaceHolderText)Grids::RowNewItem&, kept after the last row
grid.requestNewItem()nothing - raises NewItemEvent the way pressing the new-item row does
row.addControl<T>(column, args...)T&
row.controlAtColumn(column)Control*, null if none
row.controlAtColumnAs<T>(column)T&, unchecked

Without a blueprint, a grid answers two questions through events — which cells exist, and what they say:

m_grid.onCellContent([](Grids::CellContentEvent& event) {
    event.hasContent = event.column().tag() != Tag{ Col::Mute };   // defaults to true
});

m_grid.onGetCellText([this](Grids::GetCellTextEvent& event) {
    writeCell(event);
});
Note the default: hasContent arrives as true, so without a blueprint every cell exists unless you say otherwise. That is the opposite of a blueprint row, where a cell exists only if you declared it.

The same two events exist on every row, which is how a blueprint answers for the rows it builds. A row takes a handler for either in its argument list, as an OnEvent like any other, and RowBase::onCellContent / onGetCellText connect one later:

m_grid.addControlRow(Tag{ Kind::Channel, Channel::Input },
    OnEvent{ [](Grids::CellContentEvent& event) {
        event.hasContent = event.column().tag() == Tag{ Col::Channel };
        event.stopPropagation();   // keep the grid handler out of it
    } });
A row's handler runs before the grid's. If it calls stopPropagation() the grid handler never sees that cell — which is how a blueprint row stays authoritative about its own content while rows it did not declare fall through to whatever the grid says.
All manuals Top