/* Crafter®.Graphics Copyright (C) 2026 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 version 3.0 as published by the Free Software Foundation; 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" export module Crafter.Graphics:RenderingElement2D; import std; import :Transform2D; import :Font; import :Types; import :Window; export namespace Crafter { enum class TextAlignment { Left, Center, Right }; enum class TextOverflowMode { Clip, Wrap }; enum class TextScaleMode { None, Font, Element, Buffer }; struct RenderElement2DScalingOwning { std::vector> scalingBuffer; std::uint32_t bufferWidth; std::uint32_t bufferHeight; bool bufferUpdated = true; RenderElement2DScalingOwning() = default; RenderElement2DScalingOwning(std::uint32_t bufferWidth, std::uint32_t bufferHeight) : scalingBuffer(bufferWidth*bufferHeight), bufferWidth(bufferWidth), bufferHeight(bufferHeight) { } }; struct RenderElement2DScalingNonOwning { Vector* scalingBuffer; std::uint32_t bufferWidth; std::uint32_t bufferHeight; bool bufferUpdated = true; RenderElement2DScalingNonOwning() = default; RenderElement2DScalingNonOwning(Vector* scalingBuffer, std::uint32_t bufferWidth, std::uint32_t bufferHeight) : scalingBuffer(scalingBuffer), bufferWidth(bufferWidth), bufferHeight(bufferHeight) { } }; struct RenderElement2DRotating { float rotation; bool rotationUpdated = true; RenderElement2DRotating() = default; RenderElement2DRotating(float rotation) : rotation(rotation) { } }; struct EmptyScalingBase {}; struct EmptyRotatingBase {}; template using ScalingBase = std::conditional_t< Scaling, std::conditional_t, EmptyScalingBase >; template using RotatingBase = std::conditional_t< Rotating, RenderElement2DRotating, EmptyRotatingBase >; struct RenderingElement2DBase : Transform2D { std::vector> buffer; OpaqueType opaque; RenderingElement2DBase(Anchor2D anchor) : Transform2D(anchor) { scaled.size.x = 0; } RenderingElement2DBase(Anchor2D anchor, OpaqueType opaque) : Transform2D(anchor), opaque(opaque) { scaled.size.x = 0; } }; template requires ((!Rotating || Scaling) && (!Owning || Scaling)) struct RenderingElement2D : RenderingElement2DBase, ScalingBase, RotatingBase { RenderingElement2D() = default; RenderingElement2D(Anchor2D anchor, OpaqueType opaque) : RenderingElement2DBase(anchor, opaque) { } RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t rotation) requires(Rotating) : RenderingElement2DBase(anchor, opaque), RotatingBase(rotation) { } RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, Vector* scalingBuffer) requires(Scaling && !Owning) : RenderingElement2DBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight, scalingBuffer) { } RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, Vector* scalingBuffer, std::uint32_t rotation) requires(Scaling && !Owning && Rotating) : RenderingElement2DBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight, scalingBuffer), RotatingBase(rotation) { } RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight) requires(Owning) : RenderingElement2DBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight) { } RenderingElement2D(Anchor2D anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, std::uint32_t rotation) requires(Owning && Rotating) : RenderingElement2DBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight) , RotatingBase(rotation) { } RenderingElement2D(RenderingElement2D&) = delete; RenderingElement2D& operator=(RenderingElement2D&) = delete; void ScaleNearestNeighbor() requires(Scaling) { for (std::uint32_t y = 0; y < scaled.size.y; y++) { std::uint32_t srcY = y * ScalingBase::bufferHeight / scaled.size.y; for (std::uint32_t x = 0; x < scaled.size.x; x++) { std::uint32_t srcX = x * ScalingBase::bufferWidth / scaled.size.x; buffer[y * scaled.size.x + x] = ScalingBase::scalingBuffer[srcY * ScalingBase::bufferWidth + srcX]; } } } void ScaleRotating() requires(Scaling) { const std::uint32_t dstWidth = scaled.size.x; const std::uint32_t dstHeight = scaled.size.y; const float c2 = std::abs(std::cos(RotatingBase::rotation)); const float s2 = std::abs(std::sin(RotatingBase::rotation)); const float rotatedWidth = dstWidth * c2 + dstHeight * s2; const float rotatedHeight = dstWidth * s2 + dstHeight * c2; const std::uint32_t diffX = static_cast(std::ceil((rotatedWidth - dstWidth) * 0.5)); const std::uint32_t diffY = static_cast(std::ceil((rotatedHeight - dstHeight) * 0.5)); scaled.size.x += diffX + diffX; scaled.size.y += diffY + diffY; scaled.position.x -= diffX; scaled.position.y -= diffY; buffer.clear(); buffer.resize(scaled.size.x * scaled.size.y); // Destination center const float dstCx = (static_cast(scaled.size.x) - 1.0) * 0.5; const float dstCy = (static_cast(scaled.size.y) - 1.0) * 0.5; // Source center const float srcCx = (static_cast(ScalingBase::bufferWidth) - 1.0) * 0.5; const float srcCy = (static_cast(ScalingBase::bufferHeight) - 1.0) * 0.5; const float c = std::cos(RotatingBase::rotation); const float s = std::sin(RotatingBase::rotation); // Scale factors (destination → source) const float scaleX = static_cast(ScalingBase::bufferWidth) / dstWidth; const float scaleY = static_cast(ScalingBase::bufferHeight) / dstHeight; for (std::uint32_t yB = 0; yB < scaled.size.y; ++yB) { for (std::uint32_t xB = 0; xB < scaled.size.x; ++xB) { // ---- Destination pixel relative to center ---- const float dx = (static_cast(xB) - dstCx) * scaleX; const float dy = (static_cast(yB) - dstCy) * scaleY; // ---- Inverse rotation ---- const float sx = (c * dx - s * dy) + srcCx; const float sy = (s * dx + c * dy) + srcCy; // ---- Nearest neighbour sampling ---- const std::int32_t srcX = static_cast(std::round(sx)); const std::int32_t srcY = static_cast(std::round(sy)); if (srcX >= 0 && srcX < ScalingBase::bufferWidth && srcY >= 0 && srcY < ScalingBase::bufferHeight) { buffer[yB * scaled.size.x + xB] = ScalingBase::scalingBuffer[srcY * ScalingBase::bufferWidth + srcX]; } } } } void UpdatePosition(Rendertarget& renderer, ScaleData2D oldScale) { if constexpr(Scaling && !Rotating) { if(oldScale.size.x != scaled.size.x || oldScale.size.y != scaled.size.y) { buffer.resize(scaled.size.x * scaled.size.y); ScaleNearestNeighbor(); renderer.AddDirtyRect(oldScale); renderer.AddDirtyRect(scaled); } else if(oldScale.position.x != scaled.position.x || oldScale.position.y != scaled.position.y) { renderer.AddDirtyRect(oldScale); renderer.AddDirtyRect(scaled); if(ScalingBase::bufferUpdated) { ScaleNearestNeighbor(); ScalingBase::bufferUpdated = false; } } else if(ScalingBase::bufferUpdated) { ScaleNearestNeighbor(); ScalingBase::bufferUpdated = false; } } else if constexpr(Rotating) { if(oldScale.size.x != scaled.size.x || oldScale.size.y != scaled.size.y) { buffer.resize(scaled.size.x * scaled.size.y); ScaleRotating(); renderer.AddDirtyRect(oldScale); renderer.AddDirtyRect(scaled); } else if(oldScale.position.x != scaled.position.x || oldScale.position.y != scaled.position.y) { renderer.AddDirtyRect(oldScale); renderer.AddDirtyRect(scaled); if(ScalingBase::bufferUpdated || RotatingBase::rotationUpdated) { ScaleRotating(); ScalingBase::bufferUpdated = false; RotatingBase::rotationUpdated = false; } } else if(ScalingBase::bufferUpdated || RotatingBase::rotationUpdated) { ScaleRotating(); ScalingBase::bufferUpdated = false; RotatingBase::rotationUpdated = false; } } else { if(oldScale.size.x != scaled.size.x || oldScale.size.y != scaled.size.y) { buffer.resize(scaled.size.x * scaled.size.y); renderer.AddDirtyRect(oldScale); renderer.AddDirtyRect(scaled); } if(oldScale.position.x != scaled.position.x || oldScale.position.y != scaled.position.y) { renderer.AddDirtyRect(oldScale); renderer.AddDirtyRect(scaled); } } } void UpdatePosition(Rendertarget& window) override { ScaleData2D oldScale = scaled; ScaleElement(window.transform); UpdatePosition(window, oldScale); for(Transform2D* child : children) { child->UpdatePosition(window, *this); } } void UpdatePosition(Rendertarget& window, Transform2D& parent) override { ScaleData2D oldScale = scaled; ScaleElement(parent); UpdatePosition(window, oldScale); for(Transform2D* child : children) { child->UpdatePosition(window, *this); } } std::vector ResizeText(Window& window, const std::string_view text, float size, Font& font, TextOverflowMode overflowMode = TextOverflowMode::Clip, TextScaleMode scaleMode = TextScaleMode::None, Transform2D* parent = nullptr) { float scale = stbtt_ScaleForPixelHeight(&font.font, size); int baseline = (int)(font.ascent * scale); std::vector lines; std::string_view remaining = text; std::uint32_t lineHeight = (font.ascent - font.descent) * scale; if(overflowMode == TextOverflowMode::Clip) { 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; } } std::uint32_t maxWidth = 0; for(const std::string_view line: lines) { std::uint32_t lineWidth = 0; for (const char c : line) { int advance, lsb; stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); lineWidth += (int)(advance * scale); } if(lineWidth > maxWidth) { maxWidth = lineWidth; } } if(scaleMode == TextScaleMode::Element) { std::int32_t logicalPerPixelY = anchor.height / scaled.size.y; std::int32_t oldHeight = anchor.height; std::int32_t logicalPerPixelX = anchor.width / scaled.size.x; std::int32_t oldwidth = anchor.width; anchor.height = lineHeight * logicalPerPixelY; anchor.width = maxWidth * logicalPerPixelX; if(oldHeight != anchor.height || oldwidth != anchor.width) { if(parent) { UpdatePosition(window, *parent); } else { UpdatePosition(window); } } } else if(scaleMode == TextScaleMode::Font) { //todo } else if(scaleMode == TextScaleMode::Buffer) { if constexpr(Scaling && Owning) { std::uint32_t neededHeight = lines.size() * lineHeight; if(neededHeight != ScalingBase::bufferHeight || maxWidth != ScalingBase::bufferWidth) { ScalingBase::bufferHeight = neededHeight; ScalingBase::bufferWidth = maxWidth; ScalingBase::bufferUpdated = true; ScalingBase::scalingBuffer.resize(neededHeight*maxWidth); } } } else { if constexpr(Scaling) { lines.resize(ScalingBase::bufferHeight / lines.size()); } else { lines.resize(scaled.size.y / lines.size()); } } } else { while (!remaining.empty()) { std::string_view line; auto newlinePos = remaining.find('\n'); if (newlinePos != std::string_view::npos) { line = remaining.substr(0, newlinePos); remaining = remaining.substr(newlinePos + 1); } else { line = remaining; remaining = ""; } std::uint32_t lineWidth = 0; std::size_t lastWrapPos = 0; // position of last space that can be used to wrap std::size_t startPos = 0; for (std::size_t i = 0; i < line.size(); ++i) { char c = line[i]; // get width of this character int advance, lsb; stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); lineWidth += (std::uint32_t)(advance * scale); // remember last space for wrapping if (c == ' ') { lastWrapPos = i; } // if line exceeds width, wrap if (lineWidth > scaled.size.x) { std::size_t wrapPos; if (lastWrapPos > startPos) { wrapPos = lastWrapPos; // wrap at last space } else { wrapPos = i; // no space, hard wrap } // push the line up to wrapPos lines.push_back(line.substr(startPos, wrapPos - startPos)); // skip any spaces at the beginning of next line startPos = wrapPos; while (startPos < line.size() && line[startPos] == ' ') { ++startPos; } // reset width and i lineWidth = 0; i = startPos - 1; // -1 because loop will increment i } } // add the remaining part of the line if (startPos < line.size()) { lines.push_back(line.substr(startPos)); } } if(scaleMode == TextScaleMode::Element) { float logicalPerPixelY = anchor.height / scaled.size.y; float oldHeight = anchor.height; anchor.height = lineHeight * logicalPerPixelY; if(oldHeight != anchor.height) { if(parent) { UpdatePosition(window, *parent); } else { UpdatePosition(window); } } } else if(scaleMode == TextScaleMode::Font) { //todo } else if(scaleMode == TextScaleMode::Buffer) { if constexpr(Scaling && Owning) { float neededHeight = lines.size() * lineHeight; if(neededHeight != ScalingBase::bufferHeight) { ScalingBase::bufferHeight = neededHeight; ScalingBase::bufferUpdated = true; ScalingBase::scalingBuffer.resize(neededHeight*ScalingBase::bufferWidth); } } } else { if constexpr(Scaling) { lines.resize(ScalingBase::bufferHeight / lines.size()); } else { lines.resize(scaled.size.y / lines.size()); } } } return lines; } void RenderText(Window& window, std::span lines, float size, Vector color, Font& font, TextAlignment alignment = TextAlignment::Left, std::uint32_t offsetX = 0, std::uint32_t offsetY = 0) { float scale = stbtt_ScaleForPixelHeight(&font.font, size); int baseline = (int)(font.ascent * scale); std::uint32_t lineHeight = (font.ascent - font.descent) * scale; std::uint32_t currentY = baseline; for(std::string_view line : lines) { std::uint32_t lineWidth = 0; for (const char c : line) { int advance, lsb; stbtt_GetCodepointHMetrics(&font.font, c, &advance, &lsb); lineWidth += (int)(advance * scale); } std::uint32_t startX = 0; switch (alignment) { case TextAlignment::Left: startX = 0; break; case TextAlignment::Center: startX = (scaled.size.x - lineWidth) / 2; break; case TextAlignment::Right: startX = scaled.size.x - lineWidth; break; } std::uint32_t x = startX; 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 + offsetX; int bufferY = currentY + j + c_y1 + offsetY; // Only draw pixels that are within our scaled buffer bounds if constexpr(Scaling) { if (bufferX >= 0 && bufferX < ScalingBase::bufferWidth && bufferY >= 0 && bufferY < ScalingBase::bufferHeight) { ScalingBase::scalingBuffer[bufferY * ScalingBase::bufferWidth + bufferX] = {color.r, color.g, color.b, bitmap[j * w + i]}; } } else { if (bufferX >= 0 && bufferX < (int)scaled.size.x && bufferY >= 0 && bufferY < (int)scaled.size.y) { buffer[bufferY * scaled.size.x + 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]); } } currentY += lineHeight; } } }; }