From c659293bf8a981b0a04f9f387477f50be695d752 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Fri, 29 May 2026 12:44:55 +0300 Subject: [PATCH] Working on UI --- UI.md | 441 ++++++++++++++++++ resources/config2/ui_quest_journal.json | 16 +- resources/w/ui/img/journal/ButtonBkg001.png | 3 + .../img/journal/ButtonBkgTransparent001.png | 3 + resources/w/ui/screen_inventory.json | 4 +- resources/w/ui/screen_journal.json | 113 +++-- src/Game.cpp | 2 - src/UiManager.cpp | 26 +- src/UiManager.h | 4 +- 9 files changed, 550 insertions(+), 62 deletions(-) create mode 100644 UI.md create mode 100644 resources/w/ui/img/journal/ButtonBkg001.png create mode 100644 resources/w/ui/img/journal/ButtonBkgTransparent001.png diff --git a/UI.md b/UI.md new file mode 100644 index 0000000..df585af --- /dev/null +++ b/UI.md @@ -0,0 +1,441 @@ +# 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 +``` diff --git a/resources/config2/ui_quest_journal.json b/resources/config2/ui_quest_journal.json index 307fecd..3481197 100644 --- a/resources/config2/ui_quest_journal.json +++ b/resources/config2/ui_quest_journal.json @@ -32,7 +32,7 @@ "text": "QUESTS", "fontSize": 32, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": true, + "textCentered": true, "topAligned": true, "paddingY": 2.0, "color": [1.0, 1.0, 1.0, 1.0] @@ -83,7 +83,7 @@ "text": "ЗАДАНИЯ", "fontSize": 22, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, + "textCentered": false, "topAligned": true, "paddingX": 4.0, "paddingY": 0.0, @@ -234,7 +234,7 @@ "text": "Выберите задание", "fontSize": 22, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, + "textCentered": false, "topAligned": true, "wrap": true, "paddingX": 8.0, @@ -252,7 +252,7 @@ "text": "", "fontSize": 16, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, + "textCentered": false, "topAligned": true, "wrap": true, "paddingX": 8.0, @@ -270,7 +270,7 @@ "text": "ЦЕЛИ", "fontSize": 20, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, + "textCentered": false, "topAligned": true, "paddingX": 8.0, "paddingY": 0.0, @@ -286,7 +286,7 @@ "text": "", "fontSize": 17, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, + "textCentered": false, "topAligned": true, "wrap": true, "paddingX": 8.0, @@ -304,7 +304,7 @@ "text": "Описание задания", "fontSize": 22, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, + "textCentered": false, "topAligned": true, "paddingX": 8.0, "paddingY": 0.0, @@ -320,7 +320,7 @@ "text": "", "fontSize": 18, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, + "textCentered": false, "topAligned": true, "wrap": true, "paddingX": 8.0, diff --git a/resources/w/ui/img/journal/ButtonBkg001.png b/resources/w/ui/img/journal/ButtonBkg001.png new file mode 100644 index 0000000..7fae81e --- /dev/null +++ b/resources/w/ui/img/journal/ButtonBkg001.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c6806a2a3a255023314354f5d506354a387f347d87211729932e62411dca0d4 +size 784 diff --git a/resources/w/ui/img/journal/ButtonBkgTransparent001.png b/resources/w/ui/img/journal/ButtonBkgTransparent001.png new file mode 100644 index 0000000..c151e5a --- /dev/null +++ b/resources/w/ui/img/journal/ButtonBkgTransparent001.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15d4513848f7fed94816e4f9eff6809a261b2ed5473550bc98c272311e3b73e7 +size 366 diff --git a/resources/w/ui/screen_inventory.json b/resources/w/ui/screen_inventory.json index ff96819..df2c86d 100644 --- a/resources/w/ui/screen_inventory.json +++ b/resources/w/ui/screen_inventory.json @@ -170,7 +170,7 @@ "text": "Серебряный нож.", "fontSize": 24, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, + "textCentered": false, "topAligned": true, "wrap": true, "paddingX": 0.0, @@ -189,7 +189,7 @@ "text": "А все бегут бегут бегут бегут бегут бегут бегут бегут бегут бегут бегут бегут бегут бегут бегут бегут бегут бегут а мы идем.", "fontSize": 24, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, + "textCentered": false, "topAligned": true, "wrap": true, "paddingX": 0.0, diff --git a/resources/w/ui/screen_journal.json b/resources/w/ui/screen_journal.json index 755d11b..ac099ec 100644 --- a/resources/w/ui/screen_journal.json +++ b/resources/w/ui/screen_journal.json @@ -34,11 +34,11 @@ { "type": "LinearLayout", "orientation": "vertical", - "vertical_align": "center", + "vertical_align": "top", "horizontal_align": "center", "spacing": 0, "x": 0, - "y": -195, + "y": 195, "width": 290, "height": 800, "children": [ @@ -48,25 +48,25 @@ "x": 0.0, "y": 0.0, "width": 270.0, - "height": 60.0, - "text": "Задание 1", - "textPaddingY": -8.0, + "height": 90, + "text": "Главное Задание 1", + "textPaddingY": 4.0, + "textPaddingX": 16.0, "fontSize": 32, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, - "textCentered": false, - "topAligned": false, + "textCentered": false, + "topAligned": true, "wrap": true, "color": [ - 1.0, - 1.0, - 1.0, + 0.996, + 0.977, + 0.761, 1.0 ], "textures": { - "normal": "resources/w/red.png", - "hover": "resources/w/red.png", - "pressed": "resources/w/red.png" + "normal": "resources/w/ui/img/journal/ButtonBkg001.png", + "hover": "resources/w/ui/img/journal/ButtonBkg001.png", + "pressed": "resources/w/ui/img/journal/ButtonBkg001.png" } }, { @@ -77,23 +77,50 @@ "width": 270.0, "height": 60.0, "text": "Задание 2", - "textPaddingY": -8.0, + "textPaddingY": 4.0, + "textPaddingX": 16.0, "fontSize": 32, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, - "textCentered": false, - "topAligned": false, + "textCentered": false, + "topAligned": true, "wrap": true, "color": [ - 1.0, - 1.0, - 1.0, + 0.996, + 0.977, + 0.761, 1.0 ], "textures": { - "normal": "resources/w/blue.png", - "hover": "resources/w/blue.png", - "pressed": "resources/w/blue.png" + "normal": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "hover": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "pressed": "resources/w/ui/img/journal/ButtonBkgTransparent001.png" + } + }, + { + "type": "TextButton", + "name": "item3name", + "x": 0.0, + "y": 0.0, + "width": 270.0, + "height": 60.0, + "text": "Задание 3", + "textPaddingY": 4.0, + "textPaddingX": 16.0, + "fontSize": 32, + "fontPath": "resources/fonts/DroidSans.ttf", + "textCentered": false, + "topAligned": true, + "wrap": true, + "color": [ + 0.02, + 0.875, + 0.447, + 0.6 + ], + "textures": { + "normal": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "hover": "resources/w/ui/img/journal/ButtonBkgTransparent001.png", + "pressed": "resources/w/ui/img/journal/ButtonBkgTransparent001.png" } } ] @@ -107,15 +134,15 @@ "height": 44.0, "horizontal_gravity": "center", "vertical_gravity": "top", - "text": "Выбрано задание 1", + "text": "Главное Задание 1", "fontSize": 32, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, + "textCentered": false, "topAligned": false, "wrap": true, "color": [ - 1.0, - 1.0, + 0.992, + 0.78, 0.0, 1.0 ] @@ -156,14 +183,14 @@ "text": "Цель 1 Цель 1 Цель 1 Цель 1", "fontSize": 32, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, + "textCentered": false, "topAligned": false, "wrap": true, "color": [ - 1.0, - 1.0, - 1.0, - 1.0 + 0.02, + 0.875, + 0.447, + 0.6 ] } ] @@ -193,14 +220,14 @@ "text": "Цель 2", "fontSize": 32, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, + "textCentered": false, "topAligned": false, "wrap": true, "color": [ - 1.0, - 1.0, - 1.0, - 1.0 + 0.996, + 0.977, + 0.761, + 0.9 ] } ] @@ -219,14 +246,14 @@ "text": "А все бегут бегут бегут бегут бегут бегут бегут бегут бегут бегут бегут а мы идем.", "fontSize": 32, "fontPath": "resources/fonts/DroidSans.ttf", - "centered": false, + "textCentered": false, "topAligned": false, "wrap": true, "color": [ - 1.0, - 1.0, - 0.0, - 1.0 + 0.996, + 0.977, + 0.761, + 0.75 ] } ] diff --git a/src/Game.cpp b/src/Game.cpp index 905a063..8b85a4b 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -234,7 +234,6 @@ namespace ZL */ - uniInteriorParams.navigationJsonPaths = { "resources/navigation/uni_interior3_all_locked.json", "resources/navigation/uni_interior3_hall.json", @@ -454,7 +453,6 @@ namespace ZL glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); - renderer.shaderManager.PushShader(defaultShaderName); renderer.RenderUniform1i(textureUniformName, 0); glEnable(GL_BLEND); diff --git a/src/UiManager.cpp b/src/UiManager.cpp index b48c8a2..f9506fc 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -210,12 +210,24 @@ namespace ZL { // Draw text on top (uses absolute coords, add anim offset manually) // use left padding, which is required for inventory/quest lists. if (textRenderer && !text.empty()) { + const float scale = 1.0f; + const std::string displayText = wrap + ? wrapTextByPixels(text, *textRenderer, rect.w - textPaddingX * 2.0f, scale) + : text; + float tx = rect.x + rect.w / 2.0f + animOffsetX; if (!textCentered) { tx = rect.x + textPaddingX + animOffsetX; } - const float ty = rect.y + rect.h * 0.5f + textPaddingY + animOffsetY; - textRenderer->drawText(text, tx, ty, 1.0f, textCentered, color); + + float ty; + if (topAligned) { + ty = rect.y + rect.h - textPaddingY - static_cast(fontSize) + animOffsetY; + } else { + ty = rect.y + rect.h * 0.5f + textPaddingY + animOffsetY; + } + + textRenderer->drawText(displayText, tx, ty, scale, textCentered, color); } glEnable(GL_DEPTH_TEST); } @@ -239,7 +251,7 @@ namespace ZL { rect.x + rect.w * 0.5f, rect.y + rect.h * 0.5f, scale, - centered, + textCentered, color ); return; @@ -250,7 +262,7 @@ namespace ZL { ? wrapTextByPixels(text, *textRenderer, availableWidth, scale, maxLines) : limitLines(text, maxLines); - float tx = centered ? rect.x + rect.w * 0.5f : rect.x + paddingX; + float tx = textCentered ? rect.x + rect.w * 0.5f : rect.x + paddingX; float ty = rect.y + rect.h * 0.5f; if (topAligned) { @@ -259,7 +271,7 @@ namespace ZL { ty = rect.y + rect.h - paddingY - static_cast(fontSize); } - textRenderer->drawText(finalText, tx, ty, scale, centered, color); + textRenderer->drawText(finalText, tx, ty, scale, textCentered, color); } void UiSlider::buildTrackMesh() { @@ -623,6 +635,8 @@ namespace ZL { if (j.contains("textCentered")) tb->textCentered = j["textCentered"].get(); if (j.contains("textPaddingX")) tb->textPaddingX = j["textPaddingX"].get(); if (j.contains("textPaddingY")) tb->textPaddingY = j["textPaddingY"].get(); + if (j.contains("wrap")) tb->wrap = j["wrap"].get(); + if (j.contains("topAligned")) tb->topAligned = j["topAligned"].get(); if (j.contains("color") && j["color"].is_array() && j["color"].size() == 4) { for (int i = 0; i < 4; ++i) tb->color[i] = j["color"][i].get(); } @@ -723,7 +737,7 @@ namespace ZL { tv->color[i] = j["color"][i].get(); } } - if (j.contains("centered")) tv->centered = j["centered"].get(); + if (j.contains("textCentered")) tv->textCentered = j["textCentered"].get(); if (j.contains("wrap")) tv->wrap = j["wrap"].get(); if (j.contains("topAligned")) tv->topAligned = j["topAligned"].get(); if (j.contains("paddingX")) tv->paddingX = j["paddingX"].get(); diff --git a/src/UiManager.h b/src/UiManager.h index 6d8e84f..0f033d1 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -169,6 +169,8 @@ namespace ZL { bool textCentered = true; float textPaddingX = 12.0f; float textPaddingY = 0.0f; + bool wrap = false; + bool topAligned = false; std::unique_ptr textRenderer; @@ -192,7 +194,7 @@ namespace ZL { std::string fontPath = "resources/fonts/DroidSans.ttf"; int fontSize = 32; std::array color = { 1.f, 1.f, 1.f, 1.f }; // rgba - bool centered = true; + bool textCentered = true; bool wrap = false; bool topAligned = true; float paddingX = 0.0f;