UI System¶
The UI system is a lightweight scene graph for screen-space UI. Each Scene owns a UIRoot (created via UIElementRegistry) that manages layout, input, and rendering. UI elements are not Components; they live in their own tree under the root.
Creating UI Elements¶
using namespace snowpulse;
auto* ui = context()->uiRegistry();
auto* root = scene()->rootUI();
auto* panel = ui->create<UIRect>("panel");
panel->color = { 0.08f, 0.08f, 0.1f, 0.9f };
panel->transform.anchorMin = { 0.5f, 0.5f };
panel->transform.anchorMax = { 0.5f, 0.5f };
panel->transform.pivot = { 0.5f, 0.5f };
panel->transform.position = { 0.0f, 0.0f };
panel->transform.size = { 420.0f, 260.0f };
root->addChild(panel);
Transforms, Anchors, and Stretching¶
UITransform uses normalized anchors (anchorMin/anchorMax) and a pivot to place and size elements relative to their parent. If the anchors differ, the element stretches along that axis.
The runtime computes the final rect like this:
finalSize = transform.size + parentSize * (anchorMax - anchorMin)
anchorCenter = parentMin + parentSize * ((anchorMin + anchorMax) * 0.5)
finalMin = anchorCenter + transform.position - transform.pivot * finalSize
On stretched axes, position offsets from the anchor center and size acts like an additive delta.
using namespace snowpulse;
auto* badge = ui->create<UIImage>("badge");
badge->sprite(assetManager()->loadSprite("ui_badge.png"));
badge->transform.anchorMin = { 1.0f, 1.0f };
badge->transform.anchorMax = { 1.0f, 1.0f };
badge->transform.pivot = { 1.0f, 1.0f };
badge->transform.position = { -12.0f, -12.0f };
root->addChild(badge);
auto* footer = ui->create<UIRect>("footer");
footer->transform.anchorMin = { 0.0f, 0.0f };
footer->transform.anchorMax = { 1.0f, 0.0f };
footer->transform.pivot = { 0.5f, 0.0f };
footer->transform.size = { 0.0f, 48.0f }; // stretch width, fixed height
root->addChild(footer);
For a fully stretched element (anchorMin = {0,0}, anchorMax = {1,1}, pivot = {0.5,0.5}):
position = {0, -20},size = {0, 20}does not create symmetric 20px margins.- For symmetric top/bottom 20px inset, use
position.y = 0andsize.y = -40.
Debug Rects¶
Any UIElement can render a debug overlay for its computed rect:
auto* panel = ui->create<UIElement>("panel");
panel->showDebugRect = true;
panel->debugRectColor = { 1.0f, 0.0f, 0.0f, 0.2f };
Layout Containers (Stack and Grid)¶
Set layoutType on a parent element to automatically arrange its children using their preferredSize().
using namespace snowpulse;
auto* menu = ui->create<UIElement>("menu");
menu->layoutType = UILayoutType::Stack;
menu->stackLayout.direction = UIStackDirection::Vertical;
menu->stackLayout.align = UIStackAlign::Center;
menu->stackLayout.spacing = 12.0f;
menu->stackLayout.padding = { 16.0f, 16.0f, 12.0f, 12.0f };
menu->stackLayout.reverse = false;
menu->transform.anchorMin = { 0.5f, 0.5f };
menu->transform.anchorMax = { 0.5f, 0.5f };
menu->transform.pivot = { 0.5f, 0.5f };
menu->transform.size = { 260.0f, 320.0f };
root->addChild(menu);
auto* play = ui->create<UIButton>("play");
play->setSprite(assetManager()->loadSprite("button_play.png"));
menu->addChild(play);
auto* options = ui->create<UIButton>("options");
options->setSprite(assetManager()->loadSprite("button_options.png"));
menu->addChild(options);
using namespace snowpulse;
auto* grid = ui->create<UIElement>("inventory");
grid->layoutType = UILayoutType::Grid;
grid->gridLayout.columns = 3;
grid->gridLayout.cellSize = { 64.0f, 64.0f };
grid->gridLayout.spacing = { 8.0f, 8.0f };
grid->gridLayout.padding = { 8.0f, 8.0f, 8.0f, 8.0f };
grid->gridLayout.horizontalAlign = UIStackAlign::Center;
grid->gridLayout.verticalAlign = UIStackAlign::Start;
grid->transform.size = { 240.0f, 240.0f };
root->addChild(grid);
for (int i = 0; i < 6; ++i) {
auto* icon = ui->create<UIImage>();
icon->sprite(assetManager()->loadSprite("item_icon.png"));
grid->addChild(icon);
}
Built-in Elements¶
UIRect¶
UIRect draws a solid color quad. It is useful for panels, overlays, and blockers.
It exposes the same scale9 and textureFiltering submission options as other
UI quads, but has no sprite; use UIImage for a textured nine-slice panel.
using namespace snowpulse;
auto* dimmer = ui->create<UIRect>("dimmer");
dimmer->color = { 0.0f, 0.0f, 0.0f, 0.6f };
dimmer->transform.anchorMin = { 0.0f, 0.0f };
dimmer->transform.anchorMax = { 1.0f, 1.0f };
dimmer->transform.size = { 0.0f, 0.0f };
root->addChild(dimmer);
UIImage¶
UIImage renders a sprite atlas region or standalone texture. Enable scale9 for nine-slice scaling.
The four border values are left, right, top, and bottom measurements in source-sprite pixels. When the
destination is smaller than the sprite's native size, Snowpulse uniformly scales the destination borders
down using the most constrained axis so corners keep their proportions. Borders remain fixed at native
size when the destination is the same size or larger. UIImage also supports fillAmount (0..1) with
Linear and Radial fill modes.
using namespace snowpulse;
auto* logo = ui->create<UIImage>("logo");
logo->sprite(assetManager()->loadSprite("ui_logo.png"));
logo->scale9.enabled = true;
logo->scale9.border = { 12.0f, 12.0f, 12.0f, 12.0f };
root->addChild(logo);
using namespace snowpulse;
auto* cooldown = ui->create<UIImage>("cooldown");
cooldown->sprite(assetManager()->loadSprite("ui_cooldown_ring.png"));
cooldown->fillAmount = 0.65f;
cooldown->fillType = UIImageFillType::Radial;
cooldown->radialStartAngle = 90.0f;
cooldown->radialDirection = UIImageRadialDirection::Clockwise;
root->addChild(cooldown);
Linear mode uses linearDirection (Left, Right, Top, Bottom).
Radial mode uses radialStartAngle (degrees) and radialDirection (Clockwise, CounterClockwise).
radialStartAngle uses 0° at +X (to the right), increasing counterclockwise.
If scale9 is active and valid, nine-slice rendering takes priority over partial fill.
UIText¶
UIText renders MSDF fonts with alignment controls.
UIText::effects can add a colored MSDF outline and drop shadow without a
separate font atlas. Effect widths, spreads, softness, and offsets use the same
local units as fontSize. Very thick effects require a font atlas generated
with enough MSDF distance range and padding.
UIText::color supplies the solid fill by default. Set gradient.enabled for
a two-color vertical fill across the complete visible text layout. Gradient
colors replace the solid fill while outline and shadow colors remain unchanged.
Defaults:
wordWrap = trueoverflowMode = UITextOverflowMode::Ellipsis(Cutoffis also available)autoSize = false
autoSize only updates non-stretched axes. With wrapping enabled, width stays as the wrap constraint and height auto-fits content.
using namespace snowpulse;
auto* label = ui->create<UIText>("label");
label->font(assetManager()->loadFont("fonts/04b_19.json"));
label->fontSize = 32.0f;
label->text = "Start";
label->wordWrap = true;
label->overflowMode = UITextOverflowMode::Ellipsis;
label->autoSize = false;
label->gradient.enabled = true;
label->gradient.topColor = { 1.0f, 0.96f, 0.82f, 1.0f };
label->gradient.bottomColor = { 0.96f, 0.68f, 0.22f, 1.0f };
label->effects.outline.width = 2.0f;
label->effects.outline.color = { 0.05f, 0.08f, 0.12f, 1.0f };
label->effects.shadow.enabled = true;
label->effects.shadow.color = { 0.0f, 0.0f, 0.0f, 0.45f };
label->effects.shadow.setDirection({ 1.0f, -1.0f }, 4.0f);
label->horizontalAlign = UITextHorizontalAlign::Center;
label->verticalAlign = UITextVerticalAlign::Middle;
label->transform.size = { 200.0f, 60.0f };
root->addChild(label);
UIButton¶
UIButton provides hover/press states and callbacks. You can assign different sprites per state.
using namespace snowpulse;
auto* button = ui->create<UIButton>("playButton");
button->setSprite(assetManager()->loadSprite("button_idle.png"));
button->setSprite(assetManager()->loadSprite("button_hover.png"), UIButtonState::Hovered);
button->setSprite(assetManager()->loadSprite("button_down.png"), UIButtonState::Pressed);
button->onClickListener = []() {
// Start game
};
root->addChild(button);
onPressedListener fires on pointer down. onReleasedListener fires only when the pointer is released over the button (release-inside behavior). onHoverChangedListener reports hover enter/exit.
UIScrollView¶
UIScrollView clips its children and handles mouse wheel scrolling. Add children to its content element.
using namespace snowpulse;
auto* scroll = ui->create<UIScrollView>("list");
scroll->axis = UIScrollAxis::Vertical;
scroll->scrollSpeed = 40.0f;
scroll->transform.size = { 300.0f, 200.0f };
root->addChild(scroll);
auto* content = scroll->content();
content->layoutType = UILayoutType::Stack;
content->stackLayout.direction = UIStackDirection::Vertical;
content->stackLayout.spacing = 8.0f;
for (int i = 0; i < 5; ++i) {
auto* row = ui->create<UIText>();
row->font(assetManager()->loadFont("fonts/04b_19.json"));
row->text = "Row " + std::to_string(i + 1);
content->addChild(row);
}
scroll->contentSize({ 300.0f, 400.0f });
scroll->scrollOffset({ 0.0f, 80.0f });
Set axis to Vertical, Horizontal, or Both. contentSize() overrides the
content extent used for clamping; scrollOffset() can query or set the current
offset. Setting enabled = false disables wheel handling without deactivating
the element. Scroll clipping is applied to descendants of the content element.
Input, Sorting, and Cleanup¶
- Set
interactableto receive pointer events andblocksInputto stop clicks from passing through. - Use
sortOrderorbringToFront()/sendToBack()for draw order. - Remove UI elements with
Scene::destroyUIElement()(orScript::destroyUIElement()).
using namespace snowpulse;
button->sortOrder = 10;
button->blocksInput = true;
scene()->destroyUIElement(button);
World-Space UI¶
By default, UI renders in screen space using the logical resolution. Enable view transforms to make UI follow the camera.
using namespace snowpulse;
scene()->rootUI()->useViewTransform(true); // UI is now in world space
scene()->rootUI()->fixedSize({ 1920.0f, 1080.0f });
fixedSize() also works with screen-space UI when a fixed canvas is needed.
Call clearFixedSize() to return to the app's current logical resolution.