# 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`. ```json { "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`. ```json { "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. ```json { "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. ```json { "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:** ```cpp 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. ```json { "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:** ```cpp 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. ```json { "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`, or `maxLines` are set, the text is drawn centered on `(x + width/2, y + height/2)` for backward compatibility. **C++ updates:** ```cpp 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. ```json { "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:** ```cpp 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]`. ```json { "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:** ```cpp 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. ```json { "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. ```json { "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:** ```cpp 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 ```cpp 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 ```cpp 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 ```cpp uiManager.setNodeVisible("hint5", false); bool visible = uiManager.getNodeVisible("hint5"); ``` ### Per-frame update ```cpp uiManager.update(deltaMs); // advance animations and fade-ins uiManager.draw(renderer); // render everything ```