# 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 |
| `visible` | bool | `true` | Whether the node (and all its children) are rendered and interactive. Can be toggled at runtime via `setNodeVisible` |

---

## 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 |

**C++ pop-in animation** (scales the node from 0 → 1, ease-out quad):
```cpp
uiManager.startPopIn("background", 300.0f);  // duration in milliseconds
```
Typically called immediately after making a node visible. The node is automatically removed from the animation list when the scale reaches 1.

---

## Touch / Click Priority

All `Button` and `TextButton` nodes in a layout are collected into a single ordered list during `collectButtonsAndSliders` (depth-first traversal of the node tree, which matches JSON declaration order). When a touch or mouse-down event arrives, the list is scanned **in reverse** — later-declared nodes are checked first — and the **first hit wins**. At most one element (button or textButton, regardless of type) fires per touch.

**Practical rule:** place background "catch-all" elements (e.g. a full-screen transparent exit button) **early** in the JSON, and foreground interactive elements **later**. The later-declared element will always win when they overlap.

```json
{
    "type": "FrameLayout",
    "children": [
        {
            "type": "Button",
            "name": "phoneExitButton",   // declared first → lowest priority
            "width": "match_parent",
            "height": "match_parent",
            "textures": { "normal": "resources/transparent.png", ... }
        },
        {
            "type": "TextButton",
            "name": "chat2button",       // declared later → wins over phoneExitButton
            ...
        }
    ]
}
```

This behaviour is consistent across `Button` and `TextButton` — there is no inherent type priority, only declaration order matters.

---

## 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");
```

### Pop-in animation

Scales a node from 0 to 1 using an ease-out curve. Useful for chat bubble reveals and similar "appear" effects.

```cpp
uiManager.startPopIn("messageBubble", 300.0f);  // node name, duration ms
```

Set the node's `scaleX`/`scaleY` to `0` and call `setNodeVisible` before calling `startPopIn` to avoid a one-frame flash at full size.

### Dynamic node repositioning

`node->localY` (and `localX`) can be modified directly on a node pointer, then a layout recalculation applied:

```cpp
auto node = uiManager.findNode("messageBubble");
node->localY = 350.0f;          // new bottom-Y (for vertical_gravity: bottom nodes)
uiManager.updateAllLayouts();   // recomputes screenRect and rebuilds meshes
```

This is how the phone chat manager repositions bubbles as new messages arrive.

### Per-frame update

```cpp
uiManager.update(deltaMs);   // advance animations and fade-ins
uiManager.draw(renderer);    // render everything
```

---

## Dialogue → UI integration (phone chat bubbles)

Dialogue nodes in JSON can carry a `"bubbleSlot"` field naming a `StaticImage` UI node. When the dialogue runtime presents that line, it fires the `onBubbleSlotReady` callback with the slot name, which the game uses to reveal the corresponding bubble image.

```json
{
    "id": "line_1",
    "type": "Line",
    "speaker": "Айпери",
    "text": "...",
    "next": "line_2",
    "bubbleSlot": "message01in"
}
```

Lines without `"bubbleSlot"` (or with an empty value) do not trigger any UI change — useful for internal monologue lines that have no corresponding chat image.

**C++ wiring:**
```cpp
dialogueSystem.setOnBubbleSlotReady([](const std::string& slotName) {
    // slotName == "message01in" etc.
    menuManager.revealPhoneChatBubble(slotName);
});
```
