Skip to content

Declarative UI with Vide

Vide is a fine-grained reactive UI library. Sources are callable: count() reads, count(n) writes. In CL++ that is a normal call.

#include <clpp/roblox.clh>
void init() {
Players players = GetService<Players>();
Player localPlayer = players.LocalPlayer;
guard (localPlayer != null) else { return; }
PlayerGui playerGui = localPlayer.WaitForChild("PlayerGui");
auto create = Vide.create;
auto source = Vide.source;
auto apply = Vide.apply;
auto count = source(0);
auto makeGui = create("ScreenGui");
auto makeButton = create("TextButton");
auto makeLabel = create("TextLabel");
auto label = makeLabel({
{"BackgroundTransparency", 1},
{"Size", UDim2.fromScale(1, 0.5)},
{"TextColor3", Color3.fromRGB(240, 240, 255)},
{"Text", func () {
return "Clicks: " .: count();
}}
});
auto button = makeButton({
{"Size", UDim2.fromScale(1, 0.5)},
{"Position", UDim2.fromScale(0, 0.5)},
{"Text", "Once-safe click"},
{"Activated", func () {
count(count() + 1);
}}
});
auto gui = makeGui({
{"Name", "VideCounter"},
{"ResetOnSpawn", false},
{"Parent", playerGui}
});
auto mount = apply(gui);
mount({ label, button });
}

Vide Text can be a function. CL++ lambdas func () { return ...; } become that function. Vide re-runs it when count() changes.

If a Roblox event should update UI once (tutorial toast):

workspace.ChildAdded~>Once(func (Instance child) {
match (child) {
BasePart p => count(count() + 1),
_ => warn("ignored")
};
});

~> still uses Janitor. Vide sources stay independent of that connection.

Full file: examples/ui/VideCounter.client.clpp.

Next: Advanced typing.