/* 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 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; #define STB_IMAGE_IMPLEMENTATION #include "../lib/stb_image.h" #include "../lib/stb_truetype.h" export module Crafter.Graphics:RenderingElement; import std; import :Transform; import :Font; import :Types; import :Image; import :Window; export namespace Crafter { enum class TextAlignment { Left, Center, Right }; enum class TextOverflowMode { Clip, Wrap }; enum class TextScaleMode { None, Font, Element, Buffer }; struct RenderElementScalingOwning { std::vector scalingBuffer; std::uint32_t bufferWidth; std::uint32_t bufferHeight; bool bufferUpdated = true; RenderElementScalingOwning() = default; RenderElementScalingOwning(std::uint32_t bufferWidth, std::uint32_t bufferHeight) : scalingBuffer(bufferWidth*bufferHeight), bufferWidth(bufferWidth), bufferHeight(bufferHeight) { } }; struct RenderElementScalingNonOwning { Pixel_BU8_GU8_RU8_AU8* scalingBuffer; std::uint32_t bufferWidth; std::uint32_t bufferHeight; bool bufferUpdated = true; RenderElementScalingNonOwning() = default; RenderElementScalingNonOwning(Pixel_BU8_GU8_RU8_AU8* scalingBuffer, std::uint32_t bufferWidth, std::uint32_t bufferHeight) : scalingBuffer(scalingBuffer), bufferWidth(bufferWidth), bufferHeight(bufferHeight) { } }; struct RenderElementRotating { std::uint32_t rotation; bool rotationUpdated = true; RenderElementRotating() = default; RenderElementRotating(std::uint32_t 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, RenderElementRotating, EmptyRotatingBase >; class RenderingElementBase : public Transform { public: std::vector buffer; OpaqueType opaque; RenderingElementBase(Anchor anchor) : Transform(anchor) { scaled.width = 0; } RenderingElementBase(Anchor anchor, OpaqueType opaque) : Transform(anchor), opaque(opaque) { scaled.width = 0; } }; template requires ((!Rotating || Scaling) && (!Owning || Scaling)) class RenderingElement : public RenderingElementBase, public ScalingBase, public RotatingBase { public: RenderingElement() = default; RenderingElement(Anchor anchor, OpaqueType opaque) : RenderingElementBase(anchor, opaque) { } RenderingElement(Anchor anchor, OpaqueType opaque, std::uint32_t rotation) requires(Rotating) : RenderingElementBase(anchor, opaque), RotatingBase(rotation) { } RenderingElement(Anchor anchor, const std::string_view imagePath) : RenderingElementBase(anchor) { LoadImage(imagePath); } RenderingElement(Anchor anchor, const std::string_view imagePath, std::uint32_t rotation) requires(Rotating) : RenderingElementBase(anchor), RotatingBase(rotation) { LoadImage(imagePath); } RenderingElement(Anchor anchor, const std::string_view imagePath, OpaqueType opaque) : RenderingElementBase(anchor, opaque) { LoadImageNoOpaqueCheck(imagePath); } RenderingElement(Anchor anchor, const std::string_view imagePath, OpaqueType opaque, std::uint32_t rotation) requires(Rotating) : RenderingElementBase(anchor, opaque), RotatingBase(rotation) { LoadImageNoOpaqueCheck(imagePath); } RenderingElement(Anchor anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, Pixel_BU8_GU8_RU8_AU8* scalingBuffer) requires(Scaling && !Owning) : RenderingElementBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight, scalingBuffer) { } RenderingElement(Anchor anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, Pixel_BU8_GU8_RU8_AU8* scalingBuffer, std::uint32_t rotation) requires(Scaling && !Owning && Rotating) : RenderingElementBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight, scalingBuffer), RotatingBase(rotation) { } RenderingElement(Anchor anchor, OpaqueType opaque, Image& image) requires(Scaling && !Owning) : RenderingElementBase(anchor, opaque), ScalingBase(image.buffer.data(), image.width, image.height) { } RenderingElement(Anchor anchor, OpaqueType opaque, Image& image, std::uint32_t rotation) requires(Scaling && !Owning && Rotating) : RenderingElementBase(anchor, opaque), ScalingBase(image.buffer.data(), image.width, image.height), RotatingBase(rotation) { } RenderingElement(Anchor anchor, Image& image) requires(Scaling && !Owning) : RenderingElementBase(anchor, image.opaque), ScalingBase(image.buffer.data(), image.width, image.height) { } RenderingElement(Anchor anchor, Image& image, std::uint32_t rotation) requires(Scaling && !Owning && Rotating) : RenderingElementBase(anchor, image.opaque), ScalingBase(image.buffer.data(), image.width, image.height), RotatingBase(rotation) { } RenderingElement(Anchor anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight) requires(Owning) : RenderingElementBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight) { } RenderingElement(Anchor anchor, OpaqueType opaque, std::uint32_t bufferWidth, std::uint32_t bufferHeight, std::uint32_t rotation) requires(Owning && Rotating) : RenderingElementBase(anchor, opaque), ScalingBase(bufferWidth, bufferHeight) , RotatingBase(rotation) { } RenderingElement(RenderingElement&) = delete; RenderingElement& operator=(RenderingElement&) = delete; void ScaleNearestNeighbor() requires(Scaling) { for (std::uint32_t y = 0; y < scaled.height; y++) { std::uint32_t srcY = y * ScalingBase::bufferHeight / scaled.height; for (std::uint32_t x = 0; x < scaled.width; x++) { std::uint32_t srcX = x * ScalingBase::bufferWidth / scaled.width; buffer[y * scaled.width + x] = ScalingBase::scalingBuffer[srcY * ScalingBase::bufferWidth + srcX]; } } } void ScaleRotating() requires(Scaling) { const double rad = (static_cast(RotatingBase::rotation) / static_cast(std::numeric_limits::max())) * 2.0 * std::numbers::pi; const std::uint32_t dstWidth = scaled.width; const std::uint32_t dstHeight = scaled.height; const double c2 = std::abs(std::cos(rad)); const double s2 = std::abs(std::sin(rad)); const double rotatedWidth = dstWidth * c2 + dstHeight * s2; const double 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.width += diffX + diffX; scaled.height += diffY + diffY; scaled.x -= diffX; scaled.y -= diffY; buffer.clear(); buffer.resize(scaled.width * scaled.height); // Destination center const double dstCx = (static_cast(scaled.width) - 1.0) * 0.5; const double dstCy = (static_cast(scaled.height) - 1.0) * 0.5; // Source center const double srcCx = (static_cast(ScalingBase::bufferWidth) - 1.0) * 0.5; const double srcCy = (static_cast(ScalingBase::bufferHeight) - 1.0) * 0.5; const double c = std::cos(rad); const double s = std::sin(rad); // Scale factors (destination → source) const double scaleX = static_cast(ScalingBase::bufferWidth) / dstWidth; const double scaleY = static_cast(ScalingBase::bufferHeight) / dstHeight; for (std::uint32_t yB = 0; yB < scaled.height; ++yB) { for (std::uint32_t xB = 0; xB < scaled.width; ++xB) { // ---- Destination pixel relative to center ---- const double dx = (static_cast(xB) - dstCx) * scaleX; const double dy = (static_cast(yB) - dstCy) * scaleY; // ---- Inverse rotation ---- const double sx = (c * dx - s * dy) + srcCx; const double 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.width + xB] = ScalingBase::scalingBuffer[srcY * ScalingBase::bufferWidth + srcX]; } } } } void UpdatePosition(Window& window, ScaleData oldScale) { if constexpr(Scaling && !Rotating) { if(oldScale.width != scaled.width || oldScale.height != scaled.height) { buffer.resize(scaled.width * scaled.height); ScaleNearestNeighbor(); window.AddDirtyRect(oldScale); window.AddDirtyRect(scaled); } else if(oldScale.x != scaled.x || oldScale.y != scaled.y) { window.AddDirtyRect(oldScale); window.AddDirtyRect(scaled); if(ScalingBase::bufferUpdated) { ScaleNearestNeighbor(); ScalingBase::bufferUpdated = false; } } else if(ScalingBase::bufferUpdated) { ScaleNearestNeighbor(); ScalingBase::bufferUpdated = false; } } else if constexpr(Rotating) { if(oldScale.width != scaled.width || oldScale.height != scaled.height) { buffer.resize(scaled.width * scaled.height); ScaleRotating(); window.AddDirtyRect(oldScale); window.AddDirtyRect(scaled); } else if(oldScale.x != scaled.x || oldScale.y != scaled.y) { window.AddDirtyRect(oldScale); window.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.width != scaled.width || oldScale.height != scaled.height) { buffer.resize(scaled.width * scaled.height); window.AddDirtyRect(oldScale); window.AddDirtyRect(scaled); } if(oldScale.x != scaled.x || oldScale.y != scaled.y) { window.AddDirtyRect(oldScale); window.AddDirtyRect(scaled); } } } void UpdatePosition(Window& window) override { ScaleData oldScale = scaled; window.ScaleElement(*this); UpdatePosition(window, oldScale); for(Transform* child : children) { child->UpdatePosition(window, *this); } } void UpdatePosition(Window& window, Transform& parent) override { ScaleData oldScale = scaled; window.ScaleElement(*this, parent); UpdatePosition(window, oldScale); for(Transform* child : children) { child->UpdatePosition(window, *this); } } void LoadImage(const std::string_view imagePath) { std::filesystem::path abs = std::filesystem::absolute(imagePath); int xSize; int ySize; unsigned char* bgData = stbi_load(abs.string().c_str(), &xSize, &ySize, nullptr, 4); if constexpr(Scaling && !Owning) { ScalingBase::bufferUpdated = true; } else if constexpr(Scaling && Owning) { ScalingBase::bufferWidth = xSize; ScalingBase::bufferHeight = ySize; ScalingBase::bufferUpdated = true; ScalingBase::scalingBuffer.resize(xSize*ySize); } else { buffer.resize(xSize*ySize); } opaque = OpaqueType::FullyOpaque; if constexpr(Scaling) { for(std::uint32_t x = 0; x < xSize; x++) { for(std::uint32_t y = 0; y < ySize; y++) { std::uint32_t idx = (x*ySize+y)*4; ScalingBase::scalingBuffer[x*ySize+y].r = bgData[idx]; ScalingBase::scalingBuffer[x*ySize+y].g = bgData[idx+1]; ScalingBase::scalingBuffer[x*ySize+y].b = bgData[idx+2]; ScalingBase::scalingBuffer[x*ySize+y].a = bgData[idx+3]; } } for(std::uint32_t i = 0; i < xSize*ySize; i++) { if(ScalingBase::scalingBuffer[i].a != 255) { opaque = OpaqueType::SemiOpaque; for(std::uint32_t i2 = 0; i2 < xSize*ySize; i2++) { if(ScalingBase::scalingBuffer[i2].a != 0 && ScalingBase::scalingBuffer[i2].a != 255) { opaque = OpaqueType::Transparent; return; } } return; } } } else { for(std::uint32_t x = 0; x < xSize; x++) { for(std::uint32_t y = 0; y < ySize; y++) { std::uint32_t idx = (x*ySize+y)*4; buffer[x*ySize+y].r = bgData[idx]; buffer[x*ySize+y].g = bgData[idx+1]; buffer[x*ySize+y].b = bgData[idx+2]; buffer[x*ySize+y].a = bgData[idx+3]; } } for(std::uint32_t i = 0; i < xSize*ySize; i++) { if(buffer[i].a != 255) { opaque = OpaqueType::SemiOpaque; for(std::uint32_t i2 = 0; i2 < xSize*ySize; i2++) { if(buffer[i2].a != 0 && buffer[i2].a != 255) { opaque = OpaqueType::Transparent; return; } } return; } } } } void LoadImageNoOpaqueCheck(const std::string_view imagePath) { std::filesystem::path abs = std::filesystem::absolute(imagePath); int xSize; int ySize; unsigned char* bgData = stbi_load(abs.string().c_str(), &xSize, &ySize, nullptr, 4); if constexpr(Scaling && !Owning) { ScalingBase::bufferUpdated = true; } else if constexpr(Scaling && Owning) { ScalingBase::bufferWidth = xSize; ScalingBase::bufferHeight = ySize; ScalingBase::bufferUpdated = true; ScalingBase::scalingBuffer.resize(xSize*ySize); } else { buffer.resize(xSize*ySize); } if constexpr(Scaling) { for(std::uint32_t x = 0; x < xSize; x++) { for(std::uint32_t y = 0; y < ySize; y++) { std::uint32_t idx = (x*ySize+y)*4; ScalingBase::scalingBuffer[x*ySize+y].r = bgData[idx]; ScalingBase::scalingBuffer[x*ySize+y].g = bgData[idx+1]; ScalingBase::scalingBuffer[x*ySize+y].b = bgData[idx+2]; ScalingBase::scalingBuffer[x*ySize+y].a = bgData[idx+3]; } } } else { for(std::uint32_t x = 0; x < xSize; x++) { for(std::uint32_t y = 0; y < ySize; y++) { std::uint32_t idx = (x*ySize+y)*4; buffer[x*ySize+y].r = bgData[idx]; buffer[x*ySize+y].g = bgData[idx+1]; buffer[x*ySize+y].b = bgData[idx+2]; buffer[x*ySize+y].a = bgData[idx+3]; } } } } std::vector ResizeText(Window& window, const std::string_view text, float size, Font& font, TextOverflowMode overflowMode = TextOverflowMode::Clip, TextScaleMode scaleMode = TextScaleMode::None, Transform* 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.height; std::int32_t oldHeight = anchor.height; std::int32_t logicalPerPixelX = anchor.width / scaled.width; 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.height / 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.width) { 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) { std::int32_t logicalPerPixelY = anchor.height / scaled.height; std::int32_t 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) { std::uint32_t 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.height / lines.size()); } } } return lines; } void RenderText(Window& window, std::span lines, float size, Pixel_BU8_GU8_RU8_AU8 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.width - lineWidth) / 2; break; case TextAlignment::Right: startX = scaled.width - 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.width && bufferY >= 0 && bufferY < (int)scaled.height) { buffer[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]); } } currentY += lineHeight; } } }; }