Components: GUIs that update themselves
Lesson 9.9
With the widget API you create every widget yourself and change it with setters. A view works the other way round: your render function describes the whole GUI for the current state, and the game compares it with what it showed before and changes only what differs. If you know React, this is the same idea, in C.
Rendering a tree
A view calls your render callback for each component. You emit nodes with cl_view_begin / cl_view_end (containers), cl_view_node (widgets) and their props:
#include <clight.h>
#include <stdio.h>
#include <string.h>
static int str_is(cl_str s, const char* lit) { return s.len == strlen(lit) && memcmp(s.ptr, lit, s.len) == 0; }
static void render(cl_view v, uint64_t comp, cl_str type, void* user) {
double n = cl_view_state_num(v, comp, CL_STR("count"), 0); /* state, created as 0 */
char label[48];
int len = snprintf(label, sizeof label, "Clicked %d times", (int)n);
cl_view_begin(v, CL_VIEW_PANEL, CL_STR(""));
cl_view_prop_num(v, CL_STR("padding"), 8);
cl_view_node(v, CL_VIEW_HEADING, CL_STR(""));
cl_view_prop_str(v, CL_STR("text"), CL_STR("Counter"));
cl_view_node(v, CL_VIEW_BUTTON, CL_STR(""));
cl_view_prop_str(v, CL_STR("label"), (cl_str){ label, (size_t)len });
cl_view_prop_str(v, CL_STR("on_press"), CL_STR("inc")); /* a handler is a name */
cl_view_end(v);
}
static void on_event(cl_view v, uint64_t comp, cl_str handler, double value, cl_str text, void* user) {
if (str_is(handler, "inc")) {
double n = cl_view_state_num(v, comp, CL_STR("count"), 0);
cl_view_set_state_num(v, comp, CL_STR("count"), n + 1); /* renders the component again */
}
}
void open_counter(void) {
cl_view_desc d = CL_VIEW_DESC_INIT;
d.title = CL_STR("Counter");
d.x = 40; d.y = 40; d.width = 200;
d.render = render;
d.on_event = on_event;
cl_view v;
cl_view_open(&d, &v);
}
After a click, render runs again and emits the same tree with a new label. The game finds the button (same kind, same place) and changes only its label. If nothing changes, render is not called at all.
Components inside components
cl_view_component(v, type, key) adds a child component and returns its instance number. Its props follow with cl_view_prop_*; render is then called with that type and the child’s comp, and reads the props with cl_view_get_num / cl_view_get_str:
static void render(cl_view v, uint64_t comp, cl_str type, void* user) {
if (str_is(type, "root")) {
cl_view_begin(v, CL_VIEW_COLUMN, CL_STR(""));
static const char* names[] = { "Home", "Mine" };
for (int i = 0; i < 2; ++i) {
cl_view_component(v, CL_STR("row"), (cl_str){ names[i], strlen(names[i]) }); /* keyed by name */
cl_view_prop_str(v, CL_STR("name"), (cl_str){ names[i], strlen(names[i]) });
}
cl_view_end(v);
} else if (str_is(type, "row")) {
char name[64];
size_t len = 0;
cl_view_get_str(v, comp, CL_STR("name"), name, sizeof name, &len);
cl_view_node(v, CL_VIEW_LABEL, CL_STR(""));
cl_view_prop_str(v, CL_STR("text"), (cl_str){ name, len });
}
}
A child whose props are the same as last time is not rendered again. The key (the third argument) keeps list entries apart when the list changes.
Effects
cl_view_effect(v, name, deps) inside render asks for on_effect(v, comp, name, false) after the update, when the effect is new or deps changed (a number: a counter, a hash). Before it runs again, and when the component goes, you get on_effect(..., true) to clean up. on_unmount(v, comp) tells you a component left the tree.
cl_view_effect(v, CL_STR("save"), (uint64_t)n); /* runs on_effect after every change of n */
Exercise
Add a toggle “Sound” below the button. Keep its value in a state named "sound" and log on or off when it changes. (Toggles report 1 or 0 as value.)
Show a solution
/* in render, inside the panel */
cl_view_node(v, CL_VIEW_TOGGLE, CL_STR(""));
cl_view_prop_str(v, CL_STR("label"), CL_STR("Sound"));
cl_view_prop_num(v, CL_STR("value"), cl_view_state_num(v, comp, CL_STR("sound"), 1));
cl_view_prop_str(v, CL_STR("on_change"), CL_STR("sound"));
/* in on_event */
if (str_is(handler, "sound")) {
cl_view_set_state_num(v, comp, CL_STR("sound"), value);
cl_log(CL_LOG_INFO, value != 0 ? CL_STR("on") : CL_STR("off"));
}
Recap: cl_view_open shows a view, render emits the tree, handlers are string props that come back in on_event, cl_view_state_* hold state and cl_view_set_state_* re-render, cl_view_component nests components. The reference is Components.