ecs-ui is a small C library for authoring retained UI structure in a Flecs
world and exporting it as a renderer-neutral tree. Clay and raylib adapters are
provided as optional layers.
The library is intended for projects that already use:
- Flecs for retained state and relationships.
- An immediate layout/rendering layer such as Clay.
- Snapshot/read boundaries between app state and presentation code.
ecs_world_t *world = ecs_init();
EcsUiImport(world);
ecs_entity_t root = EcsUiRootEntity(world, "Home");
EcsUiBuilder builder = EcsUiBuilderBegin(world, root);
ecs_entity_t present_add_machine =
ecs_entity(world, {.name = "PresentAddMachineAction"});
VStack(&builder, {.id = "HomeStack", .gap = 10.0f}) {
Button(
&builder,
{
.id = "AddMachine",
.variant = ECS_UI_BUTTON_PRIMARY,
.on_click = present_add_machine,
}) {
Text(&builder, {.id = "AddLabel", .text = "add machine"});
}
}
EcsUiBuilderEnd(&builder);
EcsUiTreeSnapshot tree = {0};
EcsUiReadTree(world, root, &tree);Containers use EcsChildOf for hierarchy and EcsOrderedChildren for stable
sibling order. The exported tree is a flat preorder snapshot with
first_child/next_sibling indices, so render adapters do not need direct Flecs
access.
Click behavior is modeled as an ECS relationship:
ecs_add_pair(world, button, EcsUiOnClick, present_add_machine);Renderer/input adapters return generic events with the clicked node and action entity. Application code decides what the action entity means.
Shared widget styling uses stable token entities:
ecs_entity_t text_field_style = EcsUiStyleToken(world, "TextField");
ecs_entity_t theme = EcsUiThemeEntity(world, "LightTheme");
EcsUiThemeSetBoxStyle(world, theme, text_field_style, (EcsUiBoxStyle){
.background = {236, 245, 243, 255},
.hover_background = {224, 238, 235, 255},
.padding = 12.0f,
});
EcsUiThemeSetTextStyle(world, theme, text_field_style, (EcsUiTextStyle){
.color = {20, 31, 34, 255},
.muted_color = {80, 99, 102, 255},
.disabled_color = {105, 119, 121, 255},
});
Pressable(&builder, {
.id = "SearchField",
.style_token = text_field_style,
}) {
Text(&builder, {.id = "SearchText", .text = "search"});
}EcsUiStyleToken creates or reuses named children under EcsUiStyleTokens, so
apps can standardize semantic names such as TextField, PrimaryAction,
SubtleAction, and DangerAction. Nodes can opt in through
EcsUiSetStyleToken or the style_token field on button and pressable descs.
When both are present, a direct EcsUiBoxStyle component on the node still wins
over the token style. Foreground styling follows the same contract with
EcsUiTextStyle: a style token may define text/icon colors, muted caption
colors, and disabled text colors, and renderer adapters inherit those values
through child Text and Icon nodes.
The current action tokens are semantic handles in snapshots. Existing
EcsUiButton renderers still draw from EcsUiButtonVariant palettes and are
best treated as a legacy/convenience widget; app design systems can layer
token-driven action widgets over Pressable, as text-field views already do.
ecs_ui_text_input provides reusable field state, focus relationships, cursor
and selection state, clipboard requests, and renderer-neutral event routing.
Apps link a UI node to a field with EcsUiTextInputSetUiField, then feed
adapter events through EcsUiTextInputApplyEvent before handling app-specific
actions. For the common case, EcsUiTextInputBuildFieldView creates a basic
pressable field view and EcsUiTextInputProjectFieldView keeps its text,
placeholder, caret, and focus highlight synchronized with field state.
The text-input router consumes field focus clicks and keyboard editing events. Outside clicks enqueue blur but are not consumed, so the same click can still trigger an application action. Submit is deliberately not consumed because each app decides what Enter means for the focused field. Apps that need a custom field widget can still keep their own projection and only use the lower-level state/request APIs.
Pass a Flecs single-file distribution directory:
cmake -S . -B build \
-DECS_UI_FLECS_SOURCE=/path/to/flecs \
-DECS_UI_CLAY_SOURCE=/path/to/clay
cmake --build build
ctest --test-dir build --output-on-failureecs_ui requires Flecs. ecs_ui_clay is optional and is skipped unless Clay is
available through ECS_UI_CLAY_SOURCE or an existing CMake target named clay.
The raylib renderer is optional. When enabled, CMake fetches raylib 6.0 with
FetchContent, matching Blocksmith's dependency shape:
cmake -S . -B build-raylib \
-DECS_UI_FLECS_SOURCE=/path/to/flecs \
-DECS_UI_BUILD_RAYLIB_DEMO=ON
cmake --build build-raylib --target ecs_ui_raylib_demo
./build-raylib/ecs_ui_raylib_demoIf you already have a raylib checkout, pass
-DECS_UI_RAYLIB_SOURCE=/path/to/raylib instead of fetching it.
The demo also models app state in Flecs. Clicking add item emits the
AddItemAction target from (EcsUiOnClick, AddItemAction), the demo submits a
DemoAddItemRequest, and a Flecs system creates a DemoItem entity. UI
observers materialize retained item rows and status text under ItemList from
DemoItem changes. The static UI shell is authored once; adding an item does
not rebuild the whole UI tree.
The add-item sheet uses a small form projection pattern. Reusable field value,
focus, cursor, and selection state stay in ecs_ui_text_input. The demo-owned
DemoAddItemForm helper trims and validates the item name, projects the
retained CreateItem button disabled state, and leaves submit behavior in the
event bridge. Enter and click both re-check form validity before creating the
item, clearing fields, blurring, and dismissing the sheet.
See examples/raylib_demo/PRESSURE_TEST_PLAN.md for the phased plan to prove
out Glowfish-style actions, selection relationships, navigation, animations,
gestures, text input, theming, and custom widgets.
This is still experimental. It covers core UI hierarchy authoring, ordered children, text/button/icon/stack/pressable nodes, pair-based click action targets, snapshot export, projection helpers, navigation, animation, style tokens, active themes, text-input state/routing, a Clay adapter, and a raylib renderer/demo. Install/package rules and renderer parity hardening are still follow-up work.