From f714212d2f161d2adee9e9aeb89fded55f651ce5 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Wed, 26 Nov 2025 04:06:05 +0100 Subject: [PATCH] text --- .../Crafter.Graphics-TextElement.cpp | 193 ++++++++++++++++-- interfaces/Crafter.Graphics-TextElement.cppm | 2 + 2 files changed, 177 insertions(+), 18 deletions(-) diff --git a/implementations/Crafter.Graphics-TextElement.cpp b/implementations/Crafter.Graphics-TextElement.cpp index 9bc3c75..11e81d3 100644 --- a/implementations/Crafter.Graphics-TextElement.cpp +++ b/implementations/Crafter.Graphics-TextElement.cpp @@ -49,26 +49,12 @@ void TextElement::RenderText(const std::string_view text, float size, Pixel_BU8_ 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::Clip) { - // For wrapped text, we need to handle line breaks and calculate lines + // We need to handle line breaks and calculate lines std::vector lines; std::string_view remaining = text; - // Simple word wrapping approach while (!remaining.empty()) { // Find next newline or end of string auto newlinePos = remaining.find('\n'); @@ -81,12 +67,11 @@ void TextElement::RenderText(const std::string_view text, float size, Pixel_BU8_ } } - // Now process each line with proper wrapping + // Now process each line std::uint_fast32_t lineHeight = (font.ascent - font.descent) * scale; std::uint_fast32_t maxLineWidth = scaled.width; std::uint_fast32_t currentLineStartY = 0; - // Process lines up to maximum allowed for (std::size_t lineIndex = 0; lineIndex < lines.size(); ++lineIndex) { std::string_view line = lines[lineIndex]; @@ -156,7 +141,179 @@ void TextElement::RenderText(const std::string_view text, float size, Pixel_BU8_ currentLineStartY += lineHeight; } - } else { + } else if (overflowMode == TextOverflowMode::Wrap) { + // Implement word wrapping + std::uint_fast32_t lineHeight = (font.ascent - font.descent) * scale; + std::uint_fast32_t maxLineWidth = scaled.width; + std::uint_fast32_t currentLineStartY = 0; + // Process text character by character to wrap words + std::string remaining(text); + std::string currentLine; + std::string lastWord; + std::string currentWord; + + while (!remaining.empty()) { + // Find next word boundary + std::size_t nextSpace = remaining.find(' '); + std::size_t nextNewline = remaining.find('\n'); + + // Find the earliest delimiter + std::size_t delimiterPos = std::string_view::npos; + if (nextSpace != std::string_view::npos) { + delimiterPos = nextSpace; + } + if (nextNewline != std::string_view::npos && (delimiterPos == std::string_view::npos || nextNewline < delimiterPos)) { + delimiterPos = nextNewline; + } + + // Extract word + if (delimiterPos != std::string_view::npos) { + currentWord = remaining.substr(0, delimiterPos); + remaining = remaining.substr(delimiterPos + 1); + } else { + currentWord = remaining; + remaining = {}; + } + + // Check if adding this word would exceed the line width + std::uint_fast32_t wordWidth = 0; + for (const char c : currentWord) { + int advance, lsb; + stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); + wordWidth += (int)(advance * scale); + } + + // Add kerning for spaces between words + if (!currentLine.empty() && !currentWord.empty()) { + int lastChar = currentLine.back(); + int firstChar = currentWord.front(); + wordWidth += (int)stbtt_GetCodepointKernAdvance(&font.font, lastChar, firstChar); + } + + // If this word alone is wider than the line, force break it + if (wordWidth > maxLineWidth) { + // Force break the word + if (!currentLine.empty()) { + // Render current line + RenderWrappedLine(currentLine, scale, baseline, currentLineStartY, color, font, alignment); + currentLineStartY += lineHeight; + currentLine = {}; + } + + // Break the long word into parts + std::string_view wordPart = currentWord; + while (!wordPart.empty()) { + // Find a good place to break the word + std::size_t breakPos = 0; + std::uint_fast32_t currentWidth = 0; + + for (std::size_t i = 0; i < wordPart.length(); ++i) { + int advance, lsb; + stbtt_GetCodepointHMetrics(&font.font, wordPart[i], &advance, &lsb); + currentWidth += (int)(advance * scale); + + if (currentWidth > maxLineWidth) { + breakPos = i; + break; + } + } + + if (breakPos == 0) { + breakPos = wordPart.length(); + } + + std::string_view part = wordPart.substr(0, breakPos); + // Render the part + RenderWrappedLine(part, scale, baseline, currentLineStartY, color, font, alignment); + currentLineStartY += lineHeight; + wordPart = wordPart.substr(breakPos); + } + } else if (currentLine.empty() || (currentLine.length() + currentWord.length() + 1) * (int)(scale * 1.5f) <= maxLineWidth) { + // Add word to current line + if (!currentLine.empty()) { + currentLine = currentLine.substr(0) + std::string(" ") + currentWord; + } else { + currentLine = currentWord; + } + } else { + // Render current line and start new one + RenderWrappedLine(currentLine, scale, baseline, currentLineStartY, color, font, alignment); + currentLineStartY += lineHeight; + currentLine = currentWord; + } + } + + // Render final line if it exists + if (!currentLine.empty()) { + RenderWrappedLine(currentLine, scale, baseline, currentLineStartY, color, font, alignment); + } + } +} + +// Helper method to render a wrapped line +void TextElement::RenderWrappedLine(const std::string_view line, float scale, int baseline, std::uint_fast32_t startY, Pixel_BU8_GU8_RU8_AU8 color, Font& font, TextAlignment alignment) { + // 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 currentY = startY + baseline; + + 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 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 = currentY + 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]); + } } } \ No newline at end of file diff --git a/interfaces/Crafter.Graphics-TextElement.cppm b/interfaces/Crafter.Graphics-TextElement.cppm index 6808d4c..2962a3d 100644 --- a/interfaces/Crafter.Graphics-TextElement.cppm +++ b/interfaces/Crafter.Graphics-TextElement.cppm @@ -36,6 +36,8 @@ export namespace Crafter { }; class TextElement : public RenderingElementPreScaled { + private: + void RenderWrappedLine(const std::string_view line, float scale, int baseline, std::uint_fast32_t startY, Pixel_BU8_GU8_RU8_AU8 color, Font& font, TextAlignment alignment); 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, TextAlignment alignment = TextAlignment::Left, TextOverflowMode overflowMode = TextOverflowMode::Clip);