/* Crafter®.Graphics Copyright (C) 2025 Catcrafts® Catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; #include "../lib/stb_truetype.h" module Crafter.Graphics:TextElement_impl; import :TextElement; import :RenderingElement; import :Window; import :Types; import :Font; import std; using namespace Crafter; TextElement::TextElement(std::int_fast32_t anchorX, std::int_fast32_t anchorY, std::uint_fast32_t relativeWidth, std::uint_fast32_t relativeHeight, std::int_fast32_t anchorOffsetX, std::int_fast32_t anchorOffsetY, std::int_fast32_t z, bool ignoreScaling) : RenderingElementPreScaled(false, anchorX, anchorY, relativeWidth, relativeHeight, anchorOffsetX, anchorOffsetY, z, ignoreScaling) { } void TextElement::RenderText(const std::string_view text, float size, Pixel_BU8_GU8_RU8_AU8 color, Font& font, TextAlignment alignment, VerticalTextAlignment verticalAlignment, TextOverflowMode overflowMode) { // Calculate the actual size needed for the text float scale = stbtt_ScaleForPixelHeight(&font.font, size); int baseline = (int)(font.ascent * scale); // Clear the scaled buffer for (auto& pixel : bufferScaled) { pixel = {0, 0, 0, 0}; } // If we have no text or no space, return early if (text.empty() || scaled.width == 0 || scaled.height == 0) { return; } // Handle text overflow based on mode if (overflowMode == TextOverflowMode::Clip) { // We need to handle line breaks and calculate lines std::vector lines; std::string_view remaining = text; 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; } } // 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 totalTextHeight = lines.size() * lineHeight; // Calculate vertical offset based on vertical alignment std::int_fast32_t verticalOffset = 0; switch (verticalAlignment) { case VerticalTextAlignment::Top: verticalOffset = 0; break; case VerticalTextAlignment::Middle: verticalOffset = (scaled.height - totalTextHeight) / 2; break; case VerticalTextAlignment::Bottom: verticalOffset = scaled.height - totalTextHeight; break; } std::uint_fast32_t currentLineStartY = 0; for (std::size_t lineIndex = 0; lineIndex < lines.size(); ++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 = verticalOffset + 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 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 < line.size()) { x += (int)stbtt_GetCodepointKernAdvance(&font.font, codepoint, line[i+1]); } } currentLineStartY += lineHeight; } } 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); } // Apply vertical alignment to the entire wrapped text std::uint_fast32_t totalTextHeight = (currentLineStartY + lineHeight); // Total height including last line // Calculate vertical offset based on vertical alignment std::int_fast32_t verticalOffset = 0; switch (verticalAlignment) { case VerticalTextAlignment::Top: verticalOffset = 0; break; case VerticalTextAlignment::Middle: verticalOffset = (scaled.height - totalTextHeight) / 2; break; case VerticalTextAlignment::Bottom: verticalOffset = scaled.height - totalTextHeight; break; } // Shift all rendered content vertically if (verticalOffset != 0) { for (std::uint_fast32_t y = 0; y < scaled.height; ++y) { for (std::uint_fast32_t x = 0; x < scaled.width; ++x) { std::uint_fast32_t index = y * scaled.width + x; if (index < bufferScaled.size()) { // Move the pixel vertically std::uint_fast32_t newIndex = (y + verticalOffset) * scaled.width + x; if (newIndex < bufferScaled.size()) { bufferScaled[newIndex] = bufferScaled[index]; } } } } } } } // 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]); } } }