Working on messenger in portrait mode

This commit is contained in:
Vladislav Khorev 2026-06-26 20:04:08 +03:00
parent 0b4df8ab2d
commit 8c42179576
10 changed files with 498 additions and 65 deletions

View File

@ -0,0 +1,83 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"vertical_align": "center",
"horizontal_align": "center",
"children": [
{
"type": "Button",
"name": "phoneExitButton",
"horizontal_gravity": "center",
"vertical_gravity": "center",
"y": 0,
"width": "match_parent",
"height": "match_parent",
"textures": {
"normal": "resources/transparent.png",
"hover": "resources/transparent.png",
"pressed": "resources/transparent.png"
}
},
{
"type": "Button",
"name": "phoneMain",
"horizontal_gravity": "center",
"vertical_gravity": "center",
"y": -60,
"width": 882,
"height": 1416,
"textures": {
"normal": "resources/w/ui/img/phone/PhoneChat001.png",
"hover": "resources/w/ui/img/phone/PhoneChat001.png",
"pressed": "resources/w/ui/img/phone/PhoneChat001.png"
}
},
{
"type": "FrameLayout",
"name": "chatMessagesContainer",
"horizontal_gravity": "center",
"vertical_gravity": "center",
"width": "match_parent",
"height": 1000
},
{
"type": "StaticImage",
"name": "chatTopCover",
"width": 638,
"height": 101.25,
"x": 0,
"y": 536,
"horizontal_gravity": "center",
"vertical_gravity": "center",
"texture": "resources/w/ui/img/phone/chat_top_cover001.png"
},
{
"type": "TextButton",
"name": "chatTitleButton",
"horizontal_gravity": "center",
"vertical_gravity": "center",
"x": 0.0,
"y": 424,
"width": 638,
"height": 112,
"text": "Айпери",
"textPaddingY": 28.0,
"textPaddingX": 216.0,
"fontSize": 40,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": false,
"topAligned": true,
"wrap": true,
"color": [1.0, 1.0, 1.0, 1.0],
"textures": {
"normal": "resources/w/ui/img/phone/CharHeader001.png",
"hover": "resources/w/ui/img/phone/CharHeader001.png",
"pressed": "resources/w/ui/img/phone/CharHeader001.png"
}
}
]
}
}

View File

@ -0,0 +1,83 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"vertical_align": "center",
"horizontal_align": "center",
"children": [
{
"type": "Button",
"name": "phoneExitButton",
"horizontal_gravity": "center",
"vertical_gravity": "center",
"y": 0,
"width": "match_parent",
"height": "match_parent",
"textures": {
"normal": "resources/transparent.png",
"hover": "resources/transparent.png",
"pressed": "resources/transparent.png"
}
},
{
"type": "Button",
"name": "phoneMain",
"horizontal_gravity": "center",
"vertical_gravity": "center",
"y": -60,
"width": 882,
"height": 1416,
"textures": {
"normal": "resources/w/ui/img/phone/PhoneChat001.png",
"hover": "resources/w/ui/img/phone/PhoneChat001.png",
"pressed": "resources/w/ui/img/phone/PhoneChat001.png"
}
},
{
"type": "FrameLayout",
"name": "chatMessagesContainer",
"horizontal_gravity": "center",
"vertical_gravity": "center",
"width": "match_parent",
"height": 1000
},
{
"type": "StaticImage",
"name": "chatTopCover",
"width": 638,
"height": 101.25,
"x": 0,
"y": 536,
"horizontal_gravity": "center",
"vertical_gravity": "center",
"texture": "resources/w/ui/img/phone/chat_top_cover001.png"
},
{
"type": "TextButton",
"name": "chatTitleButton",
"horizontal_gravity": "center",
"vertical_gravity": "center",
"x": 0.0,
"y": 424,
"width": 638,
"height": 112,
"text": "Атам",
"textPaddingY": 28.0,
"textPaddingX": 216.0,
"fontSize": 40,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": false,
"topAligned": true,
"wrap": true,
"color": [1.0, 1.0, 1.0, 1.0],
"textures": {
"normal": "resources/w/ui/img/phone/CharHeader002.png",
"hover": "resources/w/ui/img/phone/CharHeader002.png",
"pressed": "resources/w/ui/img/phone/CharHeader002.png"
}
}
]
}
}

View File

@ -0,0 +1,83 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"vertical_align": "center",
"horizontal_align": "center",
"children": [
{
"type": "Button",
"name": "phoneExitButton",
"horizontal_gravity": "center",
"vertical_gravity": "center",
"y": 0,
"width": "match_parent",
"height": "match_parent",
"textures": {
"normal": "resources/transparent.png",
"hover": "resources/transparent.png",
"pressed": "resources/transparent.png"
}
},
{
"type": "Button",
"name": "phoneMain",
"horizontal_gravity": "center",
"vertical_gravity": "center",
"y": -60,
"width": 882,
"height": 1416,
"textures": {
"normal": "resources/w/ui/img/phone/PhoneChat001.png",
"hover": "resources/w/ui/img/phone/PhoneChat001.png",
"pressed": "resources/w/ui/img/phone/PhoneChat001.png"
}
},
{
"type": "FrameLayout",
"name": "chatMessagesContainer",
"horizontal_gravity": "center",
"vertical_gravity": "center",
"width": "match_parent",
"height": 1000
},
{
"type": "StaticImage",
"name": "chatTopCover",
"width": 638,
"height": 101.25,
"x": 0,
"y": 536,
"horizontal_gravity": "center",
"vertical_gravity": "center",
"texture": "resources/w/ui/img/phone/chat_top_cover001.png"
},
{
"type": "TextButton",
"name": "chatTitleButton",
"horizontal_gravity": "center",
"vertical_gravity": "center",
"x": 0.0,
"y": 424,
"width": 638,
"height": 112,
"text": "БишкекНовости",
"textPaddingY": 28.0,
"textPaddingX": 216.0,
"fontSize": 40,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": false,
"topAligned": true,
"wrap": true,
"color": [1.0, 1.0, 1.0, 1.0],
"textures": {
"normal": "resources/w/ui/img/phone/CharHeader003.png",
"hover": "resources/w/ui/img/phone/CharHeader003.png",
"pressed": "resources/w/ui/img/phone/CharHeader003.png"
}
}
]
}
}

View File

@ -672,6 +672,7 @@ namespace ZL
if (!menuManager.cutsceneHudActive_)
menuManager.uiManager.draw(renderer);
menuManager.drawChatFbo(renderer);
if (currentLocation())
{
currentLocation()->dialogueSystem.draw(renderer);

View File

@ -7,6 +7,7 @@
#include <algorithm>
#include <string>
namespace ZL {
static int questStatusPriority(Quest::QuestStatus status) {
@ -152,6 +153,9 @@ namespace ZL {
phoneChat1Root = loadUiFromFile("resources/w/ui/screen_phone_chat1.json", renderer, zipFile);
phoneChat2Root = loadUiFromFile("resources/w/ui/screen_phone_chat2.json", renderer, zipFile);
phoneChat3Root = loadUiFromFile("resources/w/ui/screen_phone_chat3.json", renderer, zipFile);
verticalPhoneChat1Root = loadUiFromFile("resources/w/ui/portrait_screen_phone_chat1.json", renderer, zipFile);
verticalPhoneChat2Root = loadUiFromFile("resources/w/ui/portrait_screen_phone_chat2.json", renderer, zipFile);
verticalPhoneChat3Root = loadUiFromFile("resources/w/ui/portrait_screen_phone_chat3.json", renderer, zipFile);
modalMenuRoot = loadUiFromFile("resources/w/ui/screen_modal_menu.json", renderer, zipFile);
newInventoryRoot = loadUiFromFile("resources/w/ui/screen_inventory.json", renderer, zipFile);
@ -657,15 +661,15 @@ namespace ZL {
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
uiManager.setTextButtonCallback("chat1button", [this](const std::string&) {
gameState_.chatUnread[0] = false;
openPhoneChatFromList(0, phoneChat1Root);
openPhoneChatFromList(0, isPortraitMode() ? verticalPhoneChat1Root : phoneChat1Root);
});
uiManager.setTextButtonCallback("chat2button", [this](const std::string&) {
gameState_.chatUnread[1] = false;
openPhoneChatFromList(1, phoneChat2Root);
openPhoneChatFromList(1, isPortraitMode() ? verticalPhoneChat2Root : phoneChat2Root);
});
uiManager.setTextButtonCallback("chat3button", [this](const std::string&) {
gameState_.chatUnread[2] = false;
openPhoneChatFromList(2, phoneChat3Root);
openPhoneChatFromList(2, isPortraitMode() ? verticalPhoneChat3Root : phoneChat3Root);
});
}
@ -784,7 +788,33 @@ namespace ZL {
gameState_.tutorialPhoneChatScreenOpened = true;
uiManager.pushMenuFromSavedRoot(chatRoot);
rebuildChatBubblesFromHistory(chatIndex);
// Build a dedicated chatUiManager root sized to the FBO chat area
{
const bool portrait = isPortraitMode();
const float fboW = portrait ? CHAT_FBO_WIDTH_P : CHAT_FBO_WIDTH_L;
const float fboH = portrait ? (CHAT_FBO_OFFSET_UP_P + CHAT_FBO_OFFSET_DOWN_P)
: (CHAT_FBO_OFFSET_UP_L + CHAT_FBO_OFFSET_DOWN_L);
auto fboRoot = std::make_shared<UiNode>();
fboRoot->name = "chatFboRoot";
fboRoot->width = fboW;
fboRoot->height = fboH;
fboRoot->screenRect = { 0.0f, 0.0f, fboW, fboH };
auto fboContainer = std::make_shared<UiNode>();
fboContainer->name = "chatMessagesContainer";
fboContainer->width = fboW;
fboContainer->height = fboH;
fboContainer->screenRect = { 0.0f, 0.0f, fboW, fboH };
fboRoot->children.push_back(fboContainer);
chatUiManager.replaceRoot(fboRoot);
chatFrameBuffer_ = std::make_unique<FrameBuffer>(
static_cast<int>(fboW), static_cast<int>(fboH), false);
chatFboQuadLastProjW_ = -1.0f;
}
rebuildChatBubblesFromHistory(chatIndex, isPortraitMode());
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
closePhoneScreenFromChat();
@ -808,6 +838,8 @@ namespace ZL {
activeChatIndex_ = -1;
currentPhoneSubScreen_ = PhoneSubScreen::ChatList;
phoneChatVisibleBubbles_.clear();
chatUiManager.replaceRoot(nullptr);
chatFrameBuffer_.reset();
uiManager.popMenu();
uiManager.updateAllLayouts();
if (gameState_.tutorialPhoneChatScreenOpened)
@ -832,6 +864,8 @@ namespace ZL {
currentPhoneSubScreen_ = PhoneSubScreen::None;
uiState_ = GameUiState::Gameplay;
phoneChatVisibleBubbles_.clear();
chatUiManager.replaceRoot(nullptr);
chatFrameBuffer_.reset();
const int depth = uiManager.menuStackSize();
for (int i = 0; i < depth; ++i) uiManager.popMenu();
uiManager.updateAllLayouts();
@ -1261,44 +1295,71 @@ namespace ZL {
if (i > 0) totalHeight += CHAT_SPACING;
}
const float available = CHAT_TOP_Y - CHAT_BOTTOM_Y;
const float topY = (totalHeight <= available)
? CHAT_TOP_Y
: CHAT_BOTTOM_Y + totalHeight;
const float fboH = CHAT_FBO_OFFSET_UP_L + CHAT_FBO_OFFSET_DOWN_L;
const float topY = (totalHeight <= fboH) ? fboH : totalHeight;
float cursor = topY;
for (auto& bubble : phoneChatVisibleBubbles_) {
auto node = uiManager.findNode(bubble.nodeName);
auto node = chatUiManager.findNode(bubble.nodeName);
if (!node) continue;
node->localY = cursor - bubble.height;
cursor -= bubble.height + CHAT_SPACING;
}
uiManager.updateAllLayouts();
chatUiManager.updateAllLayouts();
}
void MenuManager::rebuildChatBubblesFromHistory(int chatIndex) {
uiManager.clearChatBubbles("chatMessagesContainer");
void MenuManager::recomputePhoneChatPositionsPortrait() {
float totalHeight = 0.0f;
for (size_t i = 0; i < phoneChatVisibleBubbles_.size(); ++i) {
totalHeight += phoneChatVisibleBubbles_[i].height;
if (i > 0) totalHeight += CHAT_SPACING;
}
const float fboH = CHAT_FBO_OFFSET_UP_P + CHAT_FBO_OFFSET_DOWN_P;
const float topY = (totalHeight <= fboH) ? fboH : totalHeight;
float cursor = topY;
for (auto& bubble : phoneChatVisibleBubbles_) {
auto node = chatUiManager.findNode(bubble.nodeName);
if (!node) continue;
node->localY = cursor - bubble.height;
cursor -= bubble.height + CHAT_SPACING;
}
chatUiManager.updateAllLayouts();
}
void MenuManager::rebuildChatBubblesFromHistory(int chatIndex, bool isPortrait) {
chatUiManager.clearChatBubbles("chatMessagesContainer");
phoneChatVisibleBubbles_.clear();
if (chatIndex < 0 || chatIndex > 2) return;
for (const auto& msg : gameState_.chatHistory[chatIndex]) {
const bool inc = msg.incoming;
const std::string nodeName = uiManager.addChatBubble(
const std::string nodeName = chatUiManager.addChatBubble(
"chatMessagesContainer", msg.text, inc,
inc ? texBubbleInCenter_ : texBubbleOutCenter_,
inc ? texBubbleInLT_ : texBubbleOutLT_,
inc ? texBubbleInLB_ : texBubbleOutLB_,
inc ? texBubbleInRT_ : texBubbleOutRT_,
inc ? texBubbleInRB_ : texBubbleOutRB_,
renderer, "resources/fonts/DroidSans.ttf", 20, CONST_ZIP_FILE);
renderer, "resources/fonts/DroidSans.ttf", isPortrait ? 32 : 20, isPortrait, CONST_ZIP_FILE);
if (!nodeName.empty()) {
auto n = uiManager.findNode(nodeName);
auto n = chatUiManager.findNode(nodeName);
phoneChatVisibleBubbles_.push_back({ nodeName, n ? n->height : 60.0f });
}
}
if (isPortrait)
{
recomputePhoneChatPositionsPortrait();
}
else
{
recomputePhoneChatPositions();
}
}
void MenuManager::onChatBubbleReady(const std::string& text, bool incoming) {
if (activeChatIndex_ < 0) return;
@ -1310,25 +1371,34 @@ namespace ZL {
if (uiState_ != GameUiState::PhoneScreen) return;
const std::string nodeName = uiManager.addChatBubble(
bool isPortrait = isPortraitMode();
const std::string nodeName = chatUiManager.addChatBubble(
"chatMessagesContainer", text, incoming,
incoming ? texBubbleInCenter_ : texBubbleOutCenter_,
incoming ? texBubbleInLT_ : texBubbleOutLT_,
incoming ? texBubbleInLB_ : texBubbleOutLB_,
incoming ? texBubbleInRT_ : texBubbleOutRT_,
incoming ? texBubbleInRB_ : texBubbleOutRB_,
renderer, "resources/fonts/DroidSans.ttf", 20, CONST_ZIP_FILE);
renderer, "resources/fonts/DroidSans.ttf", isPortrait ? 32 : 20, isPortrait, CONST_ZIP_FILE);
if (nodeName.empty()) return;
auto node = uiManager.findNode(nodeName);
auto node = chatUiManager.findNode(nodeName);
if (node) {
node->scaleX = 0.0f;
node->scaleY = 0.0f;
phoneChatVisibleBubbles_.push_back({ nodeName, node->height });
}
if (isPortrait)
{
recomputePhoneChatPositionsPortrait();
}
else
{
recomputePhoneChatPositions();
uiManager.startPopIn(nodeName, 300.0f);
}
chatUiManager.startPopIn(nodeName, 300.0f);
}
void MenuManager::setDarklandsMode(bool enabled)
@ -1537,6 +1607,57 @@ namespace ZL {
void MenuManager::update(float deltaMs) {
updateToasts(deltaMs);
chatUiManager.update(deltaMs);
}
void MenuManager::drawChatFbo(Renderer& renderer) {
if (!chatFrameBuffer_) return;
const bool portrait = isPortraitMode();
const float fboW = portrait ? CHAT_FBO_WIDTH_P : CHAT_FBO_WIDTH_L;
const float offsetUp = portrait ? CHAT_FBO_OFFSET_UP_P : CHAT_FBO_OFFSET_UP_L;
const float offsetDown= portrait ? CHAT_FBO_OFFSET_DOWN_P : CHAT_FBO_OFFSET_DOWN_L;
const float fboH = offsetUp + offsetDown;
const float projW = Environment::projectionWidth;
const float projH = Environment::projectionHeight;
// Pass 1: render chatUiManager into FBO
chatFrameBuffer_->Bind();
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
chatUiManager.draw(renderer, fboW, fboH);
chatFrameBuffer_->Unbind();
// Pass 2: blit FBO texture onto screen, centered with up/down offsets from screen center
if (chatFboQuadLastProjW_ != projW || chatFboQuadLastPortrait_ != portrait ||
chatFboQuadLastProjH_ != projH) {
const float x0 = (projW - fboW) * 0.5f;
const float y0 = projH * 0.5f - offsetDown;
const float x1 = x0 + fboW;
const float y1 = y0 + fboH;
VertexDataStruct vd;
vd.PositionData = { {x0,y0,0},{x0,y1,0},{x1,y1,0},
{x0,y0,0},{x1,y1,0},{x1,y0,0} };
vd.TexCoordData = { {0,0},{0,1},{1,1},
{0,0},{1,1},{1,0} };
chatFboQuad_.AssignFrom(vd);
chatFboQuadLastProjW_ = projW;
chatFboQuadLastProjH_ = projH;
chatFboQuadLastPortrait_ = portrait;
}
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
renderer.PushProjectionMatrix(projW, projH, -1, 1);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.RenderUniform1f("uAlpha", 1.0f);
glBindTexture(GL_TEXTURE_2D, chatFrameBuffer_->getTextureID());
renderer.DrawVertexRenderStruct(chatFboQuad_);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
}
void MenuManager::saveSettings() {
@ -1609,10 +1730,19 @@ namespace ZL {
currentPhoneSubScreen_ = PhoneSubScreen::MapUni;
break;
case PhoneSubScreen::ChatList:
case PhoneSubScreen::Chat:
openPhoneScreen();
openPhoneMessenger();
break;
case PhoneSubScreen::Chat: {
openPhoneScreen();
openPhoneMessenger();
std::shared_ptr<UiNode> chatRoot;
if (savedChatIndex == 0) chatRoot = isPortraitMode() ? verticalPhoneChat1Root : phoneChat1Root;
else if (savedChatIndex == 1) chatRoot = isPortraitMode() ? verticalPhoneChat2Root : phoneChat2Root;
else if (savedChatIndex == 2) chatRoot = isPortraitMode() ? verticalPhoneChat3Root : phoneChat3Root;
if (chatRoot) openPhoneChatFromList(savedChatIndex, chatRoot);
break;
}
default:
openPhoneScreen();
break;

View File

@ -10,6 +10,7 @@
#include <string>
#include <memory>
#include "AudioPlayerAsync.h"
#include "render/FrameBuffer.h"
// Forward-declared here to avoid pulling Game.h into MenuManager.h.
namespace ZL { struct SaveSlotInfo; }
@ -31,6 +32,7 @@ namespace ZL {
public:
UiManager uiManager;
UiManager topUiManager;
UiManager chatUiManager;
MenuManager(Renderer& iRenderer, GameState& gameState, AudioPlayerAsync& audioPlayer);
@ -96,6 +98,9 @@ namespace ZL {
// Called by Game when the window is resized and orientation has changed.
void onOrientationChanged();
// Called from Game::drawUI() to render chat bubbles through an FBO.
void drawChatFbo(Renderer& renderer);
void saveSettings();
void loadSettings();
@ -125,11 +130,12 @@ namespace ZL {
void refreshChatUnreadIndicators();
void resetPhoneChatNodes();
void recomputePhoneChatPositions();
void recomputePhoneChatPositionsPortrait();
void openPhoneChatFromList(int chatIndex, std::shared_ptr<UiNode> chatRoot);
void returnToPhoneChatList();
void closePhoneScreenFromChat();
void applyUniIntHud();
void rebuildChatBubblesFromHistory(int chatIndex);
void rebuildChatBubblesFromHistory(int chatIndex, bool isPortrait);
void showModalMenuScreen();
void showAboutScreen();
@ -210,6 +216,9 @@ namespace ZL {
std::shared_ptr<UiNode> phoneChat1Root;
std::shared_ptr<UiNode> phoneChat2Root;
std::shared_ptr<UiNode> phoneChat3Root;
std::shared_ptr<UiNode> verticalPhoneChat1Root;
std::shared_ptr<UiNode> verticalPhoneChat2Root;
std::shared_ptr<UiNode> verticalPhoneChat3Root;
std::shared_ptr<UiNode> modalMenuRoot;
@ -253,6 +262,27 @@ namespace ZL {
static constexpr float CHAT_TOP_Y = 610.0f;
static constexpr float CHAT_BOTTOM_Y = 290.0f;
static constexpr float CHAT_SPACING = 10.0f;
static constexpr float P_CHAT_TOP_Y = 610.0f + 246;
static constexpr float P_CHAT_BOTTOM_Y = 290.0f + 126;
// FBO dimensions for chat clipping.
// OFFSET_UP / OFFSET_DOWN = how far the FBO extends above / below screen center.
// Height = UP + DOWN. Quad: y0 = projH/2 - DOWN, y1 = projH/2 + UP.
static constexpr float CHAT_FBO_WIDTH_L = 469.0f;
static constexpr float CHAT_FBO_OFFSET_UP_L = 260.0f;
static constexpr float CHAT_FBO_OFFSET_DOWN_L = 70.0f;
static constexpr float CHAT_FBO_WIDTH_P = 670.0f;
static constexpr float CHAT_FBO_OFFSET_UP_P = 360.0f;
static constexpr float CHAT_FBO_OFFSET_DOWN_P = 70.0f;
std::unique_ptr<FrameBuffer> chatFrameBuffer_;
VertexRenderStruct chatFboQuad_;
float chatFboQuadLastProjW_ = -1.0f;
float chatFboQuadLastProjH_ = -1.0f;
bool chatFboQuadLastPortrait_ = false;
};
} // namespace ZL

View File

@ -854,6 +854,22 @@ namespace ZL {
mesh.RefreshVBO();
}
float UiChatBubble::computeWidthPortrait() const {
if (!textRenderer) return P_MAX_WIDTH;
const float measuredTextW = textRenderer->measureTextWidth(text, 1.0f);
const float required = measuredTextW + P_CORNER_SIZE * 2.0f + P_PAD_X * 2.0f;
return min(required, P_MAX_WIDTH);
}
float UiChatBubble::computeHeightPortrait() const {
if (!textRenderer) return P_CORNER_SIZE * 2 + P_PAD_Y * 2 + static_cast<float>(fontSize);
const float textW = computeWidthPortrait() - P_CORNER_SIZE * 2.0f - P_PAD_X * 2.0f;
const std::string wrapped = wrapTextByPixels(text, *textRenderer, textW, 1.0f);
int lines = 1;
for (char c : wrapped) if (c == '\n') ++lines;
return P_CORNER_SIZE * 2.0f + P_PAD_Y * 2.0f + lines * static_cast<float>(fontSize) * 1.25f;
}
float UiChatBubble::computeWidth() const {
if (!textRenderer) return MAX_WIDTH;
const float measuredTextW = textRenderer->measureTextWidth(text, 1.0f);
@ -892,7 +908,7 @@ namespace ZL {
buildQuadMesh(meshCornerRB, x + w - cs, y, x + w, y + cs);
}
void UiChatBubble::draw(Renderer& renderer, float nodeScaleX, float nodeScaleY) const {
void UiChatBubble::draw(Renderer& renderer, float nodeScaleX, float nodeScaleY, float projW, float projH) const {
renderer.RenderUniform1i(textureUniformName, 0);
@ -936,7 +952,7 @@ namespace ZL {
const float tx = cx + (tx0 - cx) * nodeScaleX;
const float ty = cy + (ty0 - cy) * nodeScaleY;
textRenderer->drawText(wrapped, tx, ty, nodeScaleX, false, { 1.f, 1.f, 1.f, 1.f });
textRenderer->drawText(wrapped, tx, ty, nodeScaleX, false, { 1.f, 1.f, 1.f, 1.f }, projW, projH);
}
}
@ -1456,8 +1472,10 @@ namespace ZL {
return static_cast<int>(menuStack.size());
}
void UiManager::draw(Renderer& renderer) {
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, -1, 1);
void UiManager::draw(Renderer& renderer, float projW, float projH) {
if (projW <= 0.0f) projW = Environment::projectionWidth;
if (projH <= 0.0f) projH = Environment::projectionHeight;
renderer.PushProjectionMatrix(projW, projH, -1, 1);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.RenderUniform1f("uAlpha", 1.0f); // ensure alpha is fully opaque unless a widget overrides it
@ -1481,7 +1499,7 @@ namespace ZL {
// 1b. Chat bubble (composite widget)
if (node->chatBubble) {
node->chatBubble->draw(renderer, node->scaleX, node->scaleY);
node->chatBubble->draw(renderer, node->scaleX, node->scaleY, projW, projH);
}
// 2. Потом кнопки
@ -2136,6 +2154,7 @@ namespace ZL {
std::shared_ptr<Texture> texLT, std::shared_ptr<Texture> texLB,
std::shared_ptr<Texture> texRT, std::shared_ptr<Texture> texRB,
Renderer& renderer, const std::string& fontPath, int fontSize,
bool isPortrait,
const std::string& zipFile)
{
if (!root) return "";
@ -2168,39 +2187,37 @@ namespace ZL {
// Build the UiNode
auto node = std::make_shared<UiNode>();
node->name = nodeName;
node->width = bubble->computeWidth();
node->height = bubble->computeHeight();
node->width = isPortrait? bubble->computeWidthPortrait() : bubble->computeWidth();
node->height = isPortrait? bubble->computeHeightPortrait() : bubble->computeHeight();
if (isPortrait)
{
if (incoming) {
node->localX = -node->width * 0.5+110 - (UiChatBubble::MAX_WIDTH - node->width);// +(425.f - node->width);
node->localX = -300 + node->width * 0.5;
}
else
{
/*if (node->width == UiChatBubble::MAX_WIDTH)
{
node->localX = node->width * 0.5 - 110 - (UiChatBubble::MAX_WIDTH - node->width);// -(425.f - node->width);
node->localX = 300 - node->width * 0.5;
}
}
else
{*/
//node->localX = node->width * 0.5 - 110;
//node->localX = - 110 +x;
{
if (incoming) {
node->localX = -210 + node->width * 0.5;
}
else
{
node->localX = 210 - node->width * 0.5;
//}
}
//430
//1280
}
node->localY = 0.0f;
node->visible = true;
node->chatBubble = bubble;
node->layoutSettings.hGravity = HorizontalGravity::Center;
/*
if (incoming) {
node->layoutSettings.hGravity = HorizontalGravity::Left;
} else {
node->layoutSettings.hGravity = HorizontalGravity::Right;
}*/
// Bottom gravity means localY is used as-is for screenRect.y (no parentH transform).
// The original static chat nodes all had "vertical_gravity": "bottom" for the same reason.
node->layoutSettings.vGravity = VerticalGravity::Bottom;

View File

@ -262,11 +262,14 @@ namespace ZL {
static constexpr float CORNER_SIZE = 22.4f;
static constexpr float MAX_WIDTH = 320.6f;
//static constexpr float PAD_X = 7.0f;
//static constexpr float PAD_Y = 5.0f;
static constexpr float PAD_X = -10.0f;
static constexpr float PAD_Y = -10.0f;
static constexpr float P_CORNER_SIZE = 32;
static constexpr float P_MAX_WIDTH = 456;
static constexpr float P_PAD_X = -14.0f;
static constexpr float P_PAD_Y = -14.0f;
// 7 meshes: middle-center, top-strip, bottom-strip, LT, RT, LB, RB
VertexRenderStruct meshMiddle;
VertexRenderStruct meshTopStrip;
@ -282,10 +285,12 @@ namespace ZL {
// Must be called after textRenderer is initialised
float computeWidth() const; // capped at MAX_WIDTH
float computeHeight() const; // depends on computeWidth()
float computeWidthPortrait() const;
float computeHeightPortrait() const;
void buildMeshes();
// nodeScaleX/Y: the owning UiNode's current scale (for pop-in / animations).
// Text position and glyph size are transformed to match the renderer's scale matrix.
void draw(Renderer& renderer, float nodeScaleX = 1.0f, float nodeScaleY = 1.0f) const;
void draw(Renderer& renderer, float nodeScaleX = 1.0f, float nodeScaleY = 1.0f, float projW = 0.0f, float projH = 0.0f) const;
};
struct UiNode {
@ -366,7 +371,7 @@ namespace ZL {
void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
void appendFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
void draw(Renderer& renderer);
void draw(Renderer& renderer, float projW = 0.0f, float projH = 0.0f);
// Multi-touch methods (used directly for touch events with per-finger IDs)
void onTouchDown(int64_t fingerId, int x, int y);
@ -461,6 +466,7 @@ namespace ZL {
std::shared_ptr<Texture> texLT, std::shared_ptr<Texture> texLB,
std::shared_ptr<Texture> texRT, std::shared_ptr<Texture> texRB,
Renderer& renderer, const std::string& fontPath, int fontSize,
bool isPortrait,
const std::string& zipFile);
// Remove all chat-bubble children from named parent and clear chatBubbles collection.

View File

@ -380,9 +380,11 @@ float TextRenderer::measureTextWidth(const std::string& text, float scale) const
return maxLineWidth;
}
void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color)
void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color, float projW, float projH)
{
if (!r || text.empty() || !atlasTexture) return;
if (projW <= 0.0f) projW = Environment::projectionWidth;
if (projH <= 0.0f) projH = Environment::projectionHeight;
// формируем ключ кеша
std::string key = text + "|" + std::to_string(scale) + "|" + (centered ? "1" : "0");
@ -471,16 +473,14 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
// 4. Рендеринг
r->shaderManager.PushShader(shaderName);
// Матрица проекции — используем виртуальные проекционные размеры,
// чтобы координаты текста были независимы от физического разрешения экрана.
float W = Environment::projectionWidth;
float H = Environment::projectionHeight;
// Матрица проекции — используем переданные проекционные размеры.
// Это позволяет корректно рендерить текст как в основной сцене, так и внутри FBO.
Eigen::Matrix4f proj = Eigen::Matrix4f::Identity();
proj(0, 0) = 2.0f / W;
proj(1, 1) = 2.0f / H;
proj(0, 0) = 2.0f / projW;
proj(1, 1) = 2.0f / projH;
// Сдвигаем проекцию так, чтобы локальные координаты меша (pen-origin=0,0) оказались в (tx,ty)
proj(0, 3) = -1.0f + 2.0f * (tx) / W;
proj(1, 3) = -1.0f + 2.0f * (ty) / H;
proj(0, 3) = -1.0f + 2.0f * (tx) / projW;
proj(1, 3) = -1.0f + 2.0f * (ty) / projH;
//cached.mesh.RefreshVBO();

View File

@ -26,7 +26,7 @@ public:
~TextRenderer();
bool init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename);
void drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color = { 1.f,1.f,1.f,1.f });
void drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color = { 1.f,1.f,1.f,1.f }, float projW = 0.0f, float projH = 0.0f);
float measureTextWidth(const std::string& text, float scale = 1.0f) const;
float getLineHeight(float scale = 1.0f) const { return lineHeight * scale; }