FORGE KUI / learn

Build a virtual-list workspace

Connect the data model, visible rows, selection, navigation and details.

Version 3.61.7

Lesson 08
Before you start
Layout and events lessons completed
What you will build
A 10,000-record workspace with scrolling and navigation
Download project ↓workbench.zip

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.

powershell
$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.exe

Dataset 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.

cpp
    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.

cpp
    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.

  1. Scroll down and back up, checking row indices and details.
  2. Jump to record 5,000 and check the visible range.
  3. Compare rendered rows with total records; this is not a performance benchmark.
  4. 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.