Components: GUIs that update themselves
Lesson 9.9
clight_ui.hpp (next to clight.hpp in the SDK) lets you write GUIs as components: functions that return what the GUI looks like for the current state. The game compares every new result with the last one and changes only the widgets that differ, in the player’s Appearance style. If you know React, this is the same idea.
A first component
#include <clight_ui.hpp>
namespace ui = clight::ui;
ui::Element counter(const ui::Props& p) {
auto [n, set_n] = ui::use_state(0);
return ui::panel().padding(8)
.child(ui::heading(p.str("title")))
.child(ui::button("Clicked " + std::to_string(n) + " times").on_press([=] { set_n(n + 1); }));
}
void open_counter() {
ui::open("Counter", counter, ui::Props().set("title", "My first component"), "counter", {.x = 40, .y = 40, .width = 200});
}
A click calls set_n, the component renders again on the next frame, and the game changes the button’s label. Nothing else runs while nobody clicks.
Components inside components
ui::Element row(const ui::Props& p) {
return ui::row().gap(4).height(20)
.child(ui::label(p.str("name")).grow())
.child(ui::icon_button("trash", "Remove"));
}
ui::Element places(const ui::Props&) {
ui::Element list = ui::column().gap(2);
for (const char* name : {"Home", "Mine"})
list.child(ui::component("row", row, ui::Props().set("name", name)).key(name));
return list;
}
A child whose props are equal to last time (and that has no handlers or children in its props) is not rendered again. key keeps list entries apart when the list changes.
Effects
void save_volume(double level); // yours: writes the config
ui::Element volume(const ui::Props&) {
auto [level, set_level] = ui::use_state(50.0);
ui::use_effect([=] {
save_volume(level); // after every change of level
return std::function<void()>(); // nothing to clean up
}, level);
return ui::slider("Volume", 0, 100, level, 1).on_change([=](const ui::Event& e) { set_level(e.value); });
}
use_effect(fn, deps...) runs fn after the update when one of deps changed (no deps: after every render); what it returns is the cleanup. use_effect_once(fn) runs once. use_ref(v) keeps a value that doesn’t render when changed.
Handlers
.on_press(fn()) for buttons, cards and icon buttons; .on_change(fn(const ui::Event&)) for toggles (e.on()), sliders and colours (e.value), dropdowns and tabs (e.index(), e.text) and text fields (e.text); .on_select for lists.
Exercise
Add a toggle “Sound” to counter whose state lives in counter and that shows “Sound is off” under it while it is off.
Show a solution
ui::Element counter(const ui::Props& p) {
auto [n, set_n] = ui::use_state(0);
auto [sound, set_sound] = ui::use_state(true);
ui::Element panel = ui::panel().padding(8)
.child(ui::heading(p.str("title")))
.child(ui::button("Clicked " + std::to_string(n) + " times").on_press([=] { set_n(n + 1); }))
.child(ui::toggle("Sound", sound).on_change([=](const ui::Event& e) { set_sound(e.on()); }));
if (!sound) panel.child(ui::label("Sound is off"));
return panel;
}
Recap: components are functions ui::Element f(const ui::Props&), ui::open shows one, ui::use_state holds state, ui::use_effect runs work after updates. The reference is Components.