From a0550daf4ba4cb8a8d8f2d6c88d61292abd4975f Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sat, 30 May 2026 22:27:09 +0300 Subject: [PATCH] Working on UI --- UI.md | 30 ++++++++++++ resources/w/ui/screen_phone_chat_list.json | 2 +- src/MenuManager.cpp | 55 ++++++++++++++++++---- src/MenuManager.h | 11 ++++- src/UiManager.cpp | 29 +++++++----- src/UiManager.h | 7 +++ 6 files changed, 111 insertions(+), 23 deletions(-) diff --git a/UI.md b/UI.md index 0583530..04ec28a 100644 --- a/UI.md +++ b/UI.md @@ -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. diff --git a/resources/w/ui/screen_phone_chat_list.json b/resources/w/ui/screen_phone_chat_list.json index 783de72..c219af1 100644 --- a/resources/w/ui/screen_phone_chat_list.json +++ b/resources/w/ui/screen_phone_chat_list.json @@ -200,7 +200,7 @@ }, { "type": "TextView", - "name": "chat2msg", + "name": "chat3msg", "x": 100.0, "y": 36.0, "width": 446.25, diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index 94a98d6..70a06f2 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -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&) {}); + 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 + }); + } - // Reset chat state and start the dialogue + void MenuManager::openPhoneChatFromList(std::shared_ptr chatRoot, const std::string& dialogueId) { phoneChatVisibleBubbles_.clear(); - resetPhoneChatNodes(); - if (startDialogueFunc) { - startDialogueFunc("dialog_chat_aiperi001"); + 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() { diff --git a/src/MenuManager.h b/src/MenuManager.h index 6b8ac01..019c200 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -8,6 +8,7 @@ #include #include #include +#include namespace ZL { @@ -72,6 +73,9 @@ namespace ZL { void setupStep5Callbacks(); void resetPhoneChatNodes(); void recomputePhoneChatPositions(); + void openPhoneChatFromList(std::shared_ptr 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 hudStep5aRoot; std::shared_ptr hudStep5bRoot; std::shared_ptr hudStep5abRoot; - std::shared_ptr phoneScreenRoot; + std::shared_ptr phoneChatListRoot; + std::shared_ptr phoneChat1Root; + std::shared_ptr phoneChat2Root; std::shared_ptr newInventoryRoot; std::shared_ptr questJournalRoot; @@ -103,6 +109,9 @@ namespace ZL { int selectedQuestIndex = -1; std::vector visibleQuestIds; + // Dialogues that have been started at least once; re-opening a chat won't restart them + std::unordered_set startedDialogues_; + // Phone chat state struct PhoneChatBubbleInfo { std::string nodeName; diff --git a/src/UiManager.cpp b/src/UiManager.cpp index 6e9309c..679b577 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -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& 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>(*it)) { + auto& b = std::get>(*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>(*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); diff --git a/src/UiManager.h b/src/UiManager.h index a505d94..cc14b33 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace ZL { @@ -446,9 +447,14 @@ namespace ZL { bool stepStarted = false; }; + using AnyButton = std::variant, std::shared_ptr>; + std::shared_ptr root; std::vector> buttons; std::vector> textButtons; + // All buttons and textButtons in DFS declaration order. + // onTouchDown iterates this in reverse so later-declared elements have higher priority. + std::vector allInteractives; std::vector> sliders; std::vector> textViews; std::vector> textFields; @@ -469,6 +475,7 @@ namespace ZL { std::shared_ptr root; std::vector> buttons; std::vector> textButtons; + std::vector allInteractives; std::vector> sliders; std::vector> textViews; std::vector> textFields;