- Before you start
- Layout and events lessons completed
- What you will build
- A 10,000-record workspace with scrolling and navigation
Run it and locate the files
Extract and build the workspace. workbench.html provides structure and styles; main.cpp supplies synthetic data, VirtualList and events. Sidebar items illustrate layout and files are not read from disk.
$cxxStandard = Read-Host SDK-standard
cmake -S . -B build -A x64 "-DCMAKE_CXX_STANDARD=$cxxStandard" -DKui_DIR=C:/SDK/kui-cpp/cmake
cmake --build build --config Release --parallel 4
.\build\Release\forge_workbench.exeDataset size versus rendered rows
totalItems is the record count, itemHeight the fixed row height, and overscan the extra rows beyond the viewport. renderItem creates one indexed row, avoiding a DOM node for every record.
kui::VirtualListConfig rows;
rows.totalItems = 10000;
rows.itemHeight = 54;
rows.overscan = 3;
rows.renderItem = [&](kui::Document* document, kui::u32 index) {
const char* names[] = {"workspace-shell", "component-tokens", "asset-inspector", "project-overview", "navigation-rail", "activity-stream"};
const std::string name = std::string(names[index % 6]) + (index < 6 ? "" : "-" + std::to_string(index + 1)) + ".html";
auto row = document->createElement("div");
row->setAttribute("class", index == selectedIndex ? "row selected" : "row");
row->setAttribute("data-index", std::to_string(index));
row->setInnerHTML("<span class='file-icon'>HTML</span><span class='file-name'>" + name + "<span class='file-sub'>Desktop studio / Interface</span></span><span class='file-kind'>HTML</span><span class='status'>Ready</span><span class='file-size'>" + std::to_string(12 + index % 80) + " KB</span>");
row->addEventListener("click", [&, name, index](kui::Event&) { select(index, name); });
return row;
};
list.setup(container, rows);
Keep selection in the model
The host retains selectedIndex and renderItem restores the selected class from it. Rows leave the visible area, so a DOM class without stored selection is insufficient.
kui::u32 selectedIndex = 0;
auto select = [&](kui::u32 index, const std::string& name) {
selectedIndex = index;
auto visible = doc->querySelectorAll(".row");
for (const auto& node : *visible) {
auto* element = dynamic_cast<kui::Element*>(node.get());
if (!element) continue;
element->removeClass("selected");
if (element->getAttribute("data-index") == std::to_string(index)) element->addClass("selected");
}
doc->getElementById("selected")->setTextContent(name);
doc->getElementById("event-status")->setTextContent("Selected: " + name);
};
Observe virtualization
Inspect behavior before changing the dataset.
- Scroll down and back up, checking row indices and details.
- Jump to record 5,000 and check the visible range.
- Compare rendered rows with total records; this is not a performance benchmark.
- Select a row, scroll it away and return; selection and details should agree.
Exercise: change data and row height
Change totalItems, then separately change itemHeight and the row CSS. Rebuild and check navigation, scroll range and selection. Reset out-of-range selection after shrinking data.
- Changing the count must not create gaps or invalid indices.
- The final item remains reachable at the minimum window size.
- Taller rows should reduce visible count rather than overlap text.
Something unclear?
Report an issue on this page →