/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #include #include #include #include #include #include #include #include // ft2 texture #include #include FT_FREETYPE_H #include #include #include #include #include #include using namespace std::chrono_literals; enum { FONT_NAME_SIZE = 128, }; struct SGlyph { enum class EState { UNINITIALIZED, RENDERED, ERROR, }; EState m_State = EState::UNINITIALIZED; int m_FontSize; FT_Face m_Face; int m_Chr; FT_UInt m_GlyphIndex; // these values are scaled to the font size // width * font_size == real_size float m_Width; float m_Height; float m_CharWidth; float m_CharHeight; float m_OffsetX; float m_OffsetY; float m_AdvanceX; float m_aUVs[4]; }; struct SGlyphKeyHash { size_t operator()(const std::tuple &Key) const { size_t Hash = 17; Hash = Hash * 31 + std::hash()(std::get<0>(Key)); Hash = Hash * 31 + std::hash()(std::get<1>(Key)); Hash = Hash * 31 + std::hash()(std::get<2>(Key)); return Hash; } }; struct SGlyphKeyEquals { bool operator()(const std::tuple &Lhs, const std::tuple &Rhs) const { return std::get<0>(Lhs) == std::get<0>(Rhs) && std::get<1>(Lhs) == std::get<1>(Rhs) && std::get<2>(Lhs) == std::get<2>(Rhs); } }; class CAtlas { struct SSectionKeyHash { size_t operator()(const std::tuple &Key) const { // Width and height should never be above 2^16 so this hash should cause no collisions return (std::get<0>(Key) << 16) ^ std::get<1>(Key); } }; struct SSectionKeyEquals { bool operator()(const std::tuple &Lhs, const std::tuple &Rhs) const { return std::get<0>(Lhs) == std::get<0>(Rhs) && std::get<1>(Lhs) == std::get<1>(Rhs); } }; struct SSection { size_t m_X; size_t m_Y; size_t m_W; size_t m_H; SSection() = default; SSection(size_t X, size_t Y, size_t W, size_t H) : m_X(X), m_Y(Y), m_W(W), m_H(H) { } }; /** * Sections with a smaller width or height will not be created * when cutting larger sections, to prevent collecting many * small, mostly unusable sections. */ static constexpr size_t MIN_SECTION_DIMENSION = 6; /** * Sections with larger width or height will be stored in m_vSections. * Sections with width and height equal or smaller will be stored in m_SectionsMap. * This achieves a good balance between the size of the vector storing all large * sections and the map storing vectors of all sections with specific small sizes. * Lowering this value will result in the size of m_vSections becoming the bottleneck. * Increasing this value will result in the map becoming the bottleneck. */ static constexpr size_t MAX_SECTION_DIMENSION_MAPPED = 8 * MIN_SECTION_DIMENSION; size_t m_TextureDimension; std::vector m_vSections; std::unordered_map, std::vector, SSectionKeyHash, SSectionKeyEquals> m_SectionsMap; void AddSection(size_t X, size_t Y, size_t W, size_t H) { std::vector &vSections = W <= MAX_SECTION_DIMENSION_MAPPED && H <= MAX_SECTION_DIMENSION_MAPPED ? m_SectionsMap[std::make_tuple(W, H)] : m_vSections; vSections.emplace_back(X, Y, W, H); } void UseSection(const SSection &Section, size_t Width, size_t Height, int &PosX, int &PosY) { PosX = Section.m_X; PosY = Section.m_Y; // Create cut sections const size_t CutW = Section.m_W - Width; const size_t CutH = Section.m_H - Height; if(CutW == 0) { if(CutH >= MIN_SECTION_DIMENSION) AddSection(Section.m_X, Section.m_Y + Height, Section.m_W, CutH); } else if(CutH == 0) { if(CutW >= MIN_SECTION_DIMENSION) AddSection(Section.m_X + Width, Section.m_Y, CutW, Section.m_H); } else if(CutW > CutH) { if(CutW >= MIN_SECTION_DIMENSION) AddSection(Section.m_X + Width, Section.m_Y, CutW, Section.m_H); if(CutH >= MIN_SECTION_DIMENSION) AddSection(Section.m_X, Section.m_Y + Height, Width, CutH); } else { if(CutH >= MIN_SECTION_DIMENSION) AddSection(Section.m_X, Section.m_Y + Height, Section.m_W, CutH); if(CutW >= MIN_SECTION_DIMENSION) AddSection(Section.m_X + Width, Section.m_Y, CutW, Height); } } public: void Clear(size_t TextureDimension) { m_TextureDimension = TextureDimension; m_vSections.clear(); m_vSections.emplace_back(0, 0, m_TextureDimension, m_TextureDimension); m_SectionsMap.clear(); } void IncreaseDimension(size_t NewTextureDimension) { dbg_assert(NewTextureDimension == m_TextureDimension * 2, "New atlas dimension must be twice the old one"); // Create 3 square sections to cover the new area, add the sections // to the beginning of the vector so they are considered last. m_vSections.emplace_back(m_TextureDimension, m_TextureDimension, m_TextureDimension, m_TextureDimension); m_vSections.emplace_back(m_TextureDimension, 0, m_TextureDimension, m_TextureDimension); m_vSections.emplace_back(0, m_TextureDimension, m_TextureDimension, m_TextureDimension); std::rotate(m_vSections.rbegin(), m_vSections.rbegin() + 3, m_vSections.rend()); m_TextureDimension = NewTextureDimension; } bool Add(size_t Width, size_t Height, int &PosX, int &PosY) { if(m_vSections.empty() || m_TextureDimension < Width || m_TextureDimension < Height) return false; // Find small section more efficiently by using maps if(Width <= MAX_SECTION_DIMENSION_MAPPED && Height <= MAX_SECTION_DIMENSION_MAPPED) { const auto UseSectionFromVector = [&](std::vector &vSections) { if(!vSections.empty()) { const SSection Section = vSections.back(); vSections.pop_back(); UseSection(Section, Width, Height, PosX, PosY); return true; } return false; }; if(UseSectionFromVector(m_SectionsMap[std::make_tuple(Width, Height)])) return true; for(size_t CheckWidth = Width + 1; CheckWidth <= MAX_SECTION_DIMENSION_MAPPED; ++CheckWidth) { if(UseSectionFromVector(m_SectionsMap[std::make_tuple(CheckWidth, Height)])) return true; } for(size_t CheckHeight = Height + 1; CheckHeight <= MAX_SECTION_DIMENSION_MAPPED; ++CheckHeight) { if(UseSectionFromVector(m_SectionsMap[std::make_tuple(Width, CheckHeight)])) return true; } // We don't iterate sections in the map with increasing width and height at the same time, // because it's slower and doesn't noticeable increase the atlas utilization. } // Check vector for larger section size_t SmallestLossValue = std::numeric_limits::max(); size_t SmallestLossIndex = m_vSections.size(); size_t SectionIndex = m_vSections.size(); do { --SectionIndex; const SSection &Section = m_vSections[SectionIndex]; if(Section.m_W < Width || Section.m_H < Height) continue; const size_t LossW = Section.m_W - Width; const size_t LossH = Section.m_H - Height; size_t Loss; if(LossW == 0) Loss = LossH; else if(LossH == 0) Loss = LossW; else Loss = LossW * LossH; if(Loss < SmallestLossValue) { SmallestLossValue = Loss; SmallestLossIndex = SectionIndex; if(SmallestLossValue == 0) break; } } while(SectionIndex > 0); if(SmallestLossIndex == m_vSections.size()) return false; // No usable section found in vector // Use the section with the smallest loss const SSection Section = m_vSections[SmallestLossIndex]; m_vSections.erase(m_vSections.begin() + SmallestLossIndex); UseSection(Section, Width, Height, PosX, PosY); return true; } }; class CGlyphMap { public: enum { FONT_TEXTURE_FILL = 0, // the main text body FONT_TEXTURE_OUTLINE, // the text outline NUM_FONT_TEXTURES, }; private: /** * The initial dimension of the atlas textures. * Results in 1 MB of memory being used per texture. */ static constexpr int INITIAL_ATLAS_DIMENSION = 1024; /** * The maximum dimension of the atlas textures. * Results in 256 MB of memory being used per texture. */ static constexpr int MAXIMUM_ATLAS_DIMENSION = 16 * 1024; /** * The minimum supported font size. */ static constexpr int MIN_FONT_SIZE = 6; /** * The maximum supported font size. */ static constexpr int MAX_FONT_SIZE = 128; /** * White square to indicate missing glyph. */ static constexpr int REPLACEMENT_CHARACTER = 0x25a1; IGraphics *m_pGraphics; IGraphics *Graphics() { return m_pGraphics; } // Atlas textures and data IGraphics::CTextureHandle m_aTextures[NUM_FONT_TEXTURES]; // Width and height are the same, all font textures have the same dimensions size_t m_TextureDimension = INITIAL_ATLAS_DIMENSION; // Keep the full texture data, because OpenGL doesn't provide texture copying uint8_t *m_apTextureData[NUM_FONT_TEXTURES]; CAtlas m_TextureAtlas; std::unordered_map, SGlyph, SGlyphKeyHash, SGlyphKeyEquals> m_Glyphs; // Data used for rendering glyphs uint8_t m_aaGlyphData[NUM_FONT_TEXTURES][64 * 1024]; // Font faces FT_Face m_DefaultFace = nullptr; FT_Face m_IconFace = nullptr; FT_Face m_VariantFace = nullptr; FT_Face m_SelectedFace = nullptr; std::vector m_vFallbackFaces; std::vector m_vFtFaces; FT_Face GetFaceByName(const char *pFamilyName) { if(pFamilyName == nullptr || pFamilyName[0] == '\0') return nullptr; FT_Face FamilyNameMatch = nullptr; char aFamilyStyleName[FONT_NAME_SIZE]; for(const auto &CurrentFace : m_vFtFaces) { // Best match: font face with matching family and style name str_format(aFamilyStyleName, sizeof(aFamilyStyleName), "%s %s", CurrentFace->family_name, CurrentFace->style_name); if(str_comp(pFamilyName, aFamilyStyleName) == 0) { return CurrentFace; } // Second best match: font face with matching family if(!FamilyNameMatch && str_comp(pFamilyName, CurrentFace->family_name) == 0) { FamilyNameMatch = CurrentFace; } } return FamilyNameMatch; } bool IncreaseGlyphMapSize() { if(m_TextureDimension >= MAXIMUM_ATLAS_DIMENSION) return false; const size_t NewTextureDimension = m_TextureDimension * 2; log_debug("textrender", "Increasing atlas dimension to %" PRIzu " (%" PRIzu " MB used for textures)", NewTextureDimension, (NewTextureDimension / 1024) * (NewTextureDimension / 1024) * NUM_FONT_TEXTURES); UnloadTextures(); for(auto &pTextureData : m_apTextureData) { uint8_t *pTmpTexBuffer = new uint8_t[NewTextureDimension * NewTextureDimension]; mem_zero(pTmpTexBuffer, NewTextureDimension * NewTextureDimension * sizeof(uint8_t)); for(size_t y = 0; y < m_TextureDimension; ++y) { mem_copy(&pTmpTexBuffer[y * NewTextureDimension], &pTextureData[y * m_TextureDimension], m_TextureDimension); } delete[] pTextureData; pTextureData = pTmpTexBuffer; } m_TextureAtlas.IncreaseDimension(NewTextureDimension); m_TextureDimension = NewTextureDimension; UploadTextures(); return true; } void UploadTextures() { const size_t NewTextureSize = m_TextureDimension * m_TextureDimension; uint8_t *pTmpTextFillData = static_cast(malloc(NewTextureSize)); uint8_t *pTmpTextOutlineData = static_cast(malloc(NewTextureSize)); mem_copy(pTmpTextFillData, m_apTextureData[FONT_TEXTURE_FILL], NewTextureSize); mem_copy(pTmpTextOutlineData, m_apTextureData[FONT_TEXTURE_OUTLINE], NewTextureSize); Graphics()->LoadTextTextures(m_TextureDimension, m_TextureDimension, m_aTextures[FONT_TEXTURE_FILL], m_aTextures[FONT_TEXTURE_OUTLINE], pTmpTextFillData, pTmpTextOutlineData); } void UnloadTextures() { Graphics()->UnloadTextTextures(m_aTextures[FONT_TEXTURE_FILL], m_aTextures[FONT_TEXTURE_OUTLINE]); } FT_UInt GetCharGlyph(int Chr, FT_Face *pFace, bool AllowReplacementCharacter) { for(FT_Face Face : {m_SelectedFace, m_DefaultFace, m_VariantFace}) { if(Face && Face->charmap) { FT_UInt GlyphIndex = FT_Get_Char_Index(Face, (FT_ULong)Chr); if(GlyphIndex) { *pFace = Face; return GlyphIndex; } } } for(const auto &FallbackFace : m_vFallbackFaces) { if(FallbackFace->charmap) { FT_UInt GlyphIndex = FT_Get_Char_Index(FallbackFace, (FT_ULong)Chr); if(GlyphIndex) { *pFace = FallbackFace; return GlyphIndex; } } } if(!m_DefaultFace || !m_DefaultFace->charmap || !AllowReplacementCharacter) { *pFace = nullptr; return 0; } FT_UInt GlyphIndex = FT_Get_Char_Index(m_DefaultFace, (FT_ULong)REPLACEMENT_CHARACTER); *pFace = m_DefaultFace; if(GlyphIndex == 0) { log_debug("textrender", "Default font has no glyph for either %d or replacement char %d.", Chr, REPLACEMENT_CHARACTER); } return GlyphIndex; } void Grow(const unsigned char *pIn, unsigned char *pOut, int w, int h, int OutlineCount) const { for(int y = 0; y < h; y++) { for(int x = 0; x < w; x++) { int c = pIn[y * w + x]; for(int sy = -OutlineCount; sy <= OutlineCount; sy++) { for(int sx = -OutlineCount; sx <= OutlineCount; sx++) { int GetX = x + sx; int GetY = y + sy; if(GetX >= 0 && GetY >= 0 && GetX < w && GetY < h) { int Index = GetY * w + GetX; float Mask = 1.f - clamp(length(vec2(sx, sy)) - OutlineCount, 0.f, 1.f); c = maximum(c, int(pIn[Index] * Mask)); } } } pOut[y * w + x] = c; } } } int AdjustOutlineThicknessToFontSize(int OutlineThickness, int FontSize) const { if(FontSize > 48) OutlineThickness *= 4; else if(FontSize >= 18) OutlineThickness *= 2; return OutlineThickness; } void UploadGlyph(int TextureIndex, int PosX, int PosY, size_t Width, size_t Height, const unsigned char *pData) { for(size_t y = 0; y < Height; ++y) { mem_copy(&m_apTextureData[TextureIndex][PosX + ((y + PosY) * m_TextureDimension)], &pData[y * Width], Width); } Graphics()->UpdateTextTexture(m_aTextures[TextureIndex], PosX, PosY, Width, Height, pData); } bool FitGlyph(size_t Width, size_t Height, int &PosX, int &PosY) { return m_TextureAtlas.Add(Width, Height, PosX, PosY); } bool RenderGlyph(SGlyph &Glyph) { FT_Set_Pixel_Sizes(Glyph.m_Face, 0, Glyph.m_FontSize); if(FT_Load_Glyph(Glyph.m_Face, Glyph.m_GlyphIndex, FT_LOAD_RENDER | FT_LOAD_NO_BITMAP)) { log_debug("textrender", "Error loading glyph. Chr=%d GlyphIndex=%u", Glyph.m_Chr, Glyph.m_GlyphIndex); return false; } const FT_Bitmap *pBitmap = &Glyph.m_Face->glyph->bitmap; const unsigned RealWidth = pBitmap->width; const unsigned RealHeight = pBitmap->rows; // adjust spacing int OutlineThickness = 0; int x = 0; int y = 0; if(RealWidth > 0) { OutlineThickness = AdjustOutlineThicknessToFontSize(1, Glyph.m_FontSize); x += (OutlineThickness + 1); y += (OutlineThickness + 1); } const unsigned Width = RealWidth + x * 2; const unsigned Height = RealHeight + y * 2; int X = 0; int Y = 0; if(Width > 0 && Height > 0) { // find space in atlas, or increase size if necessary while(!FitGlyph(Width, Height, X, Y)) { if(!IncreaseGlyphMapSize()) { log_debug("textrender", "Cannot fit glyph into atlas, which is already at maximum size. Chr=%d GlyphIndex=%u", Glyph.m_Chr, Glyph.m_GlyphIndex); return false; } } // prepare glyph data mem_zero(m_aaGlyphData[FONT_TEXTURE_FILL], (size_t)Width * Height * sizeof(uint8_t)); for(unsigned py = 0; py < pBitmap->rows; ++py) { mem_copy(&m_aaGlyphData[FONT_TEXTURE_FILL][(py + y) * Width + x], &pBitmap->buffer[py * pBitmap->width], pBitmap->width); } // upload the glyph UploadGlyph(FONT_TEXTURE_FILL, X, Y, Width, Height, m_aaGlyphData[FONT_TEXTURE_FILL]); Grow(m_aaGlyphData[FONT_TEXTURE_FILL], m_aaGlyphData[FONT_TEXTURE_OUTLINE], Width, Height, OutlineThickness); UploadGlyph(FONT_TEXTURE_OUTLINE, X, Y, Width, Height, m_aaGlyphData[FONT_TEXTURE_OUTLINE]); } // set glyph info { const int BmpWidth = pBitmap->width + x * 2; const int BmpHeight = pBitmap->rows + y * 2; Glyph.m_Height = Height; Glyph.m_Width = Width; Glyph.m_CharHeight = RealHeight; Glyph.m_CharWidth = RealWidth; Glyph.m_OffsetX = (Glyph.m_Face->glyph->metrics.horiBearingX >> 6); Glyph.m_OffsetY = -((Glyph.m_Face->glyph->metrics.height >> 6) - (Glyph.m_Face->glyph->metrics.horiBearingY >> 6)); Glyph.m_AdvanceX = (Glyph.m_Face->glyph->advance.x >> 6); Glyph.m_aUVs[0] = X; Glyph.m_aUVs[1] = Y; Glyph.m_aUVs[2] = Glyph.m_aUVs[0] + BmpWidth; Glyph.m_aUVs[3] = Glyph.m_aUVs[1] + BmpHeight; Glyph.m_State = SGlyph::EState::RENDERED; } return true; } public: CGlyphMap(IGraphics *pGraphics) { m_pGraphics = pGraphics; for(auto &pTextureData : m_apTextureData) { pTextureData = new uint8_t[m_TextureDimension * m_TextureDimension]; mem_zero(pTextureData, m_TextureDimension * m_TextureDimension * sizeof(uint8_t)); } m_TextureAtlas.Clear(m_TextureDimension); UploadTextures(); } ~CGlyphMap() { UnloadTextures(); for(auto &pTextureData : m_apTextureData) { delete[] pTextureData; } } FT_Face DefaultFace() const { return m_DefaultFace; } FT_Face IconFace() const { return m_IconFace; } void AddFace(FT_Face Face) { m_vFtFaces.push_back(Face); if(!m_DefaultFace) m_DefaultFace = Face; } void SetDefaultFaceByName(const char *pFamilyName) { m_DefaultFace = GetFaceByName(pFamilyName); } void SetIconFaceByName(const char *pFamilyName) { m_IconFace = GetFaceByName(pFamilyName); } void AddFallbackFaceByName(const char *pFamilyName) { FT_Face Face = GetFaceByName(pFamilyName); if(Face != nullptr && std::find(m_vFallbackFaces.begin(), m_vFallbackFaces.end(), Face) == m_vFallbackFaces.end()) { m_vFallbackFaces.push_back(Face); } } void SetVariantFaceByName(const char *pFamilyName) { FT_Face Face = GetFaceByName(pFamilyName); if(m_VariantFace != Face) { m_VariantFace = Face; Clear(); // rebuild atlas after changing variant font } } void SetFontPreset(EFontPreset FontPreset) { switch(FontPreset) { case EFontPreset::DEFAULT_FONT: m_SelectedFace = nullptr; break; case EFontPreset::ICON_FONT: m_SelectedFace = m_IconFace; break; } } void Clear() { for(size_t TextureIndex = 0; TextureIndex < NUM_FONT_TEXTURES; ++TextureIndex) { mem_zero(m_apTextureData[TextureIndex], m_TextureDimension * m_TextureDimension * sizeof(uint8_t)); Graphics()->UpdateTextTexture(m_aTextures[TextureIndex], 0, 0, m_TextureDimension, m_TextureDimension, m_apTextureData[TextureIndex]); } m_TextureAtlas.Clear(m_TextureDimension); m_Glyphs.clear(); } const SGlyph *GetGlyph(int Chr, int FontSize) { FontSize = clamp(FontSize, MIN_FONT_SIZE, MAX_FONT_SIZE); // Find glyph index and most appropriate font face. FT_Face Face; FT_UInt GlyphIndex = GetCharGlyph(Chr, &Face, false); if(GlyphIndex == 0) { // Use replacement character if glyph could not be found, // also retrieve replacement character from the atlas. return Chr == REPLACEMENT_CHARACTER ? nullptr : GetGlyph(REPLACEMENT_CHARACTER, FontSize); } // Check if glyph for this (font face, character, font size)-combination was already rendered. SGlyph &Glyph = m_Glyphs[std::make_tuple(Face, Chr, FontSize)]; if(Glyph.m_State == SGlyph::EState::RENDERED) return &Glyph; else if(Glyph.m_State == SGlyph::EState::ERROR) return nullptr; // Else, render it. Glyph.m_FontSize = FontSize; Glyph.m_Face = Face; Glyph.m_Chr = Chr; Glyph.m_GlyphIndex = GlyphIndex; if(RenderGlyph(Glyph)) return &Glyph; // Use replacement character if the glyph could not be rendered, // also retrieve replacement character from the atlas. const SGlyph *pReplacementCharacter = Chr == REPLACEMENT_CHARACTER ? nullptr : GetGlyph(REPLACEMENT_CHARACTER, FontSize); if(pReplacementCharacter) { Glyph = *pReplacementCharacter; return &Glyph; } // Keep failed glyph in the cache so we don't attempt to render it again, // but set its state to ERROR so we don't return it to the text render. Glyph.m_State = SGlyph::EState::ERROR; return nullptr; } vec2 Kerning(const SGlyph *pLeft, const SGlyph *pRight) const { if(pLeft != nullptr && pRight != nullptr && pLeft->m_Face == pRight->m_Face && pLeft->m_FontSize == pRight->m_FontSize) { FT_Vector Kerning = {0, 0}; FT_Set_Pixel_Sizes(pLeft->m_Face, 0, pLeft->m_FontSize); FT_Get_Kerning(pLeft->m_Face, pLeft->m_Chr, pRight->m_Chr, FT_KERNING_DEFAULT, &Kerning); return vec2(Kerning.x >> 6, Kerning.y >> 6); } return vec2(0.0f, 0.0f); } void UploadEntityLayerText(const CImageInfo &TextImage, int TexSubWidth, int TexSubHeight, const char *pText, int Length, float x, float y, int FontSize) { if(FontSize < 1) return; const size_t PixelSize = TextImage.PixelSize(); const char *pCurrent = pText; const char *pEnd = pCurrent + Length; int WidthLastChars = 0; while(pCurrent < pEnd) { const char *pTmp = pCurrent; const int NextCharacter = str_utf8_decode(&pTmp); if(NextCharacter) { FT_Face Face; FT_UInt GlyphIndex = GetCharGlyph(NextCharacter, &Face, true); if(GlyphIndex == 0) { pCurrent = pTmp; continue; } FT_Set_Pixel_Sizes(Face, 0, FontSize); if(FT_Load_Char(Face, NextCharacter, FT_LOAD_RENDER | FT_LOAD_NO_BITMAP)) { log_debug("textrender", "Error loading glyph. Chr=%d GlyphIndex=%u", NextCharacter, GlyphIndex); pCurrent = pTmp; continue; } const FT_Bitmap *pBitmap = &Face->glyph->bitmap; // prepare glyph data const size_t GlyphDataSize = (size_t)pBitmap->width * pBitmap->rows * sizeof(uint8_t); if(pBitmap->pixel_mode == FT_PIXEL_MODE_GRAY) mem_copy(m_aaGlyphData[FONT_TEXTURE_FILL], pBitmap->buffer, GlyphDataSize); else mem_zero(m_aaGlyphData[FONT_TEXTURE_FILL], GlyphDataSize); for(unsigned OffY = 0; OffY < pBitmap->rows; ++OffY) { for(unsigned OffX = 0; OffX < pBitmap->width; ++OffX) { const int ImgOffX = clamp(x + OffX + WidthLastChars, x, (x + TexSubWidth) - 1); const int ImgOffY = clamp(y + OffY, y, (y + TexSubHeight) - 1); const size_t ImageOffset = ImgOffY * (TextImage.m_Width * PixelSize) + ImgOffX * PixelSize; const size_t GlyphOffset = OffY * pBitmap->width + OffX; for(size_t i = 0; i < PixelSize; ++i) { if(i != PixelSize - 1) { *(TextImage.m_pData + ImageOffset + i) = 255; } else { *(TextImage.m_pData + ImageOffset + i) = *(m_aaGlyphData[FONT_TEXTURE_FILL] + GlyphOffset); } } } } WidthLastChars += (pBitmap->width + 1); } pCurrent = pTmp; } } size_t TextureDimension() const { return m_TextureDimension; } IGraphics::CTextureHandle Texture(size_t TextureIndex) const { return m_aTextures[TextureIndex]; } }; typedef vector4_base STextCharQuadVertexColor; struct STextCharQuadVertex { STextCharQuadVertex() { m_Color.r = m_Color.g = m_Color.b = m_Color.a = 255; } float m_X, m_Y; // do not use normalized floats as coordinates, since the texture might grow float m_U, m_V; STextCharQuadVertexColor m_Color; }; struct STextCharQuad { STextCharQuadVertex m_aVertices[4]; }; struct SStringInfo { int m_QuadBufferObjectIndex; int m_QuadBufferContainerIndex; int m_SelectionQuadContainerIndex; std::vector m_vCharacterQuads; }; struct STextContainer { STextContainer() { Reset(); } SStringInfo m_StringInfo; // keep these values to calculate offsets float m_AlignedStartX; float m_AlignedStartY; float m_X; float m_Y; int m_Flags; int m_LineCount; int m_GlyphCount; int m_CharCount; int m_MaxLines; float m_LineWidth; unsigned m_RenderFlags; bool m_HasCursor; bool m_ForceCursorRendering; bool m_HasSelection; bool m_SingleTimeUse; STextBoundingBox m_BoundingBox; // prefix of the container's text stored for debugging purposes char m_aDebugText[32]; STextContainerIndex m_ContainerIndex; void Reset() { m_StringInfo.m_QuadBufferObjectIndex = m_StringInfo.m_QuadBufferContainerIndex = m_StringInfo.m_SelectionQuadContainerIndex = -1; m_StringInfo.m_vCharacterQuads.clear(); m_AlignedStartX = m_AlignedStartY = m_X = m_Y = 0.0f; m_Flags = m_LineCount = m_CharCount = m_GlyphCount = 0; m_MaxLines = -1; m_LineWidth = -1.0f; m_RenderFlags = 0; m_HasCursor = false; m_ForceCursorRendering = false; m_HasSelection = false; m_SingleTimeUse = false; m_BoundingBox = {0.0f, 0.0f, 0.0f, 0.0f}; m_aDebugText[0] = '\0'; m_ContainerIndex = STextContainerIndex{}; } }; struct SFontLanguageVariant { char m_aLanguageFile[IO_MAX_PATH_LENGTH]; char m_aFamilyName[FONT_NAME_SIZE]; }; class CTextRender : public IEngineTextRender { IConsole *m_pConsole; IGraphics *m_pGraphics; IStorage *m_pStorage; IConsole *Console() { return m_pConsole; } IGraphics *Graphics() { return m_pGraphics; } IStorage *Storage() { return m_pStorage; } CGlyphMap *m_pGlyphMap; std::vector m_vpFontData; std::vector m_vVariants; unsigned m_RenderFlags; ColorRGBA m_Color; ColorRGBA m_OutlineColor; ColorRGBA m_SelectionColor; FT_Library m_FTLibrary; std::vector m_vpTextContainers; std::vector m_vTextContainerIndices; int m_FirstFreeTextContainerIndex; SBufferContainerInfo m_DefaultTextContainerInfo; std::chrono::nanoseconds m_CursorRenderTime; int GetFreeTextContainerIndex() { if(m_FirstFreeTextContainerIndex == -1) { const int Index = (int)m_vTextContainerIndices.size(); m_vTextContainerIndices.push_back(Index); return Index; } else { const int Index = m_FirstFreeTextContainerIndex; m_FirstFreeTextContainerIndex = m_vTextContainerIndices[Index]; m_vTextContainerIndices[Index] = Index; return Index; } } void FreeTextContainerIndex(STextContainerIndex &Index) { m_vTextContainerIndices[Index.m_Index] = m_FirstFreeTextContainerIndex; m_FirstFreeTextContainerIndex = Index.m_Index; Index.Reset(); } void FreeTextContainer(STextContainerIndex &Index) { m_vpTextContainers[Index.m_Index]->Reset(); FreeTextContainerIndex(Index); } STextContainer &GetTextContainer(const STextContainerIndex &Index) { dbg_assert(Index.Valid(), "Text container index was invalid."); if(Index.m_Index >= (int)m_vpTextContainers.size()) { for(int i = 0; i < Index.m_Index + 1 - (int)m_vpTextContainers.size(); ++i) m_vpTextContainers.push_back(new STextContainer()); } if(m_vpTextContainers[Index.m_Index]->m_ContainerIndex.m_UseCount.get() != Index.m_UseCount.get()) { m_vpTextContainers[Index.m_Index]->m_ContainerIndex = Index; } return *m_vpTextContainers[Index.m_Index]; } int WordLength(const char *pText) const { const char *pCursor = pText; while(true) { if(*pCursor == '\0') return pCursor - pText; if(*pCursor == '\n' || *pCursor == '\t' || *pCursor == ' ') return pCursor - pText + 1; str_utf8_decode(&pCursor); } } bool LoadFontCollection(const char *pFontName, const FT_Byte *pFontData, FT_Long FontDataSize) { FT_Face FtFace; FT_Error CollectionLoadError = FT_New_Memory_Face(m_FTLibrary, pFontData, FontDataSize, -1, &FtFace); if(CollectionLoadError) { char aBuf[256]; str_format(aBuf, sizeof(aBuf), "Failed to load font file '%s': %s", pFontName, FT_Error_String(CollectionLoadError)); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "textrender", aBuf); return false; } const FT_Long NumFaces = FtFace->num_faces; FT_Done_Face(FtFace); bool LoadedAny = false; for(FT_Long FaceIndex = 0; FaceIndex < NumFaces; ++FaceIndex) { FT_Error FaceLoadError = FT_New_Memory_Face(m_FTLibrary, pFontData, FontDataSize, FaceIndex, &FtFace); if(FaceLoadError) { char aBuf[256]; str_format(aBuf, sizeof(aBuf), "Failed to load font face %ld from font file '%s': %s", FaceIndex, pFontName, FT_Error_String(FaceLoadError)); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "textrender", aBuf); FT_Done_Face(FtFace); continue; } m_pGlyphMap->AddFace(FtFace); char aBuf[256]; str_format(aBuf, sizeof(aBuf), "Loaded font face %ld '%s %s' from font file '%s'", FaceIndex, FtFace->family_name, FtFace->style_name, pFontName); Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "textrender", aBuf); LoadedAny = true; } if(!LoadedAny) { char aBuf[256]; str_format(aBuf, sizeof(aBuf), "Failed to load font file '%s': no font faces could be loaded", pFontName); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "textrender", aBuf); return false; } return true; } void SetRenderFlags(unsigned Flags) override { m_RenderFlags = Flags; } unsigned GetRenderFlags() const override { return m_RenderFlags; } public: CTextRender() { m_pConsole = nullptr; m_pGraphics = nullptr; m_pStorage = nullptr; m_pGlyphMap = nullptr; m_Color = DefaultTextColor(); m_OutlineColor = DefaultTextOutlineColor(); m_SelectionColor = DefaultTextSelectionColor(); m_FTLibrary = nullptr; m_RenderFlags = 0; m_CursorRenderTime = time_get_nanoseconds(); } void Init() override { m_pConsole = Kernel()->RequestInterface(); m_pGraphics = Kernel()->RequestInterface(); m_pStorage = Kernel()->RequestInterface(); FT_Init_FreeType(&m_FTLibrary); m_pGlyphMap = new CGlyphMap(m_pGraphics); // print freetype version { int LMajor, LMinor, LPatch; FT_Library_Version(m_FTLibrary, &LMajor, &LMinor, &LPatch); char aFreetypeVersion[128]; str_format(aFreetypeVersion, sizeof(aFreetypeVersion), "Freetype version %d.%d.%d (compiled = %d.%d.%d)", LMajor, LMinor, LPatch, FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "textrender", aFreetypeVersion); } m_FirstFreeTextContainerIndex = -1; m_DefaultTextContainerInfo.m_Stride = sizeof(STextCharQuadVertex); m_DefaultTextContainerInfo.m_VertBufferBindingIndex = -1; m_DefaultTextContainerInfo.m_vAttributes.emplace_back(); SBufferContainerInfo::SAttribute *pAttr = &m_DefaultTextContainerInfo.m_vAttributes.back(); pAttr->m_DataTypeCount = 2; pAttr->m_FuncType = 0; pAttr->m_Normalized = false; pAttr->m_pOffset = nullptr; pAttr->m_Type = GRAPHICS_TYPE_FLOAT; m_DefaultTextContainerInfo.m_vAttributes.emplace_back(); pAttr = &m_DefaultTextContainerInfo.m_vAttributes.back(); pAttr->m_DataTypeCount = 2; pAttr->m_FuncType = 0; pAttr->m_Normalized = false; pAttr->m_pOffset = (void *)(sizeof(float) * 2); pAttr->m_Type = GRAPHICS_TYPE_FLOAT; m_DefaultTextContainerInfo.m_vAttributes.emplace_back(); pAttr = &m_DefaultTextContainerInfo.m_vAttributes.back(); pAttr->m_DataTypeCount = 4; pAttr->m_FuncType = 0; pAttr->m_Normalized = true; pAttr->m_pOffset = (void *)(sizeof(float) * 2 + sizeof(float) * 2); pAttr->m_Type = GRAPHICS_TYPE_UNSIGNED_BYTE; } void Shutdown() override { for(auto *pTextCont : m_vpTextContainers) delete pTextCont; m_vpTextContainers.clear(); delete m_pGlyphMap; m_pGlyphMap = nullptr; if(m_FTLibrary != nullptr) FT_Done_FreeType(m_FTLibrary); m_FTLibrary = nullptr; for(auto *pFontData : m_vpFontData) free(pFontData); m_vpFontData.clear(); m_DefaultTextContainerInfo.m_vAttributes.clear(); m_pConsole = nullptr; m_pGraphics = nullptr; m_pStorage = nullptr; } void LoadFonts() override { // read file data into buffer const char *pFilename = "fonts/index.json"; void *pFileData; unsigned JsonFileSize; if(!Storage()->ReadFile(pFilename, IStorage::TYPE_ALL, &pFileData, &JsonFileSize)) { char aBuf[256]; str_format(aBuf, sizeof(aBuf), "Failed to open/read font index file '%s'", pFilename); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "textrender", aBuf); return; } // parse json data json_settings JsonSettings{}; char aError[256]; json_value *pJsonData = json_parse_ex(&JsonSettings, static_cast(pFileData), JsonFileSize, aError); free(pFileData); if(pJsonData == nullptr) { char aBuf[512]; str_format(aBuf, sizeof(aBuf), "Failed to parse font index file '%s': %s", pFilename, aError); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "textrender", aBuf); return; } // extract font file definitions const json_value &FontFiles = (*pJsonData)["font files"]; if(FontFiles.type == json_array) { for(unsigned FontFileIndex = 0; FontFileIndex < FontFiles.u.array.length; ++FontFileIndex) { if(FontFiles[FontFileIndex].type != json_string) continue; char aFontName[IO_MAX_PATH_LENGTH]; str_format(aFontName, sizeof(aFontName), "fonts/%s", FontFiles[FontFileIndex].u.string.ptr); void *pFontData; unsigned FontDataSize; if(Storage()->ReadFile(aFontName, IStorage::TYPE_ALL, &pFontData, &FontDataSize)) { if(LoadFontCollection(aFontName, static_cast(pFontData), (FT_Long)FontDataSize)) { m_vpFontData.push_back(pFontData); } else { free(pFontData); } } } } // extract default family name const json_value &DefaultFace = (*pJsonData)["default"]; if(DefaultFace.type == json_string) { m_pGlyphMap->SetDefaultFaceByName(DefaultFace.u.string.ptr); } // extract language variant family names const json_value &Variants = (*pJsonData)["language variants"]; if(Variants.type == json_object) { m_vVariants.resize(Variants.u.object.length); for(size_t i = 0; i < Variants.u.object.length; ++i) { str_format(m_vVariants[i].m_aLanguageFile, sizeof(m_vVariants[i].m_aLanguageFile), "languages/%s.txt", Variants.u.object.values[i].name); const json_value *pFamilyName = Variants.u.object.values[i].value; if(pFamilyName->type == json_string) str_copy(m_vVariants[i].m_aFamilyName, pFamilyName->u.string.ptr); else m_vVariants[i].m_aFamilyName[0] = '\0'; } } // extract fallback family names const json_value &FallbackFaces = (*pJsonData)["fallbacks"]; if(FallbackFaces.type == json_array) { for(unsigned i = 0; i < FallbackFaces.u.array.length; ++i) { if(FallbackFaces[i].type == json_string) { m_pGlyphMap->AddFallbackFaceByName(FallbackFaces[i].u.string.ptr); } } } // extract icon font family name const json_value &IconFace = (*pJsonData)["icon"]; if(IconFace.type == json_string) { m_pGlyphMap->SetIconFaceByName(IconFace.u.string.ptr); } json_value_free(pJsonData); } void SetFontPreset(EFontPreset FontPreset) override { m_pGlyphMap->SetFontPreset(FontPreset); } void SetFontLanguageVariant(const char *pLanguageFile) override { for(const auto &Variant : m_vVariants) { if(str_comp(pLanguageFile, Variant.m_aLanguageFile) == 0) { m_pGlyphMap->SetVariantFaceByName(Variant.m_aFamilyName); return; } } m_pGlyphMap->SetVariantFaceByName(nullptr); } void SetCursor(CTextCursor *pCursor, float x, float y, float FontSize, int Flags) const override { pCursor->m_Flags = Flags; pCursor->m_LineCount = 1; pCursor->m_GlyphCount = 0; pCursor->m_CharCount = 0; pCursor->m_MaxLines = 0; pCursor->m_LineSpacing = 0; pCursor->m_AlignedLineSpacing = 0; pCursor->m_StartX = x; pCursor->m_StartY = y; pCursor->m_LineWidth = -1.0f; pCursor->m_X = x; pCursor->m_Y = y; pCursor->m_MaxCharacterHeight = 0.0f; pCursor->m_LongestLineWidth = 0.0f; pCursor->m_FontSize = FontSize; pCursor->m_AlignedFontSize = FontSize; pCursor->m_CalculateSelectionMode = TEXT_CURSOR_SELECTION_MODE_NONE; pCursor->m_SelectionHeightFactor = 1.0f; pCursor->m_PressMouse = vec2(0.0f, 0.0f); pCursor->m_ReleaseMouse = vec2(0.0f, 0.0f); pCursor->m_SelectionStart = 0; pCursor->m_SelectionEnd = 0; pCursor->m_CursorMode = TEXT_CURSOR_CURSOR_MODE_NONE; pCursor->m_ForceCursorRendering = false; pCursor->m_CursorCharacter = -1; pCursor->m_CursorRenderedPosition = vec2(-1.0f, -1.0f); pCursor->m_vColorSplits = {}; } void MoveCursor(CTextCursor *pCursor, float x, float y) const override { pCursor->m_X += x; pCursor->m_Y += y; } void SetCursorPosition(CTextCursor *pCursor, float x, float y) const override { pCursor->m_X = x; pCursor->m_Y = y; } void Text(float x, float y, float Size, const char *pText, float LineWidth = -1.0f) override { CTextCursor Cursor; SetCursor(&Cursor, x, y, Size, TEXTFLAG_RENDER); Cursor.m_LineWidth = LineWidth; TextEx(&Cursor, pText, -1); } float TextWidth(float Size, const char *pText, int StrLength = -1, float LineWidth = -1.0f, int Flags = 0, const STextSizeProperties &TextSizeProps = {}) override { CTextCursor Cursor; SetCursor(&Cursor, 0, 0, Size, Flags); Cursor.m_LineWidth = LineWidth; TextEx(&Cursor, pText, StrLength); if(TextSizeProps.m_pHeight != nullptr) *TextSizeProps.m_pHeight = Cursor.Height(); if(TextSizeProps.m_pAlignedFontSize != nullptr) *TextSizeProps.m_pAlignedFontSize = Cursor.m_AlignedFontSize; if(TextSizeProps.m_pMaxCharacterHeightInLine != nullptr) *TextSizeProps.m_pMaxCharacterHeightInLine = Cursor.m_MaxCharacterHeight; if(TextSizeProps.m_pLineCount != nullptr) *TextSizeProps.m_pLineCount = Cursor.m_LineCount; return Cursor.m_LongestLineWidth; } STextBoundingBox TextBoundingBox(float Size, const char *pText, int StrLength = -1, float LineWidth = -1.0f, float LineSpacing = 0.0f, int Flags = 0) override { CTextCursor Cursor; SetCursor(&Cursor, 0, 0, Size, Flags); Cursor.m_LineWidth = LineWidth; Cursor.m_LineSpacing = LineSpacing; TextEx(&Cursor, pText, StrLength); return Cursor.BoundingBox(); } void TextColor(float r, float g, float b, float a) override { m_Color.r = r; m_Color.g = g; m_Color.b = b; m_Color.a = a; } void TextColor(ColorRGBA rgb) override { m_Color = rgb; } void TextOutlineColor(float r, float g, float b, float a) override { m_OutlineColor.r = r; m_OutlineColor.g = g; m_OutlineColor.b = b; m_OutlineColor.a = a; } void TextOutlineColor(ColorRGBA rgb) override { m_OutlineColor = rgb; } void TextSelectionColor(float r, float g, float b, float a) override { m_SelectionColor.r = r; m_SelectionColor.g = g; m_SelectionColor.b = b; m_SelectionColor.a = a; } void TextSelectionColor(ColorRGBA rgb) override { m_SelectionColor = rgb; } ColorRGBA GetTextColor() const override { return m_Color; } ColorRGBA GetTextOutlineColor() const override { return m_OutlineColor; } ColorRGBA GetTextSelectionColor() const override { return m_SelectionColor; } void TextEx(CTextCursor *pCursor, const char *pText, int Length = -1) override { const unsigned OldRenderFlags = m_RenderFlags; m_RenderFlags |= TEXT_RENDER_FLAG_ONE_TIME_USE; STextContainerIndex TextCont; CreateTextContainer(TextCont, pCursor, pText, Length); m_RenderFlags = OldRenderFlags; if(TextCont.Valid()) { if((pCursor->m_Flags & TEXTFLAG_RENDER) != 0) { ColorRGBA TextColor = DefaultTextColor(); ColorRGBA TextColorOutline = DefaultTextOutlineColor(); RenderTextContainer(TextCont, TextColor, TextColorOutline); } DeleteTextContainer(TextCont); } } bool CreateTextContainer(STextContainerIndex &TextContainerIndex, CTextCursor *pCursor, const char *pText, int Length = -1) override { dbg_assert(!TextContainerIndex.Valid(), "Text container index was not cleared."); TextContainerIndex.Reset(); TextContainerIndex.m_Index = GetFreeTextContainerIndex(); float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); STextContainer &TextContainer = GetTextContainer(TextContainerIndex); TextContainer.m_SingleTimeUse = (m_RenderFlags & TEXT_RENDER_FLAG_ONE_TIME_USE) != 0; const vec2 FakeToScreen = vec2(Graphics()->ScreenWidth() / (ScreenX1 - ScreenX0), Graphics()->ScreenHeight() / (ScreenY1 - ScreenY0)); TextContainer.m_AlignedStartX = round_to_int(pCursor->m_X * FakeToScreen.x) / FakeToScreen.x; TextContainer.m_AlignedStartY = round_to_int(pCursor->m_Y * FakeToScreen.y) / FakeToScreen.y; TextContainer.m_X = pCursor->m_X; TextContainer.m_Y = pCursor->m_Y; TextContainer.m_Flags = pCursor->m_Flags; if(pCursor->m_LineWidth <= 0) TextContainer.m_RenderFlags = m_RenderFlags | ETextRenderFlags::TEXT_RENDER_FLAG_NO_FIRST_CHARACTER_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_LAST_CHARACTER_ADVANCE; else TextContainer.m_RenderFlags = m_RenderFlags; AppendTextContainer(TextContainerIndex, pCursor, pText, Length); const bool IsRendered = (pCursor->m_Flags & TEXTFLAG_RENDER) != 0; if(TextContainer.m_StringInfo.m_vCharacterQuads.empty() && TextContainer.m_StringInfo.m_SelectionQuadContainerIndex == -1 && IsRendered) { FreeTextContainer(TextContainerIndex); return false; } else { if(Graphics()->IsTextBufferingEnabled() && IsRendered && !TextContainer.m_StringInfo.m_vCharacterQuads.empty()) { if((TextContainer.m_RenderFlags & TEXT_RENDER_FLAG_NO_AUTOMATIC_QUAD_UPLOAD) == 0) { UploadTextContainer(TextContainerIndex); } } TextContainer.m_LineCount = pCursor->m_LineCount; TextContainer.m_GlyphCount = pCursor->m_GlyphCount; TextContainer.m_CharCount = pCursor->m_CharCount; TextContainer.m_MaxLines = pCursor->m_MaxLines; TextContainer.m_LineWidth = pCursor->m_LineWidth; return true; } } void AppendTextContainer(STextContainerIndex TextContainerIndex, CTextCursor *pCursor, const char *pText, int Length = -1) override { STextContainer &TextContainer = GetTextContainer(TextContainerIndex); str_append(TextContainer.m_aDebugText, pText); float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); const vec2 FakeToScreen = vec2(Graphics()->ScreenWidth() / (ScreenX1 - ScreenX0), Graphics()->ScreenHeight() / (ScreenY1 - ScreenY0)); const float CursorX = round_to_int(pCursor->m_X * FakeToScreen.x) / FakeToScreen.x; const float CursorY = round_to_int(pCursor->m_Y * FakeToScreen.y) / FakeToScreen.y; const int ActualSize = round_truncate(pCursor->m_FontSize * FakeToScreen.y); pCursor->m_AlignedFontSize = ActualSize / FakeToScreen.y; pCursor->m_AlignedLineSpacing = round_truncate(pCursor->m_LineSpacing * FakeToScreen.y) / FakeToScreen.y; // string length if(Length < 0) Length = str_length(pText); else Length = minimum(Length, str_length(pText)); const char *pCurrent = pText; const char *pEnd = pCurrent + Length; const char *pEllipsis = "…"; const SGlyph *pEllipsisGlyph = nullptr; if(pCursor->m_Flags & TEXTFLAG_ELLIPSIS_AT_END) { if(pCursor->m_LineWidth != -1 && pCursor->m_LineWidth < TextWidth(pCursor->m_FontSize, pText, -1, -1.0f)) { pEllipsisGlyph = m_pGlyphMap->GetGlyph(0x2026, ActualSize); // … if(pEllipsisGlyph == nullptr) { // no ellipsis char in font, just stop at end instead pCursor->m_Flags &= ~TEXTFLAG_ELLIPSIS_AT_END; pCursor->m_Flags |= TEXTFLAG_STOP_AT_END; } } } const unsigned RenderFlags = TextContainer.m_RenderFlags; float DrawX = 0.0f, DrawY = 0.0f; if((RenderFlags & TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT) != 0) { DrawX = pCursor->m_X; DrawY = pCursor->m_Y; } else { DrawX = CursorX; DrawY = CursorY; } int LineCount = pCursor->m_LineCount; const bool IsRendered = (pCursor->m_Flags & TEXTFLAG_RENDER) != 0; const float CursorInnerWidth = (((ScreenX1 - ScreenX0) / Graphics()->ScreenWidth())) * 2; const float CursorOuterWidth = CursorInnerWidth * 2; const float CursorOuterInnerDiff = (CursorOuterWidth - CursorInnerWidth) / 2; std::vector vSelectionQuads; int SelectionQuadLine = -1; bool SelectionStarted = false; bool SelectionUsedPress = false; bool SelectionUsedRelease = false; int SelectionStartChar = -1; int SelectionEndChar = -1; const auto &&CheckInsideChar = [&](bool CheckOuter, vec2 CursorPos, float LastCharX, float LastCharWidth, float CharX, float CharWidth, float CharY) -> bool { return (LastCharX - LastCharWidth / 2 <= CursorPos.x && CharX + CharWidth / 2 > CursorPos.x && CursorPos.y >= CharY - pCursor->m_AlignedFontSize && CursorPos.y < CharY + pCursor->m_AlignedLineSpacing) || (CheckOuter && CursorPos.y <= CharY - pCursor->m_AlignedFontSize); }; const auto &&CheckSelectionStart = [&](bool CheckOuter, vec2 CursorPos, int &SelectionChar, bool &SelectionUsedCase, float LastCharX, float LastCharWidth, float CharX, float CharWidth, float CharY) { if(!SelectionStarted && !SelectionUsedCase && CheckInsideChar(CheckOuter, CursorPos, LastCharX, LastCharWidth, CharX, CharWidth, CharY)) { SelectionChar = pCursor->m_GlyphCount; SelectionStarted = !SelectionStarted; SelectionUsedCase = true; } }; const auto &&CheckOutsideChar = [&](bool CheckOuter, vec2 CursorPos, float CharX, float CharWidth, float CharY) -> bool { return (CharX + CharWidth / 2 > CursorPos.x && CursorPos.y >= CharY - pCursor->m_AlignedFontSize && CursorPos.y < CharY + pCursor->m_AlignedLineSpacing) || (CheckOuter && CursorPos.y >= CharY + pCursor->m_AlignedLineSpacing); }; const auto &&CheckSelectionEnd = [&](bool CheckOuter, vec2 CursorPos, int &SelectionChar, bool &SelectionUsedCase, float CharX, float CharWidth, float CharY) { if(SelectionStarted && !SelectionUsedCase && CheckOutsideChar(CheckOuter, CursorPos, CharX, CharWidth, CharY)) { SelectionChar = pCursor->m_GlyphCount; SelectionStarted = !SelectionStarted; SelectionUsedCase = true; } }; float LastSelX = DrawX; float LastSelWidth = 0; float LastCharX = DrawX; float LastCharWidth = 0; // Returns true if line was started const auto &&StartNewLine = [&]() { if(pCursor->m_MaxLines > 0 && LineCount >= pCursor->m_MaxLines) return false; DrawX = pCursor->m_StartX; DrawY += pCursor->m_AlignedFontSize + pCursor->m_AlignedLineSpacing; if((RenderFlags & TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT) == 0) { DrawX = round_to_int(DrawX * FakeToScreen.x) / FakeToScreen.x; // realign DrawY = round_to_int(DrawY * FakeToScreen.y) / FakeToScreen.y; } LastSelX = DrawX; LastSelWidth = 0; LastCharX = DrawX; LastCharWidth = 0; ++LineCount; return true; }; if(pCursor->m_CalculateSelectionMode != TEXT_CURSOR_SELECTION_MODE_NONE || pCursor->m_CursorMode != TEXT_CURSOR_CURSOR_MODE_NONE) { if(IsRendered) Graphics()->QuadContainerReset(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex); // if in calculate mode, also calculate the cursor if(pCursor->m_CursorMode == TEXT_CURSOR_CURSOR_MODE_CALCULATE) pCursor->m_CursorCharacter = -1; } IGraphics::CQuadItem aCursorQuads[2]; bool HasCursor = false; const SGlyph *pLastGlyph = nullptr; bool GotNewLineLast = false; int ColorOption = 0; while(pCurrent < pEnd && pCurrent != pEllipsis) { bool NewLine = false; const char *pBatchEnd = pEnd; if(pCursor->m_LineWidth > 0 && !(pCursor->m_Flags & TEXTFLAG_STOP_AT_END) && !(pCursor->m_Flags & TEXTFLAG_ELLIPSIS_AT_END)) { int Wlen = minimum(WordLength(pCurrent), (int)(pEnd - pCurrent)); CTextCursor Compare = *pCursor; Compare.m_CalculateSelectionMode = TEXT_CURSOR_SELECTION_MODE_NONE; Compare.m_CursorMode = TEXT_CURSOR_CURSOR_MODE_NONE; Compare.m_X = DrawX; Compare.m_Y = DrawY; Compare.m_Flags &= ~TEXTFLAG_RENDER; Compare.m_Flags |= TEXTFLAG_DISALLOW_NEWLINE; Compare.m_LineWidth = -1; TextEx(&Compare, pCurrent, Wlen); if(Compare.m_X - DrawX > pCursor->m_LineWidth) { // word can't be fitted in one line, cut it CTextCursor Cutter = *pCursor; Cutter.m_CalculateSelectionMode = TEXT_CURSOR_SELECTION_MODE_NONE; Cutter.m_CursorMode = TEXT_CURSOR_CURSOR_MODE_NONE; Cutter.m_GlyphCount = 0; Cutter.m_CharCount = 0; Cutter.m_X = DrawX; Cutter.m_Y = DrawY; Cutter.m_Flags &= ~TEXTFLAG_RENDER; Cutter.m_Flags |= TEXTFLAG_STOP_AT_END | TEXTFLAG_DISALLOW_NEWLINE; TextEx(&Cutter, pCurrent, Wlen); Wlen = str_utf8_rewind(pCurrent, Cutter.m_CharCount); // rewind once to skip the last character that did not fit NewLine = true; if(Cutter.m_GlyphCount <= 3 && !GotNewLineLast) // if we can't place 3 chars of the word on this line, take the next Wlen = 0; } else if(Compare.m_X - pCursor->m_StartX > pCursor->m_LineWidth && !GotNewLineLast) { NewLine = true; Wlen = 0; } pBatchEnd = pCurrent + Wlen; } const char *pTmp = pCurrent; int NextCharacter = str_utf8_decode(&pTmp); while(pCurrent < pBatchEnd && pCurrent != pEllipsis) { const int PrevCharCount = pCursor->m_CharCount; pCursor->m_CharCount += pTmp - pCurrent; pCurrent = pTmp; int Character = NextCharacter; NextCharacter = str_utf8_decode(&pTmp); if(Character == '\n') { if((pCursor->m_Flags & TEXTFLAG_DISALLOW_NEWLINE) == 0) { pLastGlyph = nullptr; if(!StartNewLine()) break; continue; } else { Character = ' '; } } const SGlyph *pGlyph = m_pGlyphMap->GetGlyph(Character, ActualSize); if(pGlyph) { const float Scale = 1.0f / pGlyph->m_FontSize; const bool ApplyBearingX = !(((RenderFlags & TEXT_RENDER_FLAG_NO_X_BEARING) != 0) || (pCursor->m_GlyphCount == 0 && (RenderFlags & TEXT_RENDER_FLAG_NO_FIRST_CHARACTER_X_BEARING) != 0)); const float Advance = ((((RenderFlags & TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH) != 0) ? (pGlyph->m_Width) : (pGlyph->m_AdvanceX + ((!ApplyBearingX) ? (-pGlyph->m_OffsetX) : 0.f)))) * Scale * pCursor->m_AlignedFontSize; const float OutLineRealDiff = (pGlyph->m_Width - pGlyph->m_CharWidth) * Scale * pCursor->m_AlignedFontSize; float CharKerning = 0.0f; if((RenderFlags & TEXT_RENDER_FLAG_KERNING) != 0) CharKerning = m_pGlyphMap->Kerning(pLastGlyph, pGlyph).x * Scale * pCursor->m_AlignedFontSize; pLastGlyph = pGlyph; if(pEllipsisGlyph != nullptr && pCursor->m_Flags & TEXTFLAG_ELLIPSIS_AT_END && pCurrent < pBatchEnd && pCurrent != pEllipsis) { float AdvanceEllipsis = ((((RenderFlags & TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH) != 0) ? (pEllipsisGlyph->m_Width) : (pEllipsisGlyph->m_AdvanceX + ((!ApplyBearingX) ? (-pEllipsisGlyph->m_OffsetX) : 0.f)))) * Scale * pCursor->m_AlignedFontSize; float CharKerningEllipsis = 0.0f; if((RenderFlags & TEXT_RENDER_FLAG_KERNING) != 0) { CharKerningEllipsis = m_pGlyphMap->Kerning(pGlyph, pEllipsisGlyph).x * Scale * pCursor->m_AlignedFontSize; } if(DrawX + CharKerning + Advance + CharKerningEllipsis + AdvanceEllipsis - pCursor->m_StartX > pCursor->m_LineWidth) { // we hit the end, only render ellipsis and finish pTmp = pEllipsis; NextCharacter = 0x2026; continue; } } if(pCursor->m_Flags & TEXTFLAG_STOP_AT_END && (DrawX + CharKerning) + Advance - pCursor->m_StartX > pCursor->m_LineWidth) { // we hit the end of the line, no more to render or count pCurrent = pEnd; break; } float BearingX = (!ApplyBearingX ? 0.f : pGlyph->m_OffsetX) * Scale * pCursor->m_AlignedFontSize; float CharWidth = pGlyph->m_Width * Scale * pCursor->m_AlignedFontSize; float BearingY = (((RenderFlags & TEXT_RENDER_FLAG_NO_Y_BEARING) != 0) ? 0.f : (pGlyph->m_OffsetY * Scale * pCursor->m_AlignedFontSize)); float CharHeight = pGlyph->m_Height * Scale * pCursor->m_AlignedFontSize; if((RenderFlags & TEXT_RENDER_FLAG_NO_OVERSIZE) != 0) { if(CharHeight + BearingY > pCursor->m_AlignedFontSize) { BearingY = 0; float ScaleChar = (CharHeight + BearingY) / pCursor->m_AlignedFontSize; CharHeight = pCursor->m_AlignedFontSize; CharWidth /= ScaleChar; } } const float TmpY = (DrawY + pCursor->m_AlignedFontSize); const float CharX = (DrawX + CharKerning) + BearingX; const float CharY = TmpY - BearingY; // Check if we have any color split ColorRGBA Color = m_Color; if(ColorOption < (int)pCursor->m_vColorSplits.size()) { STextColorSplit &Split = pCursor->m_vColorSplits.at(ColorOption); if(PrevCharCount >= Split.m_CharIndex && (Split.m_Length == -1 || PrevCharCount < Split.m_CharIndex + Split.m_Length)) Color = Split.m_Color; if(Split.m_Length != -1 && PrevCharCount >= (Split.m_CharIndex + Split.m_Length - 1)) { ColorOption++; if(ColorOption < (int)pCursor->m_vColorSplits.size()) { // Handle splits that are Split = pCursor->m_vColorSplits.at(ColorOption); if(PrevCharCount >= Split.m_CharIndex) Color = Split.m_Color; } } } // don't add text that isn't drawn, the color overwrite is used for that if(Color.a != 0.f && IsRendered) { TextContainer.m_StringInfo.m_vCharacterQuads.emplace_back(); STextCharQuad &TextCharQuad = TextContainer.m_StringInfo.m_vCharacterQuads.back(); TextCharQuad.m_aVertices[0].m_X = CharX; TextCharQuad.m_aVertices[0].m_Y = CharY; TextCharQuad.m_aVertices[0].m_U = pGlyph->m_aUVs[0]; TextCharQuad.m_aVertices[0].m_V = pGlyph->m_aUVs[3]; TextCharQuad.m_aVertices[0].m_Color.r = (unsigned char)(Color.r * 255.f); TextCharQuad.m_aVertices[0].m_Color.g = (unsigned char)(Color.g * 255.f); TextCharQuad.m_aVertices[0].m_Color.b = (unsigned char)(Color.b * 255.f); TextCharQuad.m_aVertices[0].m_Color.a = (unsigned char)(Color.a * 255.f); TextCharQuad.m_aVertices[1].m_X = CharX + CharWidth; TextCharQuad.m_aVertices[1].m_Y = CharY; TextCharQuad.m_aVertices[1].m_U = pGlyph->m_aUVs[2]; TextCharQuad.m_aVertices[1].m_V = pGlyph->m_aUVs[3]; TextCharQuad.m_aVertices[1].m_Color.r = (unsigned char)(Color.r * 255.f); TextCharQuad.m_aVertices[1].m_Color.g = (unsigned char)(Color.g * 255.f); TextCharQuad.m_aVertices[1].m_Color.b = (unsigned char)(Color.b * 255.f); TextCharQuad.m_aVertices[1].m_Color.a = (unsigned char)(Color.a * 255.f); TextCharQuad.m_aVertices[2].m_X = CharX + CharWidth; TextCharQuad.m_aVertices[2].m_Y = CharY - CharHeight; TextCharQuad.m_aVertices[2].m_U = pGlyph->m_aUVs[2]; TextCharQuad.m_aVertices[2].m_V = pGlyph->m_aUVs[1]; TextCharQuad.m_aVertices[2].m_Color.r = (unsigned char)(Color.r * 255.f); TextCharQuad.m_aVertices[2].m_Color.g = (unsigned char)(Color.g * 255.f); TextCharQuad.m_aVertices[2].m_Color.b = (unsigned char)(Color.b * 255.f); TextCharQuad.m_aVertices[2].m_Color.a = (unsigned char)(Color.a * 255.f); TextCharQuad.m_aVertices[3].m_X = CharX; TextCharQuad.m_aVertices[3].m_Y = CharY - CharHeight; TextCharQuad.m_aVertices[3].m_U = pGlyph->m_aUVs[0]; TextCharQuad.m_aVertices[3].m_V = pGlyph->m_aUVs[1]; TextCharQuad.m_aVertices[3].m_Color.r = (unsigned char)(Color.r * 255.f); TextCharQuad.m_aVertices[3].m_Color.g = (unsigned char)(Color.g * 255.f); TextCharQuad.m_aVertices[3].m_Color.b = (unsigned char)(Color.b * 255.f); TextCharQuad.m_aVertices[3].m_Color.a = (unsigned char)(Color.a * 255.f); } // calculate the full width from the last selection point to the end of this selection draw on screen const float SelWidth = (CharX + maximum(Advance, CharWidth - OutLineRealDiff / 2)) - (LastSelX + LastSelWidth); const float SelX = (LastSelX + LastSelWidth); if(pCursor->m_CursorMode == TEXT_CURSOR_CURSOR_MODE_CALCULATE) { if(pCursor->m_CursorCharacter == -1 && CheckInsideChar(pCursor->m_GlyphCount == 0, pCursor->m_ReleaseMouse, pCursor->m_GlyphCount == 0 ? std::numeric_limits::lowest() : LastCharX, LastCharWidth, CharX, CharWidth, TmpY)) { pCursor->m_CursorCharacter = pCursor->m_GlyphCount; } } if(pCursor->m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_CALCULATE) { if(pCursor->m_GlyphCount == 0) { CheckSelectionStart(true, pCursor->m_PressMouse, SelectionStartChar, SelectionUsedPress, std::numeric_limits::lowest(), 0, CharX, CharWidth, TmpY); CheckSelectionStart(true, pCursor->m_ReleaseMouse, SelectionEndChar, SelectionUsedRelease, std::numeric_limits::lowest(), 0, CharX, CharWidth, TmpY); } // if selection didn't start and the mouse pos is at least on 50% of the right side of the character start CheckSelectionStart(false, pCursor->m_PressMouse, SelectionStartChar, SelectionUsedPress, LastCharX, LastCharWidth, CharX, CharWidth, TmpY); CheckSelectionStart(false, pCursor->m_ReleaseMouse, SelectionEndChar, SelectionUsedRelease, LastCharX, LastCharWidth, CharX, CharWidth, TmpY); CheckSelectionEnd(false, pCursor->m_ReleaseMouse, SelectionEndChar, SelectionUsedRelease, CharX, CharWidth, TmpY); CheckSelectionEnd(false, pCursor->m_PressMouse, SelectionStartChar, SelectionUsedPress, CharX, CharWidth, TmpY); } if(pCursor->m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_SET) { if(pCursor->m_GlyphCount == pCursor->m_SelectionStart) { SelectionStarted = !SelectionStarted; SelectionStartChar = pCursor->m_GlyphCount; SelectionUsedPress = true; } if(pCursor->m_GlyphCount == pCursor->m_SelectionEnd) { SelectionStarted = !SelectionStarted; SelectionEndChar = pCursor->m_GlyphCount; SelectionUsedRelease = true; } } if(pCursor->m_CursorMode != TEXT_CURSOR_CURSOR_MODE_NONE) { if(pCursor->m_GlyphCount == pCursor->m_CursorCharacter) { HasCursor = true; aCursorQuads[0] = IGraphics::CQuadItem(SelX - CursorOuterInnerDiff, DrawY, CursorOuterWidth, pCursor->m_AlignedFontSize); aCursorQuads[1] = IGraphics::CQuadItem(SelX, DrawY + CursorOuterInnerDiff, CursorInnerWidth, pCursor->m_AlignedFontSize - CursorOuterInnerDiff * 2); pCursor->m_CursorRenderedPosition = vec2(SelX, DrawY); } } pCursor->m_MaxCharacterHeight = maximum(pCursor->m_MaxCharacterHeight, CharHeight + BearingY); if(NextCharacter == 0 && (RenderFlags & TEXT_RENDER_FLAG_NO_LAST_CHARACTER_ADVANCE) != 0 && Character != ' ') DrawX += BearingX + CharKerning + CharWidth; else DrawX += Advance + CharKerning; pCursor->m_GlyphCount++; if(SelectionStarted && IsRendered) { if(!vSelectionQuads.empty() && SelectionQuadLine == LineCount) { vSelectionQuads.back().m_Width += SelWidth; } else { const float SelectionHeight = pCursor->m_AlignedFontSize + pCursor->m_AlignedLineSpacing; const float SelectionY = DrawY + (1.0f - pCursor->m_SelectionHeightFactor) * SelectionHeight; const float ScaledSelectionHeight = pCursor->m_SelectionHeightFactor * SelectionHeight; vSelectionQuads.emplace_back(SelX, SelectionY, SelWidth, ScaledSelectionHeight); SelectionQuadLine = LineCount; } } LastSelX = SelX; LastSelWidth = SelWidth; LastCharX = CharX; LastCharWidth = CharWidth; } pCursor->m_LongestLineWidth = maximum(pCursor->m_LongestLineWidth, DrawX - pCursor->m_StartX); } if(NewLine) { if(!StartNewLine()) break; GotNewLineLast = true; } else GotNewLineLast = false; } if(!TextContainer.m_StringInfo.m_vCharacterQuads.empty() && IsRendered) { // setup the buffers if(Graphics()->IsTextBufferingEnabled()) { const size_t DataSize = TextContainer.m_StringInfo.m_vCharacterQuads.size() * sizeof(STextCharQuad); void *pUploadData = TextContainer.m_StringInfo.m_vCharacterQuads.data(); if(TextContainer.m_StringInfo.m_QuadBufferObjectIndex != -1 && (TextContainer.m_RenderFlags & TEXT_RENDER_FLAG_NO_AUTOMATIC_QUAD_UPLOAD) == 0) { Graphics()->RecreateBufferObject(TextContainer.m_StringInfo.m_QuadBufferObjectIndex, DataSize, pUploadData, TextContainer.m_SingleTimeUse ? IGraphics::EBufferObjectCreateFlags::BUFFER_OBJECT_CREATE_FLAGS_ONE_TIME_USE_BIT : 0); Graphics()->IndicesNumRequiredNotify(TextContainer.m_StringInfo.m_vCharacterQuads.size() * 6); } } } if(pCursor->m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_CALCULATE) { pCursor->m_SelectionStart = -1; pCursor->m_SelectionEnd = -1; if(SelectionStarted) { CheckSelectionEnd(true, pCursor->m_ReleaseMouse, SelectionEndChar, SelectionUsedRelease, std::numeric_limits::max(), 0, DrawY + pCursor->m_AlignedFontSize); CheckSelectionEnd(true, pCursor->m_PressMouse, SelectionStartChar, SelectionUsedPress, std::numeric_limits::max(), 0, DrawY + pCursor->m_AlignedFontSize); } } else if(pCursor->m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_SET) { if(pCursor->m_GlyphCount == pCursor->m_SelectionStart) { SelectionStarted = !SelectionStarted; SelectionStartChar = pCursor->m_GlyphCount; SelectionUsedPress = true; } if(pCursor->m_GlyphCount == pCursor->m_SelectionEnd) { SelectionStarted = !SelectionStarted; SelectionEndChar = pCursor->m_GlyphCount; SelectionUsedRelease = true; } } if(pCursor->m_CursorMode != TEXT_CURSOR_CURSOR_MODE_NONE) { if(pCursor->m_CursorMode == TEXT_CURSOR_CURSOR_MODE_CALCULATE && pCursor->m_CursorCharacter == -1 && CheckOutsideChar(true, pCursor->m_ReleaseMouse, std::numeric_limits::max(), 0, DrawY + pCursor->m_AlignedFontSize)) { pCursor->m_CursorCharacter = pCursor->m_GlyphCount; } if(pCursor->m_GlyphCount == pCursor->m_CursorCharacter) { HasCursor = true; aCursorQuads[0] = IGraphics::CQuadItem((LastSelX + LastSelWidth) - CursorOuterInnerDiff, DrawY, CursorOuterWidth, pCursor->m_AlignedFontSize); aCursorQuads[1] = IGraphics::CQuadItem((LastSelX + LastSelWidth), DrawY + CursorOuterInnerDiff, CursorInnerWidth, pCursor->m_AlignedFontSize - CursorOuterInnerDiff * 2); pCursor->m_CursorRenderedPosition = vec2(LastSelX + LastSelWidth, DrawY); } } const bool HasSelection = !vSelectionQuads.empty() && SelectionUsedPress && SelectionUsedRelease; if((HasSelection || HasCursor) && IsRendered) { Graphics()->SetColor(1.f, 1.f, 1.f, 1.f); if(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex == -1) TextContainer.m_StringInfo.m_SelectionQuadContainerIndex = Graphics()->CreateQuadContainer(false); if(HasCursor) Graphics()->QuadContainerAddQuads(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, aCursorQuads, std::size(aCursorQuads)); if(HasSelection) Graphics()->QuadContainerAddQuads(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, vSelectionQuads.data(), vSelectionQuads.size()); Graphics()->QuadContainerUpload(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex); TextContainer.m_HasCursor = HasCursor; TextContainer.m_HasSelection = HasSelection; TextContainer.m_ForceCursorRendering = pCursor->m_ForceCursorRendering; if(HasSelection) { pCursor->m_SelectionStart = SelectionStartChar; pCursor->m_SelectionEnd = SelectionEndChar; } else { pCursor->m_SelectionStart = -1; pCursor->m_SelectionEnd = -1; } } // even if no text is drawn the cursor position will be adjusted pCursor->m_X = DrawX; pCursor->m_Y = DrawY; pCursor->m_LineCount = LineCount; TextContainer.m_BoundingBox = pCursor->BoundingBox(); } bool CreateOrAppendTextContainer(STextContainerIndex &TextContainerIndex, CTextCursor *pCursor, const char *pText, int Length = -1) override { if(TextContainerIndex.Valid()) { AppendTextContainer(TextContainerIndex, pCursor, pText, Length); return true; } else { return CreateTextContainer(TextContainerIndex, pCursor, pText, Length); } } // just deletes and creates text container void RecreateTextContainer(STextContainerIndex &TextContainerIndex, CTextCursor *pCursor, const char *pText, int Length = -1) override { DeleteTextContainer(TextContainerIndex); CreateTextContainer(TextContainerIndex, pCursor, pText, Length); } void RecreateTextContainerSoft(STextContainerIndex &TextContainerIndex, CTextCursor *pCursor, const char *pText, int Length = -1) override { STextContainer &TextContainer = GetTextContainer(TextContainerIndex); TextContainer.m_StringInfo.m_vCharacterQuads.clear(); // the text buffer gets then recreated by the appended quads AppendTextContainer(TextContainerIndex, pCursor, pText, Length); } void DeleteTextContainer(STextContainerIndex &TextContainerIndex) override { if(!TextContainerIndex.Valid()) return; STextContainer &TextContainer = GetTextContainer(TextContainerIndex); if(Graphics()->IsTextBufferingEnabled()) Graphics()->DeleteBufferContainer(TextContainer.m_StringInfo.m_QuadBufferContainerIndex, true); Graphics()->DeleteQuadContainer(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex); FreeTextContainer(TextContainerIndex); } void UploadTextContainer(STextContainerIndex TextContainerIndex) override { if(Graphics()->IsTextBufferingEnabled()) { STextContainer &TextContainer = GetTextContainer(TextContainerIndex); size_t DataSize = TextContainer.m_StringInfo.m_vCharacterQuads.size() * sizeof(STextCharQuad); void *pUploadData = TextContainer.m_StringInfo.m_vCharacterQuads.data(); TextContainer.m_StringInfo.m_QuadBufferObjectIndex = Graphics()->CreateBufferObject(DataSize, pUploadData, TextContainer.m_SingleTimeUse ? IGraphics::EBufferObjectCreateFlags::BUFFER_OBJECT_CREATE_FLAGS_ONE_TIME_USE_BIT : 0); m_DefaultTextContainerInfo.m_VertBufferBindingIndex = TextContainer.m_StringInfo.m_QuadBufferObjectIndex; TextContainer.m_StringInfo.m_QuadBufferContainerIndex = Graphics()->CreateBufferContainer(&m_DefaultTextContainerInfo); Graphics()->IndicesNumRequiredNotify(TextContainer.m_StringInfo.m_vCharacterQuads.size() * 6); } } void RenderTextContainer(STextContainerIndex TextContainerIndex, const ColorRGBA &TextColor, const ColorRGBA &TextOutlineColor) override { const STextContainer &TextContainer = GetTextContainer(TextContainerIndex); if(!TextContainer.m_StringInfo.m_vCharacterQuads.empty()) { if(Graphics()->IsTextBufferingEnabled()) { Graphics()->TextureClear(); // render buffered text Graphics()->RenderText(TextContainer.m_StringInfo.m_QuadBufferContainerIndex, TextContainer.m_StringInfo.m_vCharacterQuads.size(), m_pGlyphMap->TextureDimension(), m_pGlyphMap->Texture(CGlyphMap::FONT_TEXTURE_FILL).Id(), m_pGlyphMap->Texture(CGlyphMap::FONT_TEXTURE_OUTLINE).Id(), TextColor, TextOutlineColor); } else { // render tiles const float UVScale = 1.0f / m_pGlyphMap->TextureDimension(); Graphics()->FlushVertices(); Graphics()->TextureSet(m_pGlyphMap->Texture(CGlyphMap::FONT_TEXTURE_OUTLINE)); Graphics()->QuadsBegin(); for(const STextCharQuad &TextCharQuad : TextContainer.m_StringInfo.m_vCharacterQuads) { Graphics()->SetColor(TextCharQuad.m_aVertices[0].m_Color.r / 255.f * TextOutlineColor.r, TextCharQuad.m_aVertices[0].m_Color.g / 255.f * TextOutlineColor.g, TextCharQuad.m_aVertices[0].m_Color.b / 255.f * TextOutlineColor.b, TextCharQuad.m_aVertices[0].m_Color.a / 255.f * TextOutlineColor.a); Graphics()->QuadsSetSubset(TextCharQuad.m_aVertices[0].m_U * UVScale, TextCharQuad.m_aVertices[0].m_V * UVScale, TextCharQuad.m_aVertices[2].m_U * UVScale, TextCharQuad.m_aVertices[2].m_V * UVScale); IGraphics::CQuadItem QuadItem(TextCharQuad.m_aVertices[0].m_X, TextCharQuad.m_aVertices[0].m_Y, TextCharQuad.m_aVertices[1].m_X - TextCharQuad.m_aVertices[0].m_X, TextCharQuad.m_aVertices[2].m_Y - TextCharQuad.m_aVertices[0].m_Y); Graphics()->QuadsDrawTL(&QuadItem, 1); } if(TextColor.a != 0) { Graphics()->QuadsEndKeepVertices(); Graphics()->TextureSet(m_pGlyphMap->Texture(CGlyphMap::FONT_TEXTURE_FILL)); int TextCharQuadIndex = 0; for(const STextCharQuad &TextCharQuad : TextContainer.m_StringInfo.m_vCharacterQuads) { unsigned char CR = (unsigned char)((float)(TextCharQuad.m_aVertices[0].m_Color.r) * TextColor.r); unsigned char CG = (unsigned char)((float)(TextCharQuad.m_aVertices[0].m_Color.g) * TextColor.g); unsigned char CB = (unsigned char)((float)(TextCharQuad.m_aVertices[0].m_Color.b) * TextColor.b); unsigned char CA = (unsigned char)((float)(TextCharQuad.m_aVertices[0].m_Color.a) * TextColor.a); Graphics()->ChangeColorOfQuadVertices(TextCharQuadIndex, CR, CG, CB, CA); ++TextCharQuadIndex; } // render non outlined Graphics()->QuadsDrawCurrentVertices(false); } else Graphics()->QuadsEnd(); // reset Graphics()->SetColor(1.f, 1.f, 1.f, 1.f); } } if(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex != -1) { if(TextContainer.m_HasSelection) { Graphics()->TextureClear(); Graphics()->SetColor(m_SelectionColor); Graphics()->RenderQuadContainerEx(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, TextContainer.m_HasCursor ? 2 : 0, -1, 0, 0); Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); } if(TextContainer.m_HasCursor) { const auto CurTime = time_get_nanoseconds(); Graphics()->TextureClear(); if(TextContainer.m_ForceCursorRendering || (CurTime - m_CursorRenderTime) > 500ms) { Graphics()->SetColor(TextOutlineColor); Graphics()->RenderQuadContainerEx(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, 0, 1, 0, 0); Graphics()->SetColor(TextColor); Graphics()->RenderQuadContainerEx(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, 1, 1, 0, 0); } if(TextContainer.m_ForceCursorRendering) m_CursorRenderTime = CurTime - 501ms; else if((CurTime - m_CursorRenderTime) > 1s) m_CursorRenderTime = time_get_nanoseconds(); Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); } } } void RenderTextContainer(STextContainerIndex TextContainerIndex, const ColorRGBA &TextColor, const ColorRGBA &TextOutlineColor, float X, float Y) override { STextContainer &TextContainer = GetTextContainer(TextContainerIndex); // remap the current screen, after render revert the change again float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); if((TextContainer.m_RenderFlags & TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT) == 0) { const vec2 FakeToScreen = vec2(Graphics()->ScreenWidth() / (ScreenX1 - ScreenX0), Graphics()->ScreenHeight() / (ScreenY1 - ScreenY0)); const float AlignedX = round_to_int((TextContainer.m_X + X) * FakeToScreen.x) / FakeToScreen.x; const float AlignedY = round_to_int((TextContainer.m_Y + Y) * FakeToScreen.y) / FakeToScreen.y; X = AlignedX - TextContainer.m_AlignedStartX; Y = AlignedY - TextContainer.m_AlignedStartY; } TextContainer.m_BoundingBox.m_X = X; TextContainer.m_BoundingBox.m_Y = Y; Graphics()->MapScreen(ScreenX0 - X, ScreenY0 - Y, ScreenX1 - X, ScreenY1 - Y); RenderTextContainer(TextContainerIndex, TextColor, TextOutlineColor); Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } STextBoundingBox GetBoundingBoxTextContainer(STextContainerIndex TextContainerIndex) override { const STextContainer &TextContainer = GetTextContainer(TextContainerIndex); return TextContainer.m_BoundingBox; } void UploadEntityLayerText(const CImageInfo &TextImage, int TexSubWidth, int TexSubHeight, const char *pText, int Length, float x, float y, int FontSize) override { m_pGlyphMap->UploadEntityLayerText(TextImage, TexSubWidth, TexSubHeight, pText, Length, x, y, FontSize); } int AdjustFontSize(const char *pText, int TextLength, int MaxSize, int MaxWidth) const override { const int WidthOfText = CalculateTextWidth(pText, TextLength, 0, 100); int FontSize = 100.0f / ((float)WidthOfText / (float)MaxWidth); if(MaxSize > 0 && FontSize > MaxSize) FontSize = MaxSize; return FontSize; } float GetGlyphOffsetX(int FontSize, char TextCharacter) const override { if(m_pGlyphMap->DefaultFace() == nullptr) return -1.0f; FT_Set_Pixel_Sizes(m_pGlyphMap->DefaultFace(), 0, FontSize); const char *pTmp = &TextCharacter; const int NextCharacter = str_utf8_decode(&pTmp); if(NextCharacter) { #if FREETYPE_MAJOR >= 2 && FREETYPE_MINOR >= 7 && (FREETYPE_MINOR > 7 || FREETYPE_PATCH >= 1) const FT_Int32 FTFlags = FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_BITMAP; #else const FT_Int32 FTFlags = FT_LOAD_RENDER | FT_LOAD_NO_BITMAP; #endif if(FT_Load_Char(m_pGlyphMap->DefaultFace(), NextCharacter, FTFlags)) { log_debug("textrender", "Error loading glyph. Chr=%d", NextCharacter); return -1.0f; } return (float)(m_pGlyphMap->DefaultFace()->glyph->metrics.horiBearingX >> 6); } return 0.0f; } int CalculateTextWidth(const char *pText, int TextLength, int FontWidth, int FontHeight) const override { if(m_pGlyphMap->DefaultFace() == nullptr) return 0; const char *pCurrent = pText; const char *pEnd = pCurrent + TextLength; int WidthOfText = 0; FT_Set_Pixel_Sizes(m_pGlyphMap->DefaultFace(), FontWidth, FontHeight); while(pCurrent < pEnd) { const char *pTmp = pCurrent; const int NextCharacter = str_utf8_decode(&pTmp); if(NextCharacter) { #if FREETYPE_MAJOR >= 2 && FREETYPE_MINOR >= 7 && (FREETYPE_MINOR > 7 || FREETYPE_PATCH >= 1) const FT_Int32 FTFlags = FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_BITMAP; #else const FT_Int32 FTFlags = FT_LOAD_RENDER | FT_LOAD_NO_BITMAP; #endif if(FT_Load_Char(m_pGlyphMap->DefaultFace(), NextCharacter, FTFlags)) { log_debug("textrender", "Error loading glyph. Chr=%d", NextCharacter); pCurrent = pTmp; continue; } WidthOfText += (m_pGlyphMap->DefaultFace()->glyph->metrics.width >> 6) + 1; } pCurrent = pTmp; } return WidthOfText; } void OnPreWindowResize() override { for(auto *pTextContainer : m_vpTextContainers) { if(pTextContainer->m_ContainerIndex.Valid() && pTextContainer->m_ContainerIndex.m_UseCount.use_count() <= 1) { log_error("textrender", "Found non empty text container with index %d with %" PRIzu " quads '%s'", pTextContainer->m_StringInfo.m_QuadBufferContainerIndex, pTextContainer->m_StringInfo.m_vCharacterQuads.size(), pTextContainer->m_aDebugText); dbg_assert(false, "Text container was forgotten by the implementation (the index was overwritten)."); } } } void OnWindowResize() override { bool HasNonEmptyTextContainer = false; for(auto *pTextContainer : m_vpTextContainers) { if(pTextContainer->m_StringInfo.m_QuadBufferContainerIndex != -1) { log_error("textrender", "Found non empty text container with index %d with %" PRIzu " quads '%s'", pTextContainer->m_StringInfo.m_QuadBufferContainerIndex, pTextContainer->m_StringInfo.m_vCharacterQuads.size(), pTextContainer->m_aDebugText); log_error("textrender", "The text container index was in use by %d ", (int)pTextContainer->m_ContainerIndex.m_UseCount.use_count()); HasNonEmptyTextContainer = true; } } dbg_assert(!HasNonEmptyTextContainer, "text container was not empty"); } }; IEngineTextRender *CreateEngineTextRender() { return new CTextRender; }