implement dynamic text wrapping based on pixel width

This commit is contained in:
vottozi 2026-04-29 13:30:02 +06:00
parent 0a073243ad
commit dbf0af2cb2
4 changed files with 132 additions and 6 deletions

View File

@ -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 }); 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); // const std::string wrappedBody = wrapText(model.visibleText, 56);
bodyRenderer->drawText(wrappedBody, bodyX, bodyY, 1.0f, false, { 1.0f, 1.0f, 1.0f, 1.0f }); // 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(); lastChoiceRects.clear();
if (model.mode == PresentationMode::Choice) { if (model.mode == PresentationMode::Choice) {
@ -249,11 +255,21 @@ void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel&
? std::array<float, 4>{0.82f, 0.82f, 0.82f, 1.0f} ? std::array<float, 4>{0.82f, 0.82f, 0.82f, 1.0f}
: std::array<float, 4>{ 1.0f, 0.93f, 0.65f, 1.0f }; : std::array<float, 4>{ 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( choiceRenderer->drawText(
wrapText(model.choices[i].text, 52), wrappedChoiceText,
rect.x + 14.0f, rect.x + 14.0f,
rect.y + 9.0f, rect.y + 9.0f,
1.0f, choiceTextScale,
false, false,
isHighlighted ? std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f} : color isHighlighted ? std::array<float, 4>{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 } { 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( cutsceneRenderer->drawText(
wrapText(model.visibleText, 62), wrappedSubtitle,
subtitleRect.x + 24.0f, subtitleRect.x + 24.0f,
subtitleRect.y + 30.0f, subtitleRect.y + 30.0f,
1.0f, subtitleTextScale,
false, false,
{ 1.0f, 1.0f, 1.0f, 1.0f } { 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; 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) { 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; return x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h;
} }

View File

@ -99,6 +99,7 @@ private:
void drawQuad(Renderer& renderer, const TexturedQuad& quad, const std::shared_ptr<Texture>& texture) const; void drawQuad(Renderer& renderer, const TexturedQuad& quad, const std::shared_ptr<Texture>& texture) const;
static std::string wrapText(const std::string& input, size_t maxLineLength); 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 bool rectContains(const UiRect& rect, float x, float y);
static float lerpFloat(float a, float b, float t); static float lerpFloat(float a, float b, float t);

View File

@ -347,6 +347,38 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
return true; 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<float>(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<float, 4> color) void TextRenderer::drawText(const std::string& text, float x, float y, float scale, bool centered, std::array<float, 4> color)
{ {
if (!r || text.empty() || !atlasTexture) return; if (!r || text.empty() || !atlasTexture) return;

View File

@ -29,6 +29,8 @@ public:
bool init(Renderer& renderer, const std::string& ttfPath, int pixelSize, const std::string& zipfilename); 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 measureTextWidth(const std::string& text, float scale = 1.0f) const;
// Clear cached meshes (call on window resize / DPI change) // Clear cached meshes (call on window resize / DPI change)
void ClearCache(); void ClearCache();