Working on UI

This commit is contained in:
Vladislav Khorev 2026-05-30 22:27:09 +03:00
parent 9425f9b19a
commit a0550daf4b
6 changed files with 111 additions and 23 deletions

30
UI.md
View File

@ -352,6 +352,36 @@ Typically called immediately after making a node visible. The node is automatica
---
## 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.

View File

@ -200,7 +200,7 @@
},
{
"type": "TextView",
"name": "chat2msg",
"name": "chat3msg",
"x": 100.0,
"y": 36.0,
"width": 446.25,

View File

@ -81,9 +81,9 @@ namespace ZL {
hudStep5aRoot = loadUiFromFile("resources/w/ui/hud_step5a.json", renderer, zipFile);
hudStep5bRoot = loadUiFromFile("resources/w/ui/hud_step5b.json", renderer, zipFile);
hudStep5abRoot = loadUiFromFile("resources/w/ui/hud_step5ab.json", renderer, zipFile);
//phoneScreenRoot = loadUiFromFile("resources/w/ui/screen_phone_chat_list.json", renderer, zipFile);
phoneScreenRoot = loadUiFromFile("resources/w/ui/screen_phone_chat2.json", renderer, zipFile);
//phoneScreenRoot = loadUiFromFile("resources/w/ui/screen_phone_chat1.json", renderer, zipFile);
phoneChatListRoot = loadUiFromFile("resources/w/ui/screen_phone_chat_list.json", renderer, zipFile);
phoneChat1Root = loadUiFromFile("resources/w/ui/screen_phone_chat1.json", renderer, zipFile);
phoneChat2Root = loadUiFromFile("resources/w/ui/screen_phone_chat2.json", renderer, zipFile);
newInventoryRoot = loadUiFromFile("resources/w/ui/screen_inventory.json", renderer, zipFile);
questJournalRoot = loadUiFromFile("resources/w/ui/screen_journal.json", renderer, zipFile);
@ -197,20 +197,57 @@ namespace ZL {
void MenuManager::openPhoneScreen() {
state = GameState::PhoneScreen;
tutorialPhoneScreenOpened = true;
// Hide the phone hint on the current HUD so it stays hidden when we return.
uiManager.setNodeVisible("hint6a", false);
uiManager.pushMenuFromSavedRoot(phoneScreenRoot);
uiManager.pushMenuFromSavedRoot(phoneChatListRoot);
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
closePhoneScreen();
});
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
// Reset chat state and start the dialogue
phoneChatVisibleBubbles_.clear();
resetPhoneChatNodes();
if (startDialogueFunc) {
startDialogueFunc("dialog_chat_aiperi001");
uiManager.setTextButtonCallback("chat1button", [this](const std::string&) {
openPhoneChatFromList(phoneChat1Root, "dialog_chat_parents001");
});
uiManager.setTextButtonCallback("chat2button", [this](const std::string&) {
openPhoneChatFromList(phoneChat2Root, "dialog_chat_aiperi001");
});
uiManager.setTextButtonCallback("chat3button", [this](const std::string&) {
// not yet implemented
});
}
void MenuManager::openPhoneChatFromList(std::shared_ptr<UiNode> chatRoot, const std::string& dialogueId) {
phoneChatVisibleBubbles_.clear();
uiManager.pushMenuFromSavedRoot(chatRoot);
const bool firstOpen = dialogueId.empty() || startedDialogues_.find(dialogueId) == startedDialogues_.end();
if (firstOpen) {
resetPhoneChatNodes();
}
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
closePhoneScreenFromChat();
});
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
uiManager.setTextButtonCallback("chatTitleButton", [this](const std::string&) {
returnToPhoneChatList();
});
if (firstOpen && startDialogueFunc && !dialogueId.empty()) {
startedDialogues_.insert(dialogueId);
startDialogueFunc(dialogueId);
}
}
void MenuManager::returnToPhoneChatList() {
phoneChatVisibleBubbles_.clear();
uiManager.popMenu();
}
void MenuManager::closePhoneScreenFromChat() {
state = GameState::Gameplay;
phoneChatVisibleBubbles_.clear();
uiManager.popMenu();
uiManager.popMenu();
}
void MenuManager::closePhoneScreen() {

View File

@ -8,6 +8,7 @@
#include <vector>
#include <string>
#include <memory>
#include <unordered_set>
namespace ZL {
@ -72,6 +73,9 @@ namespace ZL {
void setupStep5Callbacks();
void resetPhoneChatNodes();
void recomputePhoneChatPositions();
void openPhoneChatFromList(std::shared_ptr<UiNode> chatRoot, const std::string& dialogueId);
void returnToPhoneChatList();
void closePhoneScreenFromChat();
GameState state = GameState::Gameplay;
Inventory* inventory = nullptr;
@ -91,7 +95,9 @@ namespace ZL {
std::shared_ptr<UiNode> hudStep5aRoot;
std::shared_ptr<UiNode> hudStep5bRoot;
std::shared_ptr<UiNode> hudStep5abRoot;
std::shared_ptr<UiNode> phoneScreenRoot;
std::shared_ptr<UiNode> phoneChatListRoot;
std::shared_ptr<UiNode> phoneChat1Root;
std::shared_ptr<UiNode> phoneChat2Root;
std::shared_ptr<UiNode> newInventoryRoot;
std::shared_ptr<UiNode> questJournalRoot;
@ -103,6 +109,9 @@ namespace ZL {
int selectedQuestIndex = -1;
std::vector<std::string> visibleQuestIds;
// Dialogues that have been started at least once; re-opening a chat won't restart them
std::unordered_set<std::string> startedDialogues_;
// Phone chat state
struct PhoneChatBubbleInfo {
std::string nodeName;

View File

@ -821,6 +821,7 @@ namespace ZL {
);
buttons.clear();
textButtons.clear();
allInteractives.clear();
sliders.clear();
textViews.clear();
textFields.clear();
@ -1066,9 +1067,11 @@ namespace ZL {
void UiManager::collectButtonsAndSliders(const std::shared_ptr<UiNode>& node) {
if (node->button) {
buttons.push_back(node->button);
allInteractives.push_back(node->button);
}
if (node->textButton) {
textButtons.push_back(node->textButton);
allInteractives.push_back(node->textButton);
}
if (node->slider) {
sliders.push_back(node->slider);
@ -1191,6 +1194,7 @@ namespace ZL {
prev.root = root;
prev.buttons = buttons;
prev.textButtons = textButtons;
prev.allInteractives = allInteractives;
prev.sliders = sliders;
prev.textViews = textViews;
prev.textFields = textFields;
@ -1255,6 +1259,7 @@ namespace ZL {
root = s.root;
buttons = s.buttons;
textButtons = s.textButtons;
allInteractives = s.allInteractives;
sliders = s.sliders;
textViews = s.textViews;
textFields = s.textFields;
@ -1646,22 +1651,22 @@ namespace ZL {
void UiManager::onTouchDown(int64_t fingerId, int x, int y) {
for (auto& b : buttons) {
if (b->state != ButtonState::Disabled)
{
if (b->getClickZoneRect().containsConsideringBorder((float)x, (float)y, b->border)) {
// Iterate allInteractives in reverse DFS order: later-declared elements have higher priority.
// At most one button or textButton is pressed per touch.
for (auto it = allInteractives.rbegin(); it != allInteractives.rend(); ++it) {
if (std::holds_alternative<std::shared_ptr<UiButton>>(*it)) {
auto& b = std::get<std::shared_ptr<UiButton>>(*it);
if (b->state != ButtonState::Disabled &&
b->getClickZoneRect().containsConsideringBorder((float)x, (float)y, b->border)) {
b->state = ButtonState::Pressed;
pressedButtons[fingerId] = b;
if (b->onPress) b->onPress(b->name);
break; // a single finger can only press one button
break;
}
}
}
for (auto& tb : textButtons) {
if (tb->state != ButtonState::Disabled)
{
if (tb->getClickZoneRect().containsConsideringBorder((float)x, (float)y, tb->border)) {
} else {
auto& tb = std::get<std::shared_ptr<UiTextButton>>(*it);
if (tb->state != ButtonState::Disabled &&
tb->getClickZoneRect().containsConsideringBorder((float)x, (float)y, tb->border)) {
tb->state = ButtonState::Pressed;
pressedTextButtons[fingerId] = tb;
if (tb->onPress) tb->onPress(tb->name);

View File

@ -10,6 +10,7 @@
#include <memory>
#include <functional>
#include <map>
#include <variant>
#include <cstdint>
namespace ZL {
@ -446,9 +447,14 @@ namespace ZL {
bool stepStarted = false;
};
using AnyButton = std::variant<std::shared_ptr<UiButton>, std::shared_ptr<UiTextButton>>;
std::shared_ptr<UiNode> root;
std::vector<std::shared_ptr<UiButton>> buttons;
std::vector<std::shared_ptr<UiTextButton>> textButtons;
// All buttons and textButtons in DFS declaration order.
// onTouchDown iterates this in reverse so later-declared elements have higher priority.
std::vector<AnyButton> allInteractives;
std::vector<std::shared_ptr<UiSlider>> sliders;
std::vector<std::shared_ptr<UiTextView>> textViews;
std::vector<std::shared_ptr<UiTextField>> textFields;
@ -469,6 +475,7 @@ namespace ZL {
std::shared_ptr<UiNode> root;
std::vector<std::shared_ptr<UiButton>> buttons;
std::vector<std::shared_ptr<UiTextButton>> textButtons;
std::vector<AnyButton> allInteractives;
std::vector<std::shared_ptr<UiSlider>> sliders;
std::vector<std::shared_ptr<UiTextView>> textViews;
std::vector<std::shared_ptr<UiTextField>> textFields;