From 10308bd80570c1de74f15bbfb36df52c3ca9e8af Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Wed, 26 Nov 2025 03:41:00 +0100 Subject: [PATCH] text wrap --- .../Crafter.Graphics-TextElement.cpp | 261 +++++++++++++++--- interfaces/Crafter.Graphics-TextElement.cppm | 13 +- 2 files changed, 235 insertions(+), 39 deletions(-) diff --git a/implementations/Crafter.Graphics-TextElement.cpp b/implementations/Crafter.Graphics-TextElement.cpp index b4f8125..e07ca30 100644 --- a/implementations/Crafter.Graphics-TextElement.cpp +++ b/implementations/Crafter.Graphics-TextElement.cpp @@ -34,64 +34,249 @@ TextElement::TextElement(std::int_fast32_t anchorX, std::int_fast32_t anchorY, s } -void TextElement::RenderText(const std::string_view text, float size, Pixel_BU8_GU8_RU8_AU8 color, Font& font) { +void TextElement::RenderText(const std::string_view text, float size, Pixel_BU8_GU8_RU8_AU8 color, Font& font, TextAlignment alignment, TextOverflowMode overflowMode) { // Calculate the actual size needed for the text float scale = stbtt_ScaleForPixelHeight(&font.font, size); int baseline = (int)(font.ascent * scale); - std::uint_fast32_t textWidth = 0; - for (const char c : text) { - int advance, lsb; - stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); - textWidth += (int)(advance * scale); - } - // Clear the scaled buffer for (auto& pixel : bufferScaled) { pixel = {0, 0, 0, 0}; } - // Only render text if we have space - if (textWidth <= scaled.width && (font.ascent - font.descent) * scale <= scaled.height) { - // Calculate starting position to center text horizontally and vertically - int startX = (scaled.width - textWidth) / 2; - int startY = ( scaled.height - (font.ascent - font.descent) * scale) / 2; - startY += baseline; // Adjust for baseline + // If we have no text or no space, return early + if (text.empty() || scaled.width == 0 || scaled.height == 0) { + return; + } + + // Calculate total text width + std::uint_fast32_t totalTextWidth = 0; + std::vector charAdvances; + std::vector charLSBs; + + for (const char c : text) { + int advance, lsb; + stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); + charAdvances.push_back(advance); + charLSBs.push_back(lsb); + totalTextWidth += (int)(advance * scale); + } + + // Handle text overflow based on mode + if (overflowMode == TextOverflowMode::Wrap) { + // For wrapped text, we need to handle line breaks and calculate lines + std::vector lines; + std::string_view remaining = text; - int x = startX; - for (std::uint_fast32_t i = 0; i < text.size(); i++) { - int codepoint = text[i]; + // Simple word wrapping approach + while (!remaining.empty()) { + // Find next newline or end of string + auto newlinePos = remaining.find('\n'); + if (newlinePos != std::string_view::npos) { + lines.emplace_back(remaining.substr(0, newlinePos)); + remaining = remaining.substr(newlinePos + 1); + } else { + lines.emplace_back(remaining); + break; + } + } - int ax; - int lsb; - stbtt_GetCodepointHMetrics(&font.font, codepoint, &ax, &lsb); + // Now process each line with proper wrapping + std::uint_fast32_t lineHeight = (font.ascent - font.descent) * scale; + std::uint_fast32_t maxLineWidth = scaled.width; + std::uint_fast32_t currentLineStartY = 0; + std::uint_fast32_t maxLines = scaled.height / lineHeight; + + // Process lines up to maximum allowed + for (std::size_t lineIndex = 0; lineIndex < std::min(lines.size(), maxLines); ++lineIndex) { + std::string_view line = lines[lineIndex]; + + // Calculate line width + std::uint_fast32_t lineWidth = 0; + std::vector lineAdvances; + for (const char c : line) { + int advance, lsb; + stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); + lineAdvances.push_back(advance); + lineWidth += (int)(advance * scale); + } + + // Determine horizontal position based on alignment + int startX = 0; + switch (alignment) { + case TextAlignment::Left: + startX = 0; + break; + case TextAlignment::Center: + startX = (scaled.width - lineWidth) / 2; + break; + case TextAlignment::Right: + startX = scaled.width - lineWidth; + break; + } + + // Render the line + int x = startX; + int startY = currentLineStartY + baseline + (lineIndex * lineHeight); + + for (std::size_t i = 0; i < line.size(); ++i) { + int codepoint = line[i]; + + int ax; + int lsb; + stbtt_GetCodepointHMetrics(&font.font, codepoint, &ax, &lsb); - int c_x1, c_y1, c_x2, c_y2; - stbtt_GetCodepointBitmapBox(&font.font, codepoint, scale, scale, &c_x1, &c_y1, &c_x2, &c_y2); + int c_x1, c_y1, c_x2, c_y2; + stbtt_GetCodepointBitmapBox(&font.font, codepoint, scale, scale, &c_x1, &c_y1, &c_x2, &c_y2); - int w = c_x2 - c_x1; - int h = c_y2 - c_y1; + int w = c_x2 - c_x1; + int h = c_y2 - c_y1; - std::vector bitmap(w * h); - stbtt_MakeCodepointBitmap(&font.font, bitmap.data(), w, h, w, scale, scale, codepoint); + std::vector bitmap(w * h); + stbtt_MakeCodepointBitmap(&font.font, bitmap.data(), w, h, w, scale, scale, codepoint); - // Only render characters that fit within the scaled bounds - for (int j = 0; j < h; j++) { - for (int i = 0; i < w; i++) { - int bufferX = x + i + c_x1; - int bufferY = startY + j + c_y1; - - // Only draw pixels that are within our scaled buffer bounds - if (bufferX >= 0 && bufferX < (int)scaled.width && bufferY >= 0 && bufferY < (int) scaled.height) { - bufferScaled[bufferY * scaled.width + bufferX] = {color.r, color.g, color.b, bitmap[j * w + i]}; + // Only render characters that fit within the scaled bounds + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i++) { + int bufferX = x + i + c_x1; + int bufferY = startY + j + c_y1; + + // Only draw pixels that are within our scaled buffer bounds + if (bufferX >= 0 && bufferX < (int)scaled.width && bufferY >= 0 && bufferY < (int)scaled.height) { + bufferScaled[bufferY * scaled.width + bufferX] = {color.r, color.g, color.b, bitmap[j * w + i]}; + } } } + + x += (int)(ax * scale); + + if (i + 1 < line.size()) { + x += (int)stbtt_GetCodepointKernAdvance(&font.font, codepoint, line[i+1]); + } } + + currentLineStartY += lineHeight; + } + } else { + // Clip mode - render text that fits within bounds + // If the text fits entirely, render it normally + if (totalTextWidth <= scaled.width && (font.ascent - font.descent) * scale <= scaled.height) { + // Calculate starting position based on alignment + int startX = 0; + switch (alignment) { + case TextAlignment::Left: + startX = 0; + break; + case TextAlignment::Center: + startX = (scaled.width - totalTextWidth) / 2; + break; + case TextAlignment::Right: + startX = scaled.width - totalTextWidth; + break; + } + + int startY = (scaled.height - (font.ascent - font.descent) * scale) / 2; + startY += baseline; // Adjust for baseline + + int x = startX; + for (std::uint_fast32_t i = 0; i < text.size(); i++) { + int codepoint = text[i]; - x += (int)(ax * scale); + int ax; + int lsb; + stbtt_GetCodepointHMetrics(&font.font, codepoint, &ax, &lsb); - if (i + 1 < text.size()) { - x += (int)stbtt_GetCodepointKernAdvance(&font.font, codepoint, text[i+1] * scale); + int c_x1, c_y1, c_x2, c_y2; + stbtt_GetCodepointBitmapBox(&font.font, codepoint, scale, scale, &c_x1, &c_y1, &c_x2, &c_y2); + + int w = c_x2 - c_x1; + int h = c_y2 - c_y1; + + std::vector bitmap(w * h); + stbtt_MakeCodepointBitmap(&font.font, bitmap.data(), w, h, w, scale, scale, codepoint); + + // Only render characters that fit within the scaled bounds + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i++) { + int bufferX = x + i + c_x1; + int bufferY = startY + j + c_y1; + + // Only draw pixels that are within our scaled buffer bounds + if (bufferX >= 0 && bufferX < (int)scaled.width && bufferY >= 0 && bufferY < (int)scaled.height) { + bufferScaled[bufferY * scaled.width + bufferX] = {color.r, color.g, color.b, bitmap[j * w + i]}; + } + } + } + + x += (int)(ax * scale); + + if (i + 1 < text.size()) { + x += (int)stbtt_GetCodepointKernAdvance(&font.font, codepoint, text[i+1] * scale); + } + } + } else { + // Text doesn't fit, but we still render what we can + // For now, we'll just render the first part that fits + int startX = 0; + switch (alignment) { + case TextAlignment::Left: + startX = 0; + break; + case TextAlignment::Center: + startX = (scaled.width - totalTextWidth) / 2; + break; + case TextAlignment::Right: + startX = scaled.width - totalTextWidth; + break; + } + + // Clamp to visible area + int startY = (scaled.height - (font.ascent - font.descent) * scale) / 2; + startY += baseline; // Adjust for baseline + + int x = startX; + int renderedChars = 0; + for (std::uint_fast32_t i = 0; i < text.size(); i++) { + int codepoint = text[i]; + + int ax; + int lsb; + stbtt_GetCodepointHMetrics(&font.font, codepoint, &ax, &lsb); + + int c_x1, c_y1, c_x2, c_y2; + stbtt_GetCodepointBitmapBox(&font.font, codepoint, scale, scale, &c_x1, &c_y1, &c_x2, &c_y2); + + int w = c_x2 - c_x1; + int h = c_y2 - c_y1; + + std::vector bitmap(w * h); + stbtt_MakeCodepointBitmap(&font.font, bitmap.data(), w, h, w, scale, scale, codepoint); + + // Only render characters that fit within the scaled bounds + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i++) { + int bufferX = x + i + c_x1; + int bufferY = startY + j + c_y1; + + // Only draw pixels that are within our scaled buffer bounds + if (bufferX >= 0 && bufferX < (int)scaled.width && bufferY >= 0 && bufferY < (int)scaled.height) { + bufferScaled[bufferY * scaled.width + bufferX] = {color.r, color.g, color.b, bitmap[j * w + i]}; + } + } + } + + x += (int)(ax * scale); + + if (x > (int)scaled.width) { + // Text would overflow, stop rendering + break; + } + + if (i + 1 < text.size()) { + x += (int)stbtt_GetCodepointKernAdvance(&font.font, codepoint, text[i+1] * scale); + } + + renderedChars++; } } } diff --git a/interfaces/Crafter.Graphics-TextElement.cppm b/interfaces/Crafter.Graphics-TextElement.cppm index c94f9a9..6808d4c 100644 --- a/interfaces/Crafter.Graphics-TextElement.cppm +++ b/interfaces/Crafter.Graphics-TextElement.cppm @@ -24,9 +24,20 @@ import :Types; import :Font; export namespace Crafter { + enum class TextAlignment { + Left, + Center, + Right + }; + + enum class TextOverflowMode { + Clip, // Clip text that overflows + Wrap // Wrap text to multiple lines + }; + class TextElement : public RenderingElementPreScaled { public: TextElement(std::int_fast32_t anchorX = FractionalToMapped(0.5), std::int_fast32_t anchorY = FractionalToMapped(0.5), std::uint_fast32_t relativeWidth = FractionalToMapped(1), std::uint_fast32_t relativeHeight = FractionalToMapped(1), std::int_fast32_t anchorOffsetX = FractionalToMapped(0.5), std::int_fast32_t anchorOffsetY = FractionalToMapped(0.5), std::int_fast32_t z = 0, bool ignoreScaling = false); - void RenderText(const std::string_view text, float size, Pixel_BU8_GU8_RU8_AU8 pixel, Font& font); + void RenderText(const std::string_view text, float size, Pixel_BU8_GU8_RU8_AU8 pixel, Font& font, TextAlignment alignment = TextAlignment::Left, TextOverflowMode overflowMode = TextOverflowMode::Clip); }; } \ No newline at end of file