Skip to content

Getting Started

This guide walks you through the minimum viable setup: initialize a backend, create a root layout, add a widget, and run the frame loop.

By the end you will have a window with a styled, clickable button.


1. Prerequisites

  • C++20 or newer.
  • A backend. ZenitUI ships a reference implementation for Raylib, but the core only depends on three abstract interfaces declared in UIContext.hpp:
Interface Responsibility
IRenderer Primitive drawing, text measurement, clip / transform / target
IPlatform Window, pointer, keyboard, timers, DPI, safe area
IAssetProvider Fonts, textures, shaders, keyed by string name

If you want to use a different graphics library, implement those three and you're done. Nothing else in the framework touches the GPU.


2. Initialize the backend

ZenitUI is a library, not a framework that owns main(). You initialize the backend, hand it to UIContext, and drive the frame loop yourself.

#include <raylib.h>
#include "RaylibBackend.hpp"
#include "UI.hpp"

using namespace ZenitUI;

int main() {
    InitWindow(800, 600, "ZenitUI");
    SetWindowState(FLAG_WINDOW_RESIZABLE);

    // 1. Backend instances
    RaylibRenderer      renderer;
    RaylibPlatform      platform;
    RaylibAssetProvider assets(renderer);

    // 2. Register them on the global context
    UIContext::get().renderer = &renderer;
    UIContext::get().platform = &platform;
    UIContext::get().assets   = &assets;

    // 3. Load any asset you'll reference by name from .zstyle / ZMarkup
    assets.loadFont("calibri", "assets/calibri.ttf", 64);
    assets.loadTexture("bubble", "assets/bubble.png");
    assets.loadEffect("blur",    "assets/blur.fs");

    // ...
}

Rule of thumb: anything you want to reference by name (in a .zstyle file or in a ZMarkup attribute) must be registered with the IAssetProvider before the first frame. Fonts, textures, and shaders are all referenced by string.

UIContext is a singleton. It owns:

  • the three backend pointers,
  • the current input snapshot (pointer, inputEvents, modifiers),
  • the focus / pointer-capture / portal registries,
  • the frame clock (dt, time).

You generally don't touch it directly after setup, except for advanced cases (see Events and Portals).


3. Create a root layout

The root is just a Layout that fills the viewport. Everything else is a child of it.

auto root = std::make_shared<Layout>(LayoutType::Stack);
root->size(Percent(100), Percent(100));
root->getInlineBase().itemsH = Align::Center;
root->getInlineBase().itemsV = Align::Center;

LayoutType::Stack lays out children on top of each other, positioned by itemsH / itemsV. Use LayoutType::Vertical for a column and LayoutType::Horizontal for a row. See Layout for the full list of container properties.

Equivalent root in ZMarkup

auto ui = ZMarkup::build(R"(
    Stack#root {
        // children go here
    }
)");
auto root = ui.root();

Then put the sizing / alignment into a .zstyle file:

#root {
    width: 100%;
    height: 100%;
    items-h: center;
    items-v: center;
}

Both approaches are equivalent. Choose whichever is more comfortable — you can mix them freely in the same project.


4. Add a widget

Widgets live in namespace ZenitUI::UI and are created via their ::create() factory (or a helper like Btn / Label).

auto btn = Btn("Click me", []{
    std::printf("clicked!\n");
});
btn->cls("btn-primary");

root->addChild(btn);

Btn(text, callback) is a small helper that creates a Button and adds a Text child with class "btn-text". If you want full control, use Button::create(cb) directly and add whatever child you want.

Every widget is a Layout subclass, so it inherits:

  • .cls("name") / .id("name") / .size(w, h) fluent helpers,
  • getInlineBase() for inline style overrides,
  • addChild() / removeFromParent(),
  • callback fields onClick, onPress, onRelease, onHoverEnter, onHoverExit, onRightClick.

The same widget in ZMarkup

auto ui = ZMarkup::build(R"(
    Stack#root {
        Button.btn-primary#go "Click me"
    }
)");
ui.onClick("go", []{ std::printf("clicked!\n"); });

You can also bind to #root if you want the click to fire when clicking outside the button — see Events for bubbling rules.

Styling the button

Without .zstyle, the button will use only the widget's built-in defaults. To actually make it look like something, load a stylesheet:

ZMarkup::loadStyleFile("assets/game.zstyle");
/* assets/game.zstyle */
.btn-primary {
    background: #0079F1;
    radius: 8px;
    padding: 1.5vh 2vw;
    min-width: 80px;
    transition: background 0.25s ease-out-quad, scale 0.12s ease-out-back;
}
.btn-primary:hover   { background: #3296FF; scale: 1.15; }
.btn-primary:pressed { background: #505050; scale: 0.90; }

loadStyleFile appends rules to the global Theme. Calling it multiple times is fine — rules accumulate, and later rules with the same specificity win. See ZStyle for the full language.


5. Run the frame loop

Each frame has two phases: a logic phase and a draw phase.

while (!WindowShouldClose()) {
    float dt = GetFrameTime();

    // ---- logic phase (no GL calls inside) ----
    root->runFrame(dt);

    // ---- draw phase ----
    BeginDrawing();
    ClearBackground(BLACK);
    root->renderFrame();
    EndDrawing();
}

runFrame(dt) expands to:

UIContext::get().beginFrame(dt);   // poll platform, refresh input, advance clock
root->updateTree(dt);              // state FSM, style resolve, animations, focus
root->measure(viewport.x, viewport.y);   // bottom-up intrinsic size
root->arrange({0, 0, viewport.x, viewport.y}); // top-down positioning

renderFrame() expands to:

renderer->beginFrame();   // e.g. push DPI scale matrix
root->draw();
renderer->endFrame();

Why the split? Because measure / arrange don't touch the GPU, and draw doesn't touch input. Keeping them apart makes headless tests trivial (see Mocks.hpp) and lets you run the logic at a different rate from rendering if you ever need to.


6. The complete program

#include <raylib.h>
#include "RaylibBackend.hpp"
#include "UI.hpp"
#include <cstdio>

using namespace ZenitUI;
using namespace ZenitUI::UI;

int main() {
    InitWindow(800, 600, "ZenitUI");
    SetWindowState(FLAG_WINDOW_RESIZABLE);

    RaylibRenderer      renderer;
    RaylibPlatform      platform;
    RaylibAssetProvider assets(renderer);

    UIContext::get().renderer = &renderer;
    UIContext::get().platform = &platform;
    UIContext::get().assets   = &assets;

    assets.loadFont("calibri", "assets/calibri.ttf", 64);
    ZMarkup::loadStyleFile("assets/game.zstyle");

    auto root = std::make_shared<Layout>(LayoutType::Stack);
    root->size(Percent(100), Percent(100));
    root->getInlineBase().itemsH = Align::Center;
    root->getInlineBase().itemsV = Align::Center;

    auto btn = Btn("Click me", []{ std::printf("clicked!\n"); });
    btn->cls("btn-primary");
    root->addChild(btn);

    while (!WindowShouldClose()) {
        root->runFrame(GetFrameTime());

        BeginDrawing();
        ClearBackground(GetColor(0x181818FF));
        root->renderFrame();
        EndDrawing();
    }

    CloseWindow();
    return 0;
}

You now have a running ZenitUI app with a styled button that reacts to hover, press, and click — the rules in game.zstyle do the styling, the Btn(...) helper does the rest.


7. Optional: keyboard focus

Add setFocusable(true) to a node if you want it to be reachable by Tab and to react to Enter / Space:

auto btn = Btn("Click me", []{ /* ... */ });
btn->setFocusable(true);
btn->setKeyboardActivates(true);   // Enter / Space fire onClick

Then press Tab at runtime — the button gains focus, :focus rules from .zstyle apply, and pressing Enter fires the click callback. See Events for the full focus / navigation model.


8. Next steps

  • Layout — grow, shrink, gap, justify, items-*, absolute positioning, units.
  • Widgets — every built-in component with props and events.
  • ZMarkup — build complex trees declaratively.
  • ZStyle — selector syntax, states, ::part, var(), calc(), @media.
  • Events — bubbling, pointer capture, focus, right-click, context menus.
  • Animations — transitions, @keyframes, imperative animations.
  • Portals — Popup, Tooltip, Dropdown, Modal.
  • Effects — filters, shaders, FilterRegistry.
  • Custom Widgets — build your own component.

Common pitfalls

"My widget doesn't appear." Check that (a) the parent has a non-zero size, (b) the widget has a non-zero intrinsic size or an explicit width / height, and (c) you called runFrame before renderFrame at least once — the first runFrame is what computes rect.

"My click callback never fires." The node must be interactive and reachable by hit-test. Layout defaults to interactive = true, but custom widgets sometimes set it to false (e.g. Panel, Text). Check setBlocksRaycast too — a node that only blocks raycast won't receive onClick.

"My style rules don't apply." - Selector specificity matters. .card Button beats Button. - Inline styles (getInlineBase()) beat any rule. - State rules (:hover, :checked) only match while the state flag is set on the node — see Events. - :focus requires setFocusable(true). - Media queries (@media) are evaluated against Metrics::viewport, not the OS window size — see ZStyle.

"My text is not wrapping." Set setWrap(true) on the Text (or wrap in ZMarkup). Wrapping also needs an available width — an auto-sized container gives the text an infinite width, so it will never wrap. Give the parent a fixed or percentage width.

"My font name doesn't resolve." Font names in .zstyle (font: calibri;) and ZMarkup (font="calibri") must match a name registered with IAssetProvider::loadFont. Names are case-sensitive.