/* * This file is part of the Colobot: Gold Edition source code * Copyright (C) 2001-2023, Daniel Roux, EPSITEC SA & TerranovaTeam * http://epsitec.ch; http://colobot.info; http://github.com/colobot * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://gnu.org/licenses */ #include "graphics/engine/text.h" #include "app/app.h" #include "common/font_loader.h" #include "common/image.h" #include "common/logger.h" #include "common/stringutils.h" #include "common/resources/resourcemanager.h" #include "graphics/core/device.h" #include "graphics/core/renderers.h" #include "graphics/core/transparency.h" #include "graphics/engine/engine.h" #include "math/func.h" #include #include #include #include // Graphics module namespace namespace Gfx { /** * \struct MultisizeFont * \brief Font with multiple possible sizes */ struct MultisizeFont { std::string fileName; std::map> fonts; explicit MultisizeFont(const std::string &fn) : fileName(fn) {} }; /** * \struct FontTexture * \brief Single texture filled with character textures */ struct FontTexture { unsigned int id = 0; glm::ivec2 tileSize; int freeSlots = 0; }; /** * \struct CachedFont * \brief Base TTF font with UTF-8 char cache */ struct CachedFont { std::unique_ptr fontFile; TTF_Font* font = nullptr; std::map cache; CachedFont(std::unique_ptr fontFile, int pointSize) : fontFile(std::move(fontFile)) { font = TTF_OpenFontRW(this->fontFile->GetHandler(), 0, pointSize); } CachedFont(CachedFont&& other) noexcept : fontFile{std::move(other.fontFile)}, font{std::exchange(other.font, nullptr)}, cache{std::move(other.cache)} { } CachedFont& operator=(CachedFont&& other) noexcept { fontFile = std::move(other.fontFile); std::swap(font, other.font); cache = std::move(other.cache); return *this; } CachedFont(const CachedFont& other) = delete; CachedFont& operator=(const CachedFont& other) = delete; ~CachedFont() { if (font != nullptr) TTF_CloseFont(font); } }; std::string ToString(FontType type) { switch (type) { case FontType::FONT_COMMON: return "FontCommon"; case FontType::FONT_COMMON_BOLD: return "FontCommonBold"; case FontType::FONT_COMMON_ITALIC: return "FontCommonItalic"; case FontType::FONT_STUDIO: return "FontStudio"; case FontType::FONT_STUDIO_BOLD: return "FontStudioBold"; case FontType::FONT_STUDIO_ITALIC: return "FontStudioItalic"; case FontType::FONT_SATCOM: return "FontSatCom"; case FontType::FONT_SATCOM_BOLD: return "FontSatComBold"; case FontType::FONT_SATCOM_ITALIC: return "FontSatComItalic"; case FontType::FONT_BUTTON: return "FontButton"; default: throw std::invalid_argument("Unsupported value for Gfx::FontType -> std::string conversion: " + std::to_string(type)); } } namespace { constexpr glm::ivec2 REFERENCE_SIZE(800, 600); constexpr glm::ivec2 FONT_TEXTURE_SIZE(256, 256); Gfx::FontType ToBoldFontType(Gfx::FontType type) { return static_cast(type | FONT_BOLD); } Gfx::FontType ToItalicFontType(Gfx::FontType type) { return static_cast(type | FONT_ITALIC); } } // anonymous namespace /// The QuadBatch is responsible for collecting as many quad (aka rectangle) draws as possible and /// sending them to the CDevice in one big batch. This avoids making one CDevice::DrawPrimitive call /// for every CText::DrawCharAndAdjustPos call, which makes text rendering much faster. /// Currently we only collect textured quads (ie. ones using Vertex), not untextured quads (which /// use VertexCol). Untextured quads are only drawn via DrawHighlight, which happens much less often /// than drawing textured quads. class CText::CQuadBatch { public: explicit CQuadBatch(CEngine& engine) : m_engine(engine) { m_quads.reserve(1024); } /// Add a quad to be rendered. /// This may trigger a call to Flush() if necessary. void Add(Vertex2D vertices[4], unsigned int texID, TransparencyMode transparency, Color color) { if (texID != m_texID || transparency != m_transparency || color != m_color) { Flush(); m_texID = texID; m_transparency = transparency; m_color = color; } m_quads.emplace_back(Quad{{vertices[0], vertices[1], vertices[2], vertices[3]}}); } /// Draw all pending quads immediately. void Flush() { if (m_quads.empty()) return; auto renderer = m_engine.GetUIRenderer(); renderer->SetTexture(Texture{ m_texID }); renderer->SetTransparency(m_transparency); if (m_counts.size() < m_quads.size()) { m_counts.resize(m_quads.size(), 4); } auto vertices = renderer->BeginPrimitives(PrimitiveType::TRIANGLE_STRIP, m_quads.size(), m_counts.data()); size_t offset = 0; for (const auto& quad : m_quads) { std::copy_n(quad.vertices, 4, vertices + offset); offset += 4; } renderer->EndPrimitive(); m_engine.AddStatisticTriangle(static_cast(m_quads.size() * 2)); m_quads.clear(); } private: CEngine& m_engine; struct Quad { Vertex2D vertices[4]; }; std::vector m_quads; std::vector m_counts; Color m_color; unsigned int m_texID{}; TransparencyMode m_transparency = TransparencyMode::NONE; }; class FontsCache { public: using Fonts = std::map>; FontsCache() { ClearLastCachedFont(); } bool Reload(const CFontLoader& fontLoader, int pointSize) { Flush(); if (!PrepareCache(fontLoader)) { Flush(); return false; } if (!LoadDefaultFonts(fontLoader, pointSize)) { Flush(); return false; } return true; } CachedFont* GetOrOpenFont(FontType type, int pointSize) { if (IsLastCachedFont(type, pointSize)) return m_lastCachedFont; auto multisizeFontIt = m_fonts.find(type); if (multisizeFontIt == m_fonts.end()) { m_error = std::string("Font type not found in cache: ") + ToString(type); return nullptr; } MultisizeFont* multisizeFont = multisizeFontIt->second.get(); auto cachedFontIt = multisizeFont->fonts.find(pointSize); if (cachedFontIt != multisizeFont->fonts.end()) { auto* cachedFont = cachedFontIt->second.get(); SaveLastCachedFont(cachedFont, type, pointSize); return m_lastCachedFont; } auto newFont = LoadFont(multisizeFont, pointSize); if (!newFont) return nullptr; SaveLastCachedFont(newFont.get(), type, pointSize); multisizeFont->fonts[pointSize] = std::move(newFont); return m_lastCachedFont; } void Flush() { Clear(); ClearLastCachedFont(); } ~FontsCache() { Flush(); } std::string GetError() const { return m_error; } private: bool PrepareCache(const CFontLoader& fontLoader) { for (auto type : {FONT_COMMON, FONT_STUDIO, FONT_SATCOM}) { if (!PrepareCacheForFontType(type, fontLoader)) return false; if (!PrepareCacheForFontType(ToBoldFontType(type), fontLoader)) return false; if (!PrepareCacheForFontType(ToItalicFontType(type), fontLoader)) return false; } return true; } bool PrepareCacheForFontType(Gfx::FontType type, const CFontLoader& fontLoader) { if (auto font = fontLoader.GetFont(type)) { m_fonts[type] = std::make_unique(std::move(*font)); return true; } m_error = "Error on loading fonts: font type " + ToString(type) + " is not configured"; return false; } bool LoadDefaultFonts(const CFontLoader& fontLoader, int pointSize) { for (auto& font : m_fonts) { auto type = font.first; auto* cachedFont = GetOrOpenFont(type, pointSize); if (cachedFont == nullptr || cachedFont->font == nullptr) return false; } return true; } std::unique_ptr LoadFont(MultisizeFont* multisizeFont, int pointSize) { auto file = CResourceManager::GetSDLMemoryHandler(multisizeFont->fileName); if (!file->IsOpen()) { m_error = "Unable to open file '" + multisizeFont->fileName + "' (font size = " + std::to_string(pointSize) + ")"; return nullptr; } GetLogger()->Debug("Loaded font file %s (font size = %d)\n", multisizeFont->fileName.c_str(), pointSize); auto newFont = std::make_unique(std::move(file), pointSize); if (newFont->font == nullptr) { m_error = std::string("TTF_OpenFont error ") + std::string(TTF_GetError()); return nullptr; } return newFont; } void SaveLastCachedFont(CachedFont* font, FontType type, int pointSize) { m_lastCachedFont = font; m_lastFontType = type; m_lastFontSize = pointSize; } bool IsLastCachedFont(FontType font, int pointSize) const { return m_lastCachedFont != nullptr && m_lastFontType == font && m_lastFontSize == pointSize; } void Clear() { for (auto& [fontType, multisizeFont] : m_fonts) { for (auto& cachedFont : multisizeFont->fonts) { cachedFont.second->cache.clear(); } } m_fonts.clear(); } void ClearLastCachedFont() { m_lastCachedFont = nullptr; m_lastFontType = FONT_COMMON; m_lastFontSize = 0; } private: Fonts m_fonts; CachedFont* m_lastCachedFont; FontType m_lastFontType; int m_lastFontSize; std::string m_error; }; CText::CText(CEngine* engine) { m_device = nullptr; m_engine = engine; m_defaultSize = 12.0f; m_tabSize = 4; m_fontsCache = std::make_unique(); m_quadBatch = std::make_unique(*engine); } CText::~CText() { m_device = nullptr; m_engine = nullptr; } bool CText::Create() { if (TTF_Init() != 0) { m_error = std::string("TTF_Init error: ") + std::string(TTF_GetError()); return false; } if (!ReloadFonts()) { return false; } return true; } bool CText::ReloadFonts() { CFontLoader fontLoader; if (!fontLoader.Init()) { m_error = "Error on parsing fonts config file: failed to open file"; return false; } auto newCache = std::make_unique(); if (!newCache->Reload(fontLoader, GetFontPointSize(m_defaultSize))) { m_error = newCache->GetError(); return false; } m_fontsCache = std::move(newCache); return true; } void CText::Destroy() { m_fontsCache->Flush(); TTF_Quit(); } void CText::SetDevice(CDevice* device) { m_device = device; } std::string CText::GetError() { return m_error; } void CText::FlushCache() { for (auto& fontTexture : m_fontTextures) { Texture tex; tex.id = fontTexture.id; m_device->DestroyTexture(tex); } m_fontTextures.clear(); m_fontsCache->Flush(); } int CText::GetTabSize() { return m_tabSize; } void CText::SetTabSize(int tabSize) { m_tabSize = tabSize; } void CText::DrawText(const std::string &text, std::vector::iterator format, std::vector::iterator end, float size, glm::vec2 pos, float width, TextAlign align, int eol, Color color) { float sw = 0.0f; if (align == TEXT_ALIGN_CENTER) { sw = GetStringWidth(text, format, end, size); if (sw > width) sw = width; pos.x -= sw / 2.0f; } else if (align == TEXT_ALIGN_RIGHT) { sw = GetStringWidth(text, format, end, size); if (sw > width) sw = width; pos.x -= sw; } glm::ivec2 intPos = m_engine->InterfaceToWindowCoords(pos); int intWidth = width * m_engine->GetWindowSize().x; DrawString(text, format, end, size, intPos, intWidth, eol, color); } void CText::DrawText(const std::string &text, FontType font, float size, glm::vec2 pos, float width, TextAlign align, int eol, Color color) { float sw = 0.0f; if (align == TEXT_ALIGN_CENTER) { sw = GetStringWidth(text, font, size); if (sw > width) sw = width; pos.x -= sw / 2.0f; } else if (align == TEXT_ALIGN_RIGHT) { sw = GetStringWidth(text, font, size); if (sw > width) sw = width; pos.x -= sw; } glm::ivec2 intPos = m_engine->InterfaceToWindowCoords(pos); int intWidth = width * m_engine->GetWindowSize().x; DrawString(text, font, size, intPos, intWidth, eol, color); } void CText::SizeText(const std::string &text, std::vector::iterator format, std::vector::iterator endFormat, float size, glm::vec2 pos, TextAlign align, glm::vec2 &start, glm::vec2 &end) { start = end = pos; float sw = GetStringWidth(text, format, endFormat, size); end.x += sw; if (align == TEXT_ALIGN_CENTER) { start.x -= sw/2.0f; end.x -= sw/2.0f; } else if (align == TEXT_ALIGN_RIGHT) { start.x -= sw; end.x -= sw; } start.y -= GetDescent(FONT_COMMON, size); end.y += GetAscent(FONT_COMMON, size); } void CText::SizeText(const std::string &text, FontType font, float size, glm::vec2 pos, TextAlign align, glm::vec2 &start, glm::vec2 &end) { start = end = pos; float sw = GetStringWidth(text, font, size); end.x += sw; if (align == TEXT_ALIGN_CENTER) { start.x -= sw/2.0f; end.x -= sw/2.0f; } else if (align == TEXT_ALIGN_RIGHT) { start.x -= sw; end.x -= sw; } start.y -= GetDescent(font, size); end.y += GetAscent(font, size); } float CText::GetAscent(FontType font, float size) { assert(font != FONT_BUTTON); CachedFont* cf = GetOrOpenFont(font, size); assert(cf != nullptr); glm::ivec2 wndSize = { 0, TTF_FontAscent(cf->font) }; glm::vec2 ifSize = m_engine->WindowToInterfaceSize(wndSize); return ifSize.y; } float CText::GetDescent(FontType font, float size) { assert(font != FONT_BUTTON); CachedFont* cf = GetOrOpenFont(font, size); assert(cf != nullptr); glm::ivec2 wndSize = { 0, TTF_FontDescent(cf->font) }; glm::vec2 ifSize = m_engine->WindowToInterfaceSize(wndSize); return ifSize.y; } float CText::GetHeight(FontType font, float size) { assert(font != FONT_BUTTON); CachedFont* cf = GetOrOpenFont(font, size); assert(cf != nullptr); glm::ivec2 wndSize = { 0, TTF_FontHeight(cf->font) }; glm::vec2 ifSize = m_engine->WindowToInterfaceSize(wndSize); return ifSize.y; } int CText::GetHeightInt(FontType font, float size) { assert(font != FONT_BUTTON); CachedFont* cf = GetOrOpenFont(font, size); assert(cf != nullptr); return TTF_FontHeight(cf->font); } float CText::GetStringWidth(const std::string &text, std::vector::iterator format, std::vector::iterator end, float size) { float width = 0.0f; unsigned int index = 0; unsigned int fmtIndex = 0; while (index < text.length()) { FontType font = FONT_COMMON; if (format + fmtIndex != end) font = static_cast(*(format + fmtIndex) & FONT_MASK_FONT); UTF8Char ch; int len = GetCharSizeAt(font, text, index); if (len >= 1) ch.c1 = text[index]; if (len >= 2) ch.c2 = text[index+1]; if (len >= 3) ch.c3 = text[index+2]; width += GetCharWidth(ch, font, size, width); index += len; fmtIndex += len; } return width; } float CText::GetStringWidth(std::string text, FontType font, float size) { assert(font != FONT_BUTTON); // Skip special chars for (char& c : text) { if (c < 32 && c >= 0) c = ':'; } CachedFont* cf = GetOrOpenFont(font, size); assert(cf != nullptr); glm::ivec2 wndSize{}; TTF_SizeUTF8(cf->font, text.c_str(), &wndSize.x, &wndSize.y); glm::vec2 ifSize = m_engine->WindowToInterfaceSize(wndSize); return ifSize.x; } float CText::GetCharWidth(UTF8Char ch, FontType font, float size, float offset) { if (font == FONT_BUTTON) { glm::ivec2 windowSize = m_engine->GetWindowSize(); float height = GetHeight(FONT_COMMON, size); float width = height*(static_cast(windowSize.y)/windowSize.x); return width; } int width = 1; if (ch.c1 < 32 && ch.c1 >= 0) { if (ch.c1 == '\t') width = m_tabSize; // TODO: tab sizing at intervals? ch.c1 = ':'; } CachedFont* cf = GetOrOpenFont(font, size); assert(cf != nullptr); glm::vec2 charSize; auto it = cf->cache.find(ch); if (it != cf->cache.end()) { charSize = m_engine->WindowToInterfaceSize((*it).second.charSize); } else { glm::ivec2 wndSize{}; std::string text; text.append({ch.c1, ch.c2, ch.c3}); TTF_SizeUTF8(cf->font, text.c_str(), &wndSize.x, &wndSize.y); charSize = m_engine->WindowToInterfaceSize(wndSize); } return charSize.x * width; } int CText::GetCharWidthInt(UTF8Char ch, FontType font, float size, float offset) { if (font == FONT_BUTTON) { glm::ivec2 windowSize = m_engine->GetWindowSize(); int height = GetHeightInt(FONT_COMMON, size); int width = height*(static_cast(windowSize.y)/windowSize.x); return width; } int width = 1; if (ch.c1 < 32 && ch.c1 >= 0) { if (ch.c1 == '\t') width = m_tabSize; // TODO: tab sizing at intervals? ch.c1 = ':'; } CachedFont* cf = GetOrOpenFont(font, size); assert(cf != nullptr); glm::ivec2 charSize{ 0, 0 }; auto it = cf->cache.find(ch); if (it != cf->cache.end()) { charSize = (*it).second.charSize; } else { std::string text; text.append({ch.c1, ch.c2, ch.c3}); TTF_SizeUTF8(cf->font, text.c_str(), &charSize.x, &charSize.y); } return charSize.x * width; } int CText::Justify(const std::string &text, std::vector::iterator format, std::vector::iterator end, float size, float width) { float pos = 0.0f; int cut = 0; unsigned int index = 0; unsigned int fmtIndex = 0; while (index < text.length()) { FontType font = FONT_COMMON; if (format + fmtIndex != end) font = static_cast(*(format + fmtIndex) & FONT_MASK_FONT); UTF8Char ch; int len = GetCharSizeAt(font, text, index); if (len >= 1) ch.c1 = text[index]; if (len >= 2) ch.c2 = text[index+1]; if (len >= 3) ch.c3 = text[index+2]; if (font != FONT_BUTTON) { if (ch.c1 == '\n') return index+1; if (ch.c1 == ' ') cut = index+1; } pos += GetCharWidth(ch, font, size, pos); if (pos > width) { if (cut == 0) return index; else return cut; } index += len; fmtIndex += len; } return index; } int CText::Justify(const std::string &text, FontType font, float size, float width) { assert(font != FONT_BUTTON); float pos = 0.0f; int cut = 0; unsigned int index = 0; while (index < text.length()) { UTF8Char ch; int len = GetCharSizeAt(font, text, index); if (len >= 1) ch.c1 = text[index]; if (len >= 2) ch.c2 = text[index+1]; if (len >= 3) ch.c3 = text[index+2]; if (ch.c1 == '\n') { return index+1; } if (ch.c1 == ' ' ) cut = index+1; pos += GetCharWidth(ch, font, size, pos); if (pos > width) { if (cut == 0) return index; else return cut; } index += len; } return index; } int CText::Detect(const std::string &text, std::vector::iterator format, std::vector::iterator end, float size, float offset) { float pos = 0.0f; unsigned int index = 0; unsigned int fmtIndex = 0; while (index < text.length()) { FontType font = FONT_COMMON; if (format + fmtIndex != end) font = static_cast(*(format + fmtIndex) & FONT_MASK_FONT); UTF8Char ch; int len = GetCharSizeAt(font, text, index); if (len >= 1) ch.c1 = text[index]; if (len >= 2) ch.c2 = text[index+1]; if (len >= 3) ch.c3 = text[index+2]; if (ch.c1 == '\n') return index; float width = GetCharWidth(ch, font, size, pos); if (offset <= pos + width/2.0f) return index; pos += width; index += len; fmtIndex += len; } return index; } int CText::Detect(const std::string &text, FontType font, float size, float offset) { assert(font != FONT_BUTTON); float pos = 0.0f; unsigned int index = 0; while (index < text.length()) { UTF8Char ch; int len = GetCharSizeAt(font, text, index); if (len >= 1) ch.c1 = text[index]; if (len >= 2) ch.c2 = text[index+1]; if (len >= 3) ch.c3 = text[index+2]; index += len; if (ch.c1 == '\n') return index; float width = GetCharWidth(ch, font, size, pos); if (offset <= pos + width/2.0f) return index; pos += width; } return index; } UTF8Char CText::TranslateSpecialChar(int specialChar) { UTF8Char ch; switch (specialChar) { case CHAR_TAB: ch.c1 = ':'; ch.c2 = 0; ch.c3 = 0; break; case CHAR_NEWLINE: // Unicode: U+21B2 ch.c1 = static_cast(0xE2); ch.c2 = static_cast(0x86); ch.c3 = static_cast(0xB2); break; case CHAR_DOT: // Unicode: U+23C5 ch.c1 = static_cast(0xE2); ch.c2 = static_cast(0x8F); ch.c3 = static_cast(0x85); break; case CHAR_SQUARE: // Unicode: U+25FD ch.c1 = static_cast(0xE2); ch.c2 = static_cast(0x97); ch.c3 = static_cast(0xBD); break; case CHAR_SKIP_RIGHT: // Unicode: U+25B6 ch.c1 = static_cast(0xE2); ch.c2 = static_cast(0x96); ch.c3 = static_cast(0xB6); break; case CHAR_SKIP_LEFT: // Unicode: U+25C0 ch.c1 = static_cast(0xE2); ch.c2 = static_cast(0x97); ch.c3 = static_cast(0x80); break; default: ch.c1 = '?'; ch.c2 = 0; ch.c3 = 0; break; } return ch; } void CText::DrawString(const std::string &text, std::vector::iterator format, std::vector::iterator end, float size, const glm::ivec2& position, int width, int eol, Color color) { m_engine->SetWindowCoordinates(); glm::ivec2 pos = position; int start = pos.x; unsigned int fmtIndex = 0; std::vector chars; StringToUTFCharList(text, chars, format, end); for (auto it = chars.begin(); it != chars.end(); ++it) { FontType font = FONT_COMMON; if (format + fmtIndex != end) font = static_cast(*(format + fmtIndex) & FONT_MASK_FONT); UTF8Char ch = *it; int offset = pos.x - start; int cw = GetCharWidthInt(ch, font, size, offset); if (offset + cw > width) // exceeds the maximum width? { ch = TranslateSpecialChar(CHAR_SKIP_RIGHT); cw = GetCharWidthInt(ch, font, size, offset); pos.x = start + width - cw; color = Color(1.0f, 0.0f, 0.0f); DrawCharAndAdjustPos(ch, font, size, pos, color); break; } Color c = color; FontHighlight hl = static_cast(format[fmtIndex] & FONT_MASK_HIGHLIGHT); if (hl == FONT_HIGHLIGHT_TOKEN) { c = Color(0.490f, 0.380f, 0.165f, 1.0f); // #7D612A } else if (hl == FONT_HIGHLIGHT_TYPE) { c = Color(0.31f, 0.443f, 0.196f, 1.0f); // #4F7132 } else if (hl == FONT_HIGHLIGHT_CONST) { c = Color(0.882f, 0.176f, 0.176f, 1.0f); // #E12D2D } else if (hl == FONT_HIGHLIGHT_THIS) { c = Color(0.545f, 0.329f, 0.608f, 1.0f); // #8B549B } else if (hl == FONT_HIGHLIGHT_COMMENT) { c = Color(0.251f, 0.271f, 0.306f, 1.0f); // #40454E } else if (hl == FONT_HIGHLIGHT_KEYWORD) { c = Color(0.239f, 0.431f, 0.588f, 1.0f); // #3D6E96 } else if (hl == FONT_HIGHLIGHT_STRING) { c = Color(0.239f, 0.384f, 0.341f, 1.0f); // #3D6257 } // draw highlight background or link underline if (font != FONT_BUTTON) { glm::ivec2 charSize{}; charSize.x = GetCharWidthInt(ch, font, size, offset); charSize.y = GetHeightInt(font, size); // NB. for quad batching to improve highlight drawing performance, this code would have // to be rearranged to draw all highlights before any characters are drawn. DrawHighlight(format[fmtIndex], pos, charSize); } DrawCharAndAdjustPos(ch, font, size, pos, c); // increment fmtIndex for each byte in multibyte character if ( ch.c1 != 0 ) fmtIndex++; if ( ch.c2 != 0 ) fmtIndex++; if ( ch.c3 != 0 ) fmtIndex++; } if (eol != 0) { FontType font = FONT_COMMON; UTF8Char ch = TranslateSpecialChar(eol); color = Color(1.0f, 0.0f, 0.0f); DrawCharAndAdjustPos(ch, font, size, pos, color); } m_quadBatch->Flush(); m_engine->SetInterfaceCoordinates(); } void CText::StringToUTFCharList(const std::string &text, std::vector &chars) { unsigned int index = 0; unsigned int totalLength = text.length(); while (index < totalLength) { UTF8Char ch; int len = StrUtils::Utf8CharSizeAt(text, index); if (len >= 1) ch.c1 = text[index]; if (len >= 2) ch.c2 = text[index+1]; if (len >= 3) ch.c3 = text[index+2]; index += len; chars.push_back(ch); } } void CText::StringToUTFCharList(const std::string &text, std::vector &chars, std::vector::iterator format, std::vector::iterator end) { unsigned int index = 0; unsigned int totalLength = text.length(); while (index < totalLength) { UTF8Char ch; FontType font = FONT_COMMON; if (format + index != end) font = static_cast(*(format + index) & FONT_MASK_FONT); int len = GetCharSizeAt(font, text, index); if (len >= 1) ch.c1 = text[index]; if (len >= 2) ch.c2 = text[index+1]; if (len >= 3) ch.c3 = text[index+2]; index += len; chars.push_back(ch); } } int CText::GetCharSizeAt(Gfx::FontType font, const std::string& text, unsigned int index) const { int len = 0; if (font == FONT_BUTTON) { len = 1; } else { try { len = StrUtils::Utf8CharSizeAt(text, index); } catch (std::invalid_argument &e) { len = 1; } } return len; } void CText::DrawString(const std::string &text, FontType font, float size, const glm::ivec2& position, int width, int eol, Color color) { assert(font != FONT_BUTTON); glm::ivec2 pos = position; std::vector chars; StringToUTFCharList(text, chars); m_engine->SetWindowCoordinates(); for (auto it = chars.begin(); it != chars.end(); ++it) { DrawCharAndAdjustPos(*it, font, size, pos, color); } m_quadBatch->Flush(); m_engine->SetInterfaceCoordinates(); } void CText::DrawHighlight(FontMetaChar hl, const glm::ivec2& pos, const glm::ivec2& size) { // Gradient colors glm::u8vec4 grad[4]; // TODO: switch to alpha factors if ((hl & FONT_MASK_LINK) != 0) { grad[0] = grad[1] = grad[2] = grad[3] = { 0, 0, 255, 128 }; } else if ((hl & FONT_MASK_HIGHLIGHT) == FONT_HIGHLIGHT_KEY) { grad[0] = grad[1] = grad[2] = grad[3] = { 192, 192, 192, 128 }; } else { return; } m_quadBatch->Flush(); glm::ivec2 vsize = m_engine->GetWindowSize(); float h = 0.0f; if (vsize.y <= 768.0f) // 1024x768 or less? h = 1.01f; // 1 pixel else // more than 1024x768? h = 2.0f; // 2 pixels glm::vec2 p1, p2; p1.x = pos.x; p1.y = pos.y - size.y; p2.x = pos.x + size.x; p2.y = pos.y; if ((hl & FONT_MASK_LINK) != 0) { p1.y = pos.y - h; // just emphasized } auto renderer = m_device->GetUIRenderer(); renderer->SetTexture(Texture{}); renderer->SetTransparency(TransparencyMode::ALPHA); auto vertices = renderer->BeginPrimitive(PrimitiveType::TRIANGLE_STRIP, 4); vertices[0] = { { p1.x, p2.y }, {}, grad[3] }; vertices[1] = { { p1.x, p1.y }, {}, grad[0] }; vertices[2] = { { p2.x, p2.y }, {}, grad[2] }; vertices[3] = { { p2.x, p1.y }, {}, grad[1] }; renderer->EndPrimitive(); m_engine->AddStatisticTriangle(2); } void CText::DrawCharAndAdjustPos(UTF8Char ch, FontType font, float size, glm::ivec2&pos, Color color) { if (font == FONT_BUTTON) { glm::ivec2 windowSize = m_engine->GetWindowSize(); int height = GetHeightInt(FONT_COMMON, size); int width = height * (static_cast(windowSize.y)/windowSize.x); glm::ivec2 p1(pos.x, pos.y - height); glm::ivec2 p2(pos.x + width, pos.y); // For whatever reason ch.c1 is a SIGNED char, we need to fix that unsigned char icon = static_cast(ch.c1); // TODO: A bit of code duplication, see CControl::SetButtonTextureForIcon() unsigned int texID = m_engine->LoadTexture("textures/interface/button" + StrUtils::ToString((icon/64) + 1) + ".png").id; icon = icon%64; glm::vec2 uv1, uv2; uv1.x = (32.0f / 256.0f) * (icon%8); uv1.y = (32.0f / 256.0f) * (icon/8); uv2.x = (32.0f / 256.0f) + uv1.x; uv2.y = (32.0f / 256.0f) + uv1.y; float dp = 0.5f / 256.0f; uv1.x += dp; uv1.y += dp; uv2.x -= dp; uv2.y -= dp; Gfx::IntColor col = Gfx::ColorToIntColor(color); Gfx::Vertex2D vertices[4]; vertices[0] = { { p1.x, p2.y }, { uv1.x, uv2.y }, col }; vertices[1] = { { p1.x, p1.y }, { uv1.x, uv1.y }, col }; vertices[2] = { { p2.x, p2.y }, { uv2.x, uv2.y }, col }; vertices[3] = { { p2.x, p1.y }, { uv2.x, uv1.y }, col }; m_quadBatch->Add(vertices, texID, TransparencyMode::WHITE, color); pos.x += width; } else { int width = 1; if (ch.c1 > 0 && ch.c1 < 32) { if (ch.c1 == '\t') { color = Color(1.0f, 0.0f, 0.0f, 1.0f); width = m_tabSize; } ch = TranslateSpecialChar(ch.c1); } CharTexture tex = GetCharTexture(ch, font, size); glm::vec2 p1(pos.x, pos.y - tex.charSize.y); glm::vec2 p2(pos.x + tex.charSize.x, pos.y); const float halfPixelMargin = 0.5f; glm::vec2 texCoord1(static_cast(tex.charPos.x + halfPixelMargin) / FONT_TEXTURE_SIZE.x, static_cast(tex.charPos.y + halfPixelMargin) / FONT_TEXTURE_SIZE.y); glm::vec2 texCoord2(static_cast(tex.charPos.x + tex.charSize.x - halfPixelMargin) / FONT_TEXTURE_SIZE.x, static_cast(tex.charPos.y + tex.charSize.y - halfPixelMargin) / FONT_TEXTURE_SIZE.y); Gfx::IntColor col = Gfx::ColorToIntColor(color); Gfx::Vertex2D vertices[4]; vertices[0] = { { p1.x, p2.y }, { texCoord1.x, texCoord2.y }, col }; vertices[1] = { { p1.x, p1.y }, { texCoord1.x, texCoord1.y }, col }; vertices[2] = { { p2.x, p2.y }, { texCoord2.x, texCoord2.y }, col }; vertices[3] = { { p2.x, p1.y }, { texCoord2.x, texCoord1.y }, col }; m_quadBatch->Add(vertices, tex.id, TransparencyMode::ALPHA, color); pos.x += tex.charSize.x * width; } } int CText::GetFontPointSize(float size) const { glm::ivec2 windowSize = m_engine->GetWindowSize(); return static_cast(size * (glm::length(glm::vec2(windowSize)) / glm::length(glm::vec2(REFERENCE_SIZE)))); } CachedFont* CText::GetOrOpenFont(FontType type, float size) { auto* cachedFont = m_fontsCache->GetOrOpenFont(type, GetFontPointSize(size)); if (!cachedFont) { m_error = m_fontsCache->GetError(); return nullptr; } return cachedFont; } CharTexture CText::GetCharTexture(UTF8Char ch, FontType font, float size) { CachedFont* cf = GetOrOpenFont(font, size); if (cf == nullptr) return CharTexture(); auto it = cf->cache.find(ch); CharTexture tex; if (it != cf->cache.end()) { tex = (*it).second; } else { tex = CreateCharTexture(ch, cf); if (tex.id == 0) // invalid return CharTexture(); cf->cache[ch] = tex; } return tex; } glm::ivec2 CText::GetFontTextureSize() { return FONT_TEXTURE_SIZE; } CharTexture CText::CreateCharTexture(UTF8Char ch, CachedFont* font) { CharTexture texture; SDL_Surface* textSurface = nullptr; SDL_Color white = {255, 255, 255, 0}; char str[] = { ch.c1, ch.c2, ch.c3, '\0' }; textSurface = TTF_RenderUTF8_Blended(font->font, str, white); if (textSurface == nullptr) { m_error = "TTF_Render error"; return texture; } const int pixelMargin = 1; glm::ivec2 tileSize(Math::Max(16, Math::NextPowerOfTwo(textSurface->w)) + pixelMargin, Math::Max(16, Math::NextPowerOfTwo(textSurface->h)) + pixelMargin); FontTexture* fontTexture = GetOrCreateFontTexture(tileSize); if (fontTexture == nullptr) { m_error = "Texture create error"; } else { texture.id = fontTexture->id; texture.charPos = GetNextTilePos(*fontTexture); texture.charSize = { textSurface->w, textSurface->h }; ImageData imageData; imageData.surface = textSurface; Texture tex; tex.id = texture.id; m_device->UpdateTexture(tex, texture.charPos, &imageData, TextureFormat::RGBA); imageData.surface = nullptr; --fontTexture->freeSlots; } SDL_FreeSurface(textSurface); return texture; } FontTexture* CText::GetOrCreateFontTexture(const glm::ivec2& tileSize) { for (auto& fontTexture : m_fontTextures) { if (fontTexture.tileSize == tileSize && fontTexture.freeSlots > 0) return &fontTexture; } FontTexture newFontTexture = CreateFontTexture(tileSize); if (newFontTexture.id == 0) { return nullptr; } m_fontTextures.push_back(newFontTexture); return &m_fontTextures.back(); } FontTexture CText::CreateFontTexture(const glm::ivec2& tileSize) { SDL_Surface* textureSurface = SDL_CreateRGBSurface(0, FONT_TEXTURE_SIZE.x, FONT_TEXTURE_SIZE.y, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000); ImageData data; data.surface = textureSurface; TextureCreateParams createParams; createParams.format = TextureFormat::RGBA; createParams.filter = TextureFilter::NEAREST; createParams.mipmap = false; Texture tex = m_device->CreateTexture(&data, createParams); data.surface = nullptr; SDL_FreeSurface(textureSurface); FontTexture fontTexture; fontTexture.id = tex.id; fontTexture.tileSize = tileSize; int horizontalTiles = FONT_TEXTURE_SIZE.x / tileSize.x; int verticalTiles = FONT_TEXTURE_SIZE.y / tileSize.y; fontTexture.freeSlots = horizontalTiles * verticalTiles; return fontTexture; } glm::ivec2 CText::GetNextTilePos(const FontTexture& fontTexture) { int horizontalTiles = FONT_TEXTURE_SIZE.x / std::max(1, fontTexture.tileSize.x); //this should prevent crashes in some combinations of resolution and font size, see issue #1128 int verticalTiles = FONT_TEXTURE_SIZE.y / std::max(1, fontTexture.tileSize.y); int totalTiles = horizontalTiles * verticalTiles; int tileNumber = totalTiles - fontTexture.freeSlots; int verticalTileIndex = tileNumber / std::max(1, horizontalTiles); int horizontalTileIndex = tileNumber % horizontalTiles; return { horizontalTileIndex * fontTexture.tileSize.x, verticalTileIndex * fontTexture.tileSize.y }; } } // namespace Gfx