14 KiB
UI System
UI layouts are defined in JSON files and loaded at runtime by UiManager. Each file has a single "root" node that is the top-level container.
The coordinate system has the origin at the bottom-left of the screen. Y increases upward.
The virtual canvas size is defined by Environment::projectionWidth × Environment::projectionHeight.
{
"root": { ... }
}
Common Node Properties
These properties are available on every node type.
| Property | Type | Default | Description |
|---|---|---|---|
name |
string | "" |
Unique name used to find the node from C++ code |
x |
float | 0 |
Horizontal offset from the parent's origin (or gravity-adjusted position) |
y |
float | 0 |
Vertical offset |
width |
float | "match_parent" |
0 |
Width in virtual pixels. "match_parent" fills the parent |
height |
float | "match_parent" |
0 |
Height in virtual pixels |
horizontal_gravity |
"left" | "center" | "right" |
"left" |
Positions the node horizontally inside a FrameLayout parent |
vertical_gravity |
"bottom" | "center" | "top" |
"bottom" |
Positions the node vertically inside a FrameLayout parent |
Containers
FrameLayout
Children are positioned using absolute x/y offsets and/or horizontal_gravity / vertical_gravity.
{
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"children": [ ... ]
}
| Property | Type | Default | Description |
|---|---|---|---|
children |
array | [] |
Child nodes |
LinearLayout
Children are stacked automatically in a row or column. Gravity and align properties control the layout of the block and its children.
{
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 10,
"width": 400,
"height": 600,
"children": [ ... ]
}
| Property | Type | Default | Description |
|---|---|---|---|
orientation |
"vertical" | "horizontal" |
"vertical" |
Direction children are stacked |
spacing |
float | 0 |
Gap in pixels between consecutive children |
vertical_align |
"top" | "center" | "bottom" |
"top" |
Vertical alignment of the child block inside this layout. For vertical orientation, controls how the whole stack is aligned; for horizontal orientation, controls each child's cross-axis alignment |
horizontal_align |
"left" | "center" | "right" |
"left" |
Horizontal alignment of the child block. For horizontal orientation, controls how the whole row is aligned; for vertical orientation, controls each child's cross-axis alignment |
children |
array | [] |
Child nodes, laid out in order |
Widgets
Button
An image-only clickable button. Swaps textures on hover/press.
{
"type": "Button",
"name": "closeButton",
"width": 90,
"height": 90,
"x": 580,
"y": 240,
"horizontal_gravity": "center",
"vertical_gravity": "center",
"textures": {
"normal": "resources/w/ui/img/Close001_State=Default.png",
"hover": "resources/w/ui/img/Close001_State=Selected.png",
"pressed": "resources/w/ui/img/Close001_State=Tap.png",
"disabled": "resources/w/ui/img/Close001_State=Disabled.png"
},
"border": 4,
"clickZoneWidth": 80,
"clickZoneHeight": 80
}
| Property | Type | Default | Description |
|---|---|---|---|
textures.normal |
string | — | Texture path shown in the default state (required) |
textures.hover |
string | — | Texture path shown when the mouse hovers |
textures.pressed |
string | — | Texture path shown while pressed |
textures.disabled |
string | — | Texture path shown when the button is disabled |
border |
float | 0 |
Inset (pixels) applied to the hit-test zone on all sides |
clickZoneWidth |
float | 0 |
Explicit hit-test width; 0 uses the widget width |
clickZoneHeight |
float | 0 |
Explicit hit-test height; 0 uses the widget height |
C++ callbacks:
uiManager.setButtonCallback("closeButton", [](const std::string&) { /* click */ });
uiManager.setButtonPressCallback("closeButton", [](const std::string&) { /* press */ });
TextButton
A button that renders a text label on top of an optional background texture.
{
"type": "TextButton",
"name": "item1name",
"width": 270,
"height": 60,
"text": "Main Quest",
"fontSize": 32,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": false,
"topAligned": false,
"textPaddingX": 12,
"textPaddingY": -8,
"wrap": true,
"color": [1.0, 1.0, 1.0, 1.0],
"textures": {
"normal": "resources/w/red.png",
"hover": "resources/w/red.png",
"pressed": "resources/w/red.png"
},
"border": 0,
"clickZoneWidth": 0,
"clickZoneHeight": 0
}
| Property | Type | Default | Description |
|---|---|---|---|
text |
string | "" |
Label text. Supports Cyrillic and any codepoint in resources/symbols.txt |
fontSize |
int | 32 |
Font size in pixels |
fontPath |
string | "resources/fonts/DroidSans.ttf" |
Path to the TTF font file |
textCentered |
bool | true |
Horizontally centers the text within the widget. When false, text starts at textPaddingX from the left edge |
topAligned |
bool | false |
When true, the first text line is placed near the top of the widget; when false, the text is vertically centered |
textPaddingX |
float | 12 |
Left padding when textCentered is false; also used to compute the wrapping width |
textPaddingY |
float | 0 |
Vertical offset applied to the text baseline |
wrap |
bool | false |
Wraps text that exceeds width - textPaddingX * 2 pixels |
color |
[R, G, B, A] | [1,1,1,1] |
Text color, each channel 0..1 |
textures.* |
string | — | Background textures (all optional — button can be text-only) |
border |
float | 0 |
Hit-test inset |
clickZoneWidth / clickZoneHeight |
float | 0 |
Explicit hit-test size; 0 uses the widget size |
C++ callbacks:
uiManager.setTextButtonCallback("item1name", [](const std::string&) { /* click */ });
uiManager.setTextButtonPressCallback("item1name", [](const std::string&) { /* press */ });
// Programmatic updates
uiManager.setTextButtonText("item1name", "New Quest Name");
uiManager.setTextButtonColor("item1name", {1.f, 0.f, 0.f, 1.f});
TextView
A non-interactive text display widget.
{
"type": "TextView",
"name": "quest_description",
"x": 170,
"y": 390,
"width": 1000,
"height": 300,
"text": "Long description here.",
"fontSize": 32,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": false,
"topAligned": true,
"wrap": true,
"paddingX": 0,
"paddingY": 4,
"maxLines": 10,
"color": [1.0, 1.0, 0.0, 1.0]
}
| Property | Type | Default | Description |
|---|---|---|---|
text |
string | "" |
Display text. Supports Cyrillic and any codepoint in resources/symbols.txt |
fontSize |
int | 32 |
Font size in pixels |
fontPath |
string | "resources/fonts/DroidSans.ttf" |
Path to the TTF font file |
textCentered |
bool | true |
Horizontally centers the text when true; left-aligns from paddingX when false |
topAligned |
bool | false |
When true, the first line is placed near the top edge; when false, text is vertically centered |
wrap |
bool | false |
Wraps text at width - paddingX * 2 pixels |
paddingX |
float | 0 |
Left/right padding used for alignment and wrap width |
paddingY |
float | 0 |
Vertical inset applied when topAligned is true |
maxLines |
int | 0 |
Maximum number of lines to display; 0 means unlimited. Truncated text gets ... |
color |
[R, G, B, A] | [1,1,1,1] |
Text color |
Legacy note: If none of
wrap,topAligned,paddingX,paddingY, ormaxLinesare set, the text is drawn centered on(x + width/2, y + height/2)for backward compatibility.
C++ updates:
uiManager.setText("quest_description", "New text here.");
uiManager.setTextColor("quest_description", {1.f, 1.f, 0.f, 1.f});
TextField
An interactive single-line text input field. Receives keyboard input when focused.
{
"type": "TextField",
"name": "playerName",
"x": 100,
"y": 300,
"width": 400,
"height": 50,
"placeholder": "Enter name...",
"fontSize": 28,
"fontPath": "resources/fonts/DroidSans.ttf",
"maxLength": 64,
"color": [1.0, 1.0, 1.0, 1.0],
"placeholderColor": [0.5, 0.5, 0.5, 1.0],
"backgroundColor": [0.2, 0.2, 0.2, 1.0],
"borderColor": [0.5, 0.5, 0.5, 1.0]
}
| Property | Type | Default | Description |
|---|---|---|---|
placeholder |
string | "" |
Text shown when the field is empty |
fontSize |
int | 32 |
Font size in pixels |
fontPath |
string | "resources/fonts/DroidSans.ttf" |
Path to the TTF font file |
maxLength |
int | 256 |
Maximum number of characters |
color |
[R, G, B, A] | [1,1,1,1] |
Input text color |
placeholderColor |
[R, G, B, A] | [0.5,0.5,0.5,1] |
Placeholder text color |
backgroundColor |
[R, G, B, A] | [0.2,0.2,0.2,1] |
Field background color |
borderColor |
[R, G, B, A] | [0.5,0.5,0.5,1] |
Border color |
C++ callbacks and queries:
uiManager.setTextFieldCallback("playerName", [](const std::string& name, const std::string& value) {
// called on every keystroke
});
std::string current = uiManager.getTextFieldValue("playerName");
Slider
A draggable slider that returns a normalized value in the range [0, 1].
{
"type": "Slider",
"name": "volumeSlider",
"x": 100,
"y": 200,
"width": 40,
"height": 300,
"orientation": "vertical",
"value": 0.75,
"textures": {
"track": "resources/w/ui/img/slider_track.png",
"knob": "resources/w/ui/img/slider_knob.png"
}
}
| Property | Type | Default | Description |
|---|---|---|---|
textures.track |
string | — | Texture for the slider track |
textures.knob |
string | — | Texture for the draggable knob |
orientation |
"vertical" | "horizontal" |
"vertical" |
Drag direction |
value |
float | 0 |
Initial normalized value [0, 1] |
C++ callbacks:
uiManager.setSliderCallback("volumeSlider", [](const std::string& name, float value) {
// value is 0..1
});
uiManager.setSliderValue("volumeSlider", 0.5f);
StaticImage
A non-interactive image. Supports optional fade-in and pulse-scale animations.
{
"type": "StaticImage",
"name": "background",
"width": 1266,
"height": 585,
"horizontal_gravity": "center",
"vertical_gravity": "center",
"texture": "resources/w/ui/img/journal/QuestJournal003.png",
"fadeIn": {
"durationMs": 600
},
"pulse": {
"minScale": 0.92,
"maxScale": 1.08,
"periodMs": 1500
}
}
| Property | Type | Default | Description |
|---|---|---|---|
texture |
string | — | Path to the PNG texture |
fadeIn.durationMs |
float | — | If present, the image fades in over this many milliseconds each time the UI is shown |
pulse.minScale |
float | 0.9 |
Minimum scale during the pulse cycle |
pulse.maxScale |
float | 1.1 |
Maximum scale during the pulse cycle |
pulse.periodMs |
float | 1000 |
Duration of one full pulse cycle in milliseconds |
Animations
Animations can be defined on Button and TextButton nodes and started from C++ code. Each animation is a named sequence of steps.
{
"type": "Button",
"name": "myButton",
"width": 100,
"height": 100,
"textures": { "normal": "resources/w/btn.png" },
"animations": {
"bounce": {
"repeat": false,
"steps": [
{ "type": "move", "to": [0, 20], "duration": 0.15, "easing": "easeout" },
{ "type": "move", "to": [0, 0], "duration": 0.15, "easing": "easein" },
{ "type": "wait", "duration": 0.1 }
]
},
"pulse": {
"repeat": true,
"steps": [
{ "type": "scale", "to": [1.1, 1.1], "duration": 0.4, "easing": "easeout" },
{ "type": "scale", "to": [1.0, 1.0], "duration": 0.4, "easing": "easein" }
]
}
}
}
Animation sequence properties
| Property | Type | Default | Description |
|---|---|---|---|
repeat |
bool | false |
Whether the sequence loops after the last step |
steps |
array | — | Ordered list of animation steps |
Step properties
| Property | Type | Description |
|---|---|---|
type |
"move" | "scale" | "wait" |
Step kind |
to |
[x, y] | Target offset (move) or scale factors (scale) |
duration |
float (seconds) | Duration of the step. 0 applies the target instantly |
easing |
"linear" | "easein" | "easeout" |
Interpolation curve (default "linear") |
C++ control:
uiManager.startAnimationOnNode("myButton", "bounce");
uiManager.stopAnimationOnNode("myButton", "bounce");
uiManager.setAnimationCallback("myButton", "bounce", []() {
// called when the non-repeating sequence finishes
});
C++ API Quick Reference
Loading and navigation
uiManager.loadFromFile("resources/w/ui/screen.json", renderer);
uiManager.pushMenuFromFile("resources/w/ui/popup.json", renderer); // push on stack
uiManager.popMenu(); // restore previous UI
uiManager.clearMenuStack();
Finding nodes
auto node = uiManager.findNode("myNode");
auto btn = uiManager.findButton("myButton");
auto tbtn = uiManager.findTextButton("item1name");
auto tv = uiManager.findTextView("quest_description");
auto img = uiManager.findStaticImage("background");
auto slider = uiManager.findSlider("volumeSlider");
auto tf = uiManager.findTextField("playerName");
Visibility
uiManager.setNodeVisible("hint5", false);
bool visible = uiManager.getNodeVisible("hint5");
Per-frame update
uiManager.update(deltaMs); // advance animations and fade-ins
uiManager.draw(renderer); // render everything