From dbf0af2cb2c3226ea1685829f7b6aa2c2dc328bc Mon Sep 17 00:00:00 2001 From: vottozi Date: Wed, 29 Apr 2026 13:30:02 +0600 Subject: [PATCH] implement dynamic text wrapping based on pixel width --- src/dialogue/DialogueOverlay.cpp | 103 +++++++++++++++++++++++++++++-- src/dialogue/DialogueOverlay.h | 1 + src/render/TextRenderer.cpp | 32 ++++++++++ src/render/TextRenderer.h | 2 + 4 files changed, 132 insertions(+), 6 deletions(-) diff --git a/src/dialogue/DialogueOverlay.cpp b/src/dialogue/DialogueOverlay.cpp index a59b691..cb3c415 100644 --- a/src/dialogue/DialogueOverlay.cpp +++ b/src/dialogue/DialogueOverlay.cpp @@ -204,8 +204,14 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel& nameRenderer->drawText(model.speaker, nameX, nameY, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f }); } - const std::string wrappedBody = wrapText(model.visibleText, 56); - bodyRenderer->drawText(wrappedBody, bodyX, bodyY, 1.0f, false, { 1.0f, 1.0f, 1.0f, 1.0f }); + // const std::string wrappedBody = wrapText(model.visibleText, 56); + // bodyRenderer->drawText(wrappedBody, bodyX, bodyY, 1.0f, false, { 1.0f, 1.0f, 1.0f, 1.0f }); + + const float bodyTextScale = 1.0f; + const float bodyMaxWidthPx = textboxRect.w - 48.0f; + + const std::string wrappedBody = wrapTextToWidth(model.visibleText, *bodyRenderer, bodyMaxWidthPx, bodyTextScale); + bodyRenderer->drawText(wrappedBody, bodyX, bodyY, bodyTextScale, false, { 1.0f, 1.0f, 1.0f, 1.0f }); lastChoiceRects.clear(); if (model.mode == PresentationMode::Choice) { @@ -249,11 +255,21 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel& ? std::array{0.82f, 0.82f, 0.82f, 1.0f} : std::array{ 1.0f, 0.93f, 0.65f, 1.0f }; + const float choiceTextScale = 1.0f; + const float choiceMaxWidthPx = rect.w - 28.0f; + + const std::string wrappedChoiceText = wrapTextToWidth( + model.choices[i].text, + *choiceRenderer, + choiceMaxWidthPx, + choiceTextScale + ); + choiceRenderer->drawText( - wrapText(model.choices[i].text, 52), + wrappedChoiceText, rect.x + 14.0f, rect.y + 9.0f, - 1.0f, + choiceTextScale, false, isHighlighted ? std::array{1.0f, 1.0f, 1.0f, 1.0f} : color ); @@ -502,11 +518,21 @@ void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel& { 1.0f, 0.88f, 0.45f, 1.0f } ); } + const float subtitleTextScale = 1.0f; + const float subtitleMaxWidthPx = subtitleRect.w - 48.0f; + + const std::string wrappedSubtitle = wrapTextToWidth( + model.visibleText, + *cutsceneRenderer, + subtitleMaxWidthPx, + subtitleTextScale + ); + cutsceneRenderer->drawText( - wrapText(model.visibleText, 62), + wrappedSubtitle, subtitleRect.x + 24.0f, subtitleRect.y + 30.0f, - 1.0f, + subtitleTextScale, false, { 1.0f, 1.0f, 1.0f, 1.0f } ); @@ -671,6 +697,71 @@ std::string DialogueOverlay::wrapText(const std::string& input, size_t maxLineLe return output; } +std::string DialogueOverlay::wrapTextToWidth(const std::string& input, const TextRenderer& textRenderer, float maxWidthPx, float scale) +{ + if (input.empty() || maxWidthPx <= 1.0f) { + return input; + } + + std::string output; + std::string currentLine; + std::string currentWord; + + auto flushLine = [&]() { + if (!currentLine.empty()) { + if (!output.empty()) { + output.push_back('\n'); + } + output += currentLine; + currentLine.clear(); + } + }; + + auto pushWord = [&](const std::string& word) { + if (word.empty()) { + return; + } + + if (currentLine.empty()) { + currentLine = word; + return; + } + + const std::string candidate = currentLine + " " + word; + if (textRenderer.measureTextWidth(candidate, scale) <= maxWidthPx) { + currentLine = candidate; + } + else { + flushLine(); + currentLine = word; + } + }; + + for (size_t i = 0; i < input.size(); ++i) { + const char ch = input[i]; + + if (ch == '\n') { + pushWord(currentWord); + currentWord.clear(); + flushLine(); + continue; + } + + if (ch == ' ' || ch == '\t' || ch == '\r') { + pushWord(currentWord); + currentWord.clear(); + continue; + } + + currentWord.push_back(ch); + } + + pushWord(currentWord); + flushLine(); + + return output; +} + bool DialogueOverlay::rectContains(const UiRect& rect, float x, float y) { return x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h; } diff --git a/src/dialogue/DialogueOverlay.h b/src/dialogue/DialogueOverlay.h index 9ac3d9c..aae4a0b 100644 --- a/src/dialogue/DialogueOverlay.h +++ b/src/dialogue/DialogueOverlay.h @@ -99,6 +99,7 @@ private: void drawQuad(Renderer& renderer, const TexturedQuad& quad, const std::shared_ptr& texture) const; static std::string wrapText(const std::string& input, size_t maxLineLength); + static std::string wrapTextToWidth(const std::string& input, const TextRenderer& textRenderer, float maxWidthPx, float scale); static bool rectContains(const UiRect& rect, float x, float y); static float lerpFloat(float a, float b, float t); diff --git a/src/render/TextRenderer.cpp b/src/render/TextRenderer.cpp index ad1e915..530b8ca 100644 --- a/src/render/TextRenderer.cpp +++ b/src/render/TextRenderer.cpp @@ -347,6 +347,38 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s return true; } +float TextRenderer::measureTextWidth(const std::string& text, float scale) const +{ + if (text.empty()) { + return 0.0f; + } + + float penX = 0.0f; + float maxLineWidth = 0.0f; + + size_t textPos = 0; + while (textPos < text.size()) { + uint32_t cp = nextUtf8Codepoint(text, textPos); + + if (cp == '\n') { + maxLineWidth = max(maxLineWidth, penX); + penX = 0.0f; + continue; + } + + auto it = glyphs.find(cp); + if (it == glyphs.end()) { + continue; + } + + const GlyphInfo& g = it->second; + penX += static_cast(g.advance >> 6) * scale; + } + + maxLineWidth = max(maxLineWidth, penX); + return maxLineWidth; +} + void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array color) { if (!r || text.empty() || !atlasTexture) return; diff --git a/src/render/TextRenderer.h b/src/render/TextRenderer.h index ae77229..f70f632 100644 --- a/src/render/TextRenderer.h +++ b/src/render/TextRenderer.h @@ -29,6 +29,8 @@ public: 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 color = { 1.f,1.f,1.f,1.f }); + float measureTextWidth(const std::string& text, float scale = 1.0f) const; + // Clear cached meshes (call on window resize / DPI change) void ClearCache();