Skip to content

Advanced

Virtualized list-view

For large datasets, list-view renders only visible items (plus a buffer) and recycles widgets on scroll:

delegate "RoomCard" {
    hbox {
        fill-width #true; padding 8; gap 8; align-items "center"
        label { text bind="item.name" }
        label { text bind="'' + item.brightness + '%'" }
    }
}

list-view "rooms" as="item" delegate="RoomCard" {
    item-height 48
    buffer 3
    fill-width #true; height 300
}
PropertyDescription
first argumentState array key
as="var"Loop variable name (default "item")
delegate="Name"Item template (auto-receives item and index)
item-heightFixed height per item (required for virtualization)
bufferExtra items rendered above/below the viewport (default 2)

Updates are minimal: structural array changes add/remove/rebind slots, per-field changes update only the affected slot, and full replacement rebinds all slots. Items can also be declared inline instead of via a delegate.

Actions inside a delegate can use the loop index:

button {
    label "X"
    on-click { array-remove "items" expr="index" }
}

Timers

// Repeating. Ticks every second
timer {
    interval 1000
    set "seconds" expr="app.seconds + 1"
}

// One-shot. Fires once after 3 seconds
timer {
    interval 3000
    repeat #false
    set "message" value="Welcome!"
}

// Reactive control. Runs only while app.running is true
timer {
    interval 1000
    active bind="app.running"
    set "seconds" expr="app.seconds + 1"
}

Timers accept any action, work inside components (with self. state), and active bind= turns a stopwatch into three lines of markup.

Commands

Commands are firmware-registered callbacks invocable from the UI. They are the counterpart to state: state flows data down, commands send intents up.

interpreter.commands().register_command("shuffle", [&](const Value&) {
    // firmware-side logic
});
button {
    label "Shuffle"
    on-click { cmd "shuffle" }
}

Assets

The assets {} block declares the images and fonts a document requires:

assets {
    image "bg" source="assets/bg.png"
    font "title" source="fonts/Title.ttf" size=32
}

image { src "bg" }
label { text-font "title" }

Two validation levels apply: at compile time, literal src / text-font references are checked against the manifest (with warnings for unknown names); at load time, each declared asset is checked against what the firmware actually registered. Missing images render blank and missing fonts fall back to default. The app never crashes over an asset.

Firmware registers assets by name:

kadokit::register_font_asset("my_font", &my_custom_font);
kadokit::register_image_asset("my_icon", my_icon_data);

On desktop builds with filesystem support, image.src can also load PNG, JPG, BMP, and SVG files directly by path.

Firmware integration

The host API is deliberately small. On desktop the interpreter loads .ui.kdl sources directly; on device it loads the compiled .bui binaries (see Getting started → From desktop to device):

using namespace kadokit;

Interpreter interpreter;
interpreter.expose("app");
interpreter.load_file("app.ui.kdl");

// Read/write state
auto& state = interpreter.state();
state.set_from_firmware("temperature", Value::from_int(25));
int brightness = state.get("brightness").as_int();

// Subscribe to changes
state.subscribe("brightness", [](std::string_view key, const Value& val) {
    hardware_set_backlight(val.as_int());
});

// Register commands
interpreter.commands().register_command("reboot", [](const Value&) {
    hardware_reboot();
});

Read-only state

State declared read-only=#true can only be updated through set_from_firmware(). UI-side writes are rejected as no-ops. Sensor readings stay authoritative.

Batching

Group updates so the UI refreshes once:

{
    StateStore::BatchGuard guard(state);
    state.set_from_firmware("temperature", Value::from_int(25));
    state.set_from_firmware("humidity", Value::from_int(70));
}  // all UI updates fire here, once per changed key

The interpreter also coalesces automatically by default, flushing state changes once per refresh frame.

Hot reload

interpreter.reset();       // clears state, styles, components, screens, JS context
interpreter.expose("app");
interpreter.load_file("app.ui.kdl");

Diagnostics

Documents are validated at load time with source-location context:

error: slider > bind-value: unknown state variable 'brighness'; did you mean 'brightness'?
warning: slider > bind-value: binding 'app.name' (string) to slider.value (expected int)
error: Card: missing required prop 'body'

Unknown widget types and state variables get suggestions for likely names via edit distance. Type mismatches between bindings and widget expectations are warned about before anything renders.