1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include <base/log.h>
4#include <base/math.h>
5#include <base/system.h>
6
7#include <engine/console.h>
8#include <engine/graphics.h>
9#include <engine/shared/json.h>
10#include <engine/storage.h>
11#include <engine/textrender.h>
12
13// ft2 texture
14#include <ft2build.h>
15#include FT_FREETYPE_H
16
17#include <chrono>
18#include <cstddef>
19#include <limits>
20#include <tuple>
21#include <unordered_map>
22#include <vector>
23
24using namespace std::chrono_literals;
25
26enum
27{
28 FONT_NAME_SIZE = 128,
29};
30
31struct SGlyph
32{
33 enum class EState
34 {
35 UNINITIALIZED,
36 RENDERED,
37 ERROR,
38 };
39 EState m_State = EState::UNINITIALIZED;
40
41 int m_FontSize;
42 FT_Face m_Face;
43 int m_Chr;
44 FT_UInt m_GlyphIndex;
45
46 // these values are scaled to the font size
47 // width * font_size == real_size
48 float m_Width;
49 float m_Height;
50 float m_CharWidth;
51 float m_CharHeight;
52 float m_OffsetX;
53 float m_OffsetY;
54 float m_AdvanceX;
55
56 float m_aUVs[4];
57};
58
59struct SGlyphKeyHash
60{
61 size_t operator()(const std::tuple<FT_Face, int, int> &Key) const
62 {
63 size_t Hash = 17;
64 Hash = Hash * 31 + std::hash<FT_Face>()(std::get<0>(t: Key));
65 Hash = Hash * 31 + std::hash<int>()(std::get<1>(t: Key));
66 Hash = Hash * 31 + std::hash<int>()(std::get<2>(t: Key));
67 return Hash;
68 }
69};
70
71struct SGlyphKeyEquals
72{
73 bool operator()(const std::tuple<FT_Face, int, int> &Lhs, const std::tuple<FT_Face, int, int> &Rhs) const
74 {
75 return std::get<0>(t: Lhs) == std::get<0>(t: Rhs) && std::get<1>(t: Lhs) == std::get<1>(t: Rhs) && std::get<2>(t: Lhs) == std::get<2>(t: Rhs);
76 }
77};
78
79class CAtlas
80{
81 struct SSectionKeyHash
82 {
83 size_t operator()(const std::tuple<size_t, size_t> &Key) const
84 {
85 // Width and height should never be above 2^16 so this hash should cause no collisions
86 return (std::get<0>(t: Key) << 16) ^ std::get<1>(t: Key);
87 }
88 };
89
90 struct SSectionKeyEquals
91 {
92 bool operator()(const std::tuple<size_t, size_t> &Lhs, const std::tuple<size_t, size_t> &Rhs) const
93 {
94 return std::get<0>(t: Lhs) == std::get<0>(t: Rhs) && std::get<1>(t: Lhs) == std::get<1>(t: Rhs);
95 }
96 };
97
98 struct SSection
99 {
100 size_t m_X;
101 size_t m_Y;
102 size_t m_W;
103 size_t m_H;
104
105 SSection() = default;
106
107 SSection(size_t X, size_t Y, size_t W, size_t H) :
108 m_X(X), m_Y(Y), m_W(W), m_H(H)
109 {
110 }
111 };
112
113 /**
114 * Sections with a smaller width or height will not be created
115 * when cutting larger sections, to prevent collecting many
116 * small, mostly unusable sections.
117 */
118 static constexpr size_t MIN_SECTION_DIMENSION = 6;
119
120 /**
121 * Sections with larger width or height will be stored in m_vSections.
122 * Sections with width and height equal or smaller will be stored in m_SectionsMap.
123 * This achieves a good balance between the size of the vector storing all large
124 * sections and the map storing vectors of all sections with specific small sizes.
125 * Lowering this value will result in the size of m_vSections becoming the bottleneck.
126 * Increasing this value will result in the map becoming the bottleneck.
127 */
128 static constexpr size_t MAX_SECTION_DIMENSION_MAPPED = 8 * MIN_SECTION_DIMENSION;
129
130 size_t m_TextureDimension;
131 std::vector<SSection> m_vSections;
132 std::unordered_map<std::tuple<size_t, size_t>, std::vector<SSection>, SSectionKeyHash, SSectionKeyEquals> m_SectionsMap;
133
134 void AddSection(size_t X, size_t Y, size_t W, size_t H)
135 {
136 std::vector<SSection> &vSections = W <= MAX_SECTION_DIMENSION_MAPPED && H <= MAX_SECTION_DIMENSION_MAPPED ? m_SectionsMap[std::make_tuple(args&: W, args&: H)] : m_vSections;
137 vSections.emplace_back(args&: X, args&: Y, args&: W, args&: H);
138 }
139
140 void UseSection(const SSection &Section, size_t Width, size_t Height, int &PosX, int &PosY)
141 {
142 PosX = Section.m_X;
143 PosY = Section.m_Y;
144
145 // Create cut sections
146 const size_t CutW = Section.m_W - Width;
147 const size_t CutH = Section.m_H - Height;
148 if(CutW == 0)
149 {
150 if(CutH >= MIN_SECTION_DIMENSION)
151 AddSection(X: Section.m_X, Y: Section.m_Y + Height, W: Section.m_W, H: CutH);
152 }
153 else if(CutH == 0)
154 {
155 if(CutW >= MIN_SECTION_DIMENSION)
156 AddSection(X: Section.m_X + Width, Y: Section.m_Y, W: CutW, H: Section.m_H);
157 }
158 else if(CutW > CutH)
159 {
160 if(CutW >= MIN_SECTION_DIMENSION)
161 AddSection(X: Section.m_X + Width, Y: Section.m_Y, W: CutW, H: Section.m_H);
162 if(CutH >= MIN_SECTION_DIMENSION)
163 AddSection(X: Section.m_X, Y: Section.m_Y + Height, W: Width, H: CutH);
164 }
165 else
166 {
167 if(CutH >= MIN_SECTION_DIMENSION)
168 AddSection(X: Section.m_X, Y: Section.m_Y + Height, W: Section.m_W, H: CutH);
169 if(CutW >= MIN_SECTION_DIMENSION)
170 AddSection(X: Section.m_X + Width, Y: Section.m_Y, W: CutW, H: Height);
171 }
172 }
173
174public:
175 void Clear(size_t TextureDimension)
176 {
177 m_TextureDimension = TextureDimension;
178 m_vSections.clear();
179 m_vSections.emplace_back(args: 0, args: 0, args&: m_TextureDimension, args&: m_TextureDimension);
180 m_SectionsMap.clear();
181 }
182
183 void IncreaseDimension(size_t NewTextureDimension)
184 {
185 dbg_assert(NewTextureDimension == m_TextureDimension * 2, "New atlas dimension must be twice the old one");
186 // Create 3 square sections to cover the new area, add the sections
187 // to the beginning of the vector so they are considered last.
188 m_vSections.emplace_back(args&: m_TextureDimension, args&: m_TextureDimension, args&: m_TextureDimension, args&: m_TextureDimension);
189 m_vSections.emplace_back(args&: m_TextureDimension, args: 0, args&: m_TextureDimension, args&: m_TextureDimension);
190 m_vSections.emplace_back(args: 0, args&: m_TextureDimension, args&: m_TextureDimension, args&: m_TextureDimension);
191 std::rotate(first: m_vSections.rbegin(), middle: m_vSections.rbegin() + 3, last: m_vSections.rend());
192 m_TextureDimension = NewTextureDimension;
193 }
194
195 bool Add(size_t Width, size_t Height, int &PosX, int &PosY)
196 {
197 if(m_vSections.empty() || m_TextureDimension < Width || m_TextureDimension < Height)
198 return false;
199
200 // Find small section more efficiently by using maps
201 if(Width <= MAX_SECTION_DIMENSION_MAPPED && Height <= MAX_SECTION_DIMENSION_MAPPED)
202 {
203 const auto UseSectionFromVector = [&](std::vector<SSection> &vSections) {
204 if(!vSections.empty())
205 {
206 const SSection Section = vSections.back();
207 vSections.pop_back();
208 UseSection(Section, Width, Height, PosX, PosY);
209 return true;
210 }
211 return false;
212 };
213
214 if(UseSectionFromVector(m_SectionsMap[std::make_tuple(args&: Width, args&: Height)]))
215 return true;
216
217 for(size_t CheckWidth = Width + 1; CheckWidth <= MAX_SECTION_DIMENSION_MAPPED; ++CheckWidth)
218 {
219 if(UseSectionFromVector(m_SectionsMap[std::make_tuple(args&: CheckWidth, args&: Height)]))
220 return true;
221 }
222
223 for(size_t CheckHeight = Height + 1; CheckHeight <= MAX_SECTION_DIMENSION_MAPPED; ++CheckHeight)
224 {
225 if(UseSectionFromVector(m_SectionsMap[std::make_tuple(args&: Width, args&: CheckHeight)]))
226 return true;
227 }
228
229 // We don't iterate sections in the map with increasing width and height at the same time,
230 // because it's slower and doesn't noticeable increase the atlas utilization.
231 }
232
233 // Check vector for larger section
234 size_t SmallestLossValue = std::numeric_limits<size_t>::max();
235 size_t SmallestLossIndex = m_vSections.size();
236 size_t SectionIndex = m_vSections.size();
237 do
238 {
239 --SectionIndex;
240 const SSection &Section = m_vSections[SectionIndex];
241 if(Section.m_W < Width || Section.m_H < Height)
242 continue;
243
244 const size_t LossW = Section.m_W - Width;
245 const size_t LossH = Section.m_H - Height;
246
247 size_t Loss;
248 if(LossW == 0)
249 Loss = LossH;
250 else if(LossH == 0)
251 Loss = LossW;
252 else
253 Loss = LossW * LossH;
254
255 if(Loss < SmallestLossValue)
256 {
257 SmallestLossValue = Loss;
258 SmallestLossIndex = SectionIndex;
259 if(SmallestLossValue == 0)
260 break;
261 }
262 } while(SectionIndex > 0);
263 if(SmallestLossIndex == m_vSections.size())
264 return false; // No usable section found in vector
265
266 // Use the section with the smallest loss
267 const SSection Section = m_vSections[SmallestLossIndex];
268 m_vSections.erase(position: m_vSections.begin() + SmallestLossIndex);
269 UseSection(Section, Width, Height, PosX, PosY);
270 return true;
271 }
272};
273
274class CGlyphMap
275{
276public:
277 enum
278 {
279 FONT_TEXTURE_FILL = 0, // the main text body
280 FONT_TEXTURE_OUTLINE, // the text outline
281 NUM_FONT_TEXTURES,
282 };
283
284private:
285 /**
286 * The initial dimension of the atlas textures.
287 * Results in 1 MB of memory being used per texture.
288 */
289 static constexpr int INITIAL_ATLAS_DIMENSION = 1024;
290
291 /**
292 * The maximum dimension of the atlas textures.
293 * Results in 256 MB of memory being used per texture.
294 */
295 static constexpr int MAXIMUM_ATLAS_DIMENSION = 16 * 1024;
296
297 /**
298 * The minimum supported font size.
299 */
300 static constexpr int MIN_FONT_SIZE = 6;
301
302 /**
303 * The maximum supported font size.
304 */
305 static constexpr int MAX_FONT_SIZE = 128;
306
307 /**
308 * White square to indicate missing glyph.
309 */
310 static constexpr int REPLACEMENT_CHARACTER = 0x25a1;
311
312 IGraphics *m_pGraphics;
313 IGraphics *Graphics() { return m_pGraphics; }
314
315 // Atlas textures and data
316 IGraphics::CTextureHandle m_aTextures[NUM_FONT_TEXTURES];
317 // Width and height are the same, all font textures have the same dimensions
318 size_t m_TextureDimension = INITIAL_ATLAS_DIMENSION;
319 // Keep the full texture data, because OpenGL doesn't provide texture copying
320 uint8_t *m_apTextureData[NUM_FONT_TEXTURES];
321 CAtlas m_TextureAtlas;
322 std::unordered_map<std::tuple<FT_Face, int, int>, SGlyph, SGlyphKeyHash, SGlyphKeyEquals> m_Glyphs;
323
324 // Font faces
325 FT_Face m_DefaultFace = nullptr;
326 FT_Face m_IconFace = nullptr;
327 FT_Face m_VariantFace = nullptr;
328 FT_Face m_SelectedFace = nullptr;
329 std::vector<FT_Face> m_vFallbackFaces;
330 std::vector<FT_Face> m_vFtFaces;
331
332 FT_Face GetFaceByName(const char *pFamilyName)
333 {
334 if(pFamilyName == nullptr || pFamilyName[0] == '\0')
335 return nullptr;
336
337 FT_Face FamilyNameMatch = nullptr;
338 char aFamilyStyleName[FONT_NAME_SIZE];
339
340 for(const auto &CurrentFace : m_vFtFaces)
341 {
342 // Best match: font face with matching family and style name
343 str_format(buffer: aFamilyStyleName, buffer_size: sizeof(aFamilyStyleName), format: "%s %s", CurrentFace->family_name, CurrentFace->style_name);
344 if(str_comp(a: pFamilyName, b: aFamilyStyleName) == 0)
345 {
346 return CurrentFace;
347 }
348
349 // Second best match: font face with matching family
350 if(!FamilyNameMatch && str_comp(a: pFamilyName, b: CurrentFace->family_name) == 0)
351 {
352 FamilyNameMatch = CurrentFace;
353 }
354 }
355
356 return FamilyNameMatch;
357 }
358
359 bool IncreaseGlyphMapSize()
360 {
361 if(m_TextureDimension >= MAXIMUM_ATLAS_DIMENSION)
362 return false;
363
364 const size_t NewTextureDimension = m_TextureDimension * 2;
365 log_debug("textrender", "Increasing atlas dimension to %" PRIzu " (%" PRIzu " MB used for textures)", NewTextureDimension, (NewTextureDimension / 1024) * (NewTextureDimension / 1024) * NUM_FONT_TEXTURES);
366 UnloadTextures();
367
368 for(auto &pTextureData : m_apTextureData)
369 {
370 uint8_t *pTmpTexBuffer = new uint8_t[NewTextureDimension * NewTextureDimension];
371 mem_zero(block: pTmpTexBuffer, size: NewTextureDimension * NewTextureDimension * sizeof(uint8_t));
372 for(size_t y = 0; y < m_TextureDimension; ++y)
373 {
374 mem_copy(dest: &pTmpTexBuffer[y * NewTextureDimension], source: &pTextureData[y * m_TextureDimension], size: m_TextureDimension);
375 }
376 delete[] pTextureData;
377 pTextureData = pTmpTexBuffer;
378 }
379
380 m_TextureAtlas.IncreaseDimension(NewTextureDimension);
381
382 m_TextureDimension = NewTextureDimension;
383
384 UploadTextures();
385 return true;
386 }
387
388 void UploadTextures()
389 {
390 const size_t NewTextureSize = m_TextureDimension * m_TextureDimension;
391 uint8_t *pTmpTextFillData = static_cast<uint8_t *>(malloc(size: NewTextureSize));
392 uint8_t *pTmpTextOutlineData = static_cast<uint8_t *>(malloc(size: NewTextureSize));
393 mem_copy(dest: pTmpTextFillData, source: m_apTextureData[FONT_TEXTURE_FILL], size: NewTextureSize);
394 mem_copy(dest: pTmpTextOutlineData, source: m_apTextureData[FONT_TEXTURE_OUTLINE], size: NewTextureSize);
395 Graphics()->LoadTextTextures(Width: m_TextureDimension, Height: m_TextureDimension, TextTexture&: m_aTextures[FONT_TEXTURE_FILL], TextOutlineTexture&: m_aTextures[FONT_TEXTURE_OUTLINE], pTextData: pTmpTextFillData, pTextOutlineData: pTmpTextOutlineData);
396 }
397
398 void UnloadTextures()
399 {
400 Graphics()->UnloadTextTextures(TextTexture&: m_aTextures[FONT_TEXTURE_FILL], TextOutlineTexture&: m_aTextures[FONT_TEXTURE_OUTLINE]);
401 }
402
403 FT_UInt GetCharGlyph(int Chr, FT_Face *pFace, bool AllowReplacementCharacter)
404 {
405 for(FT_Face Face : {m_SelectedFace, m_DefaultFace, m_VariantFace})
406 {
407 if(Face && Face->charmap)
408 {
409 FT_UInt GlyphIndex = FT_Get_Char_Index(face: Face, charcode: (FT_ULong)Chr);
410 if(GlyphIndex)
411 {
412 *pFace = Face;
413 return GlyphIndex;
414 }
415 }
416 }
417
418 for(const auto &FallbackFace : m_vFallbackFaces)
419 {
420 if(FallbackFace->charmap)
421 {
422 FT_UInt GlyphIndex = FT_Get_Char_Index(face: FallbackFace, charcode: (FT_ULong)Chr);
423 if(GlyphIndex)
424 {
425 *pFace = FallbackFace;
426 return GlyphIndex;
427 }
428 }
429 }
430
431 if(!m_DefaultFace || !m_DefaultFace->charmap || !AllowReplacementCharacter)
432 {
433 *pFace = nullptr;
434 return 0;
435 }
436
437 FT_UInt GlyphIndex = FT_Get_Char_Index(face: m_DefaultFace, charcode: (FT_ULong)REPLACEMENT_CHARACTER);
438 *pFace = m_DefaultFace;
439
440 if(GlyphIndex == 0)
441 {
442 log_debug("textrender", "Default font has no glyph for either %d or replacement char %d.", Chr, REPLACEMENT_CHARACTER);
443 }
444
445 return GlyphIndex;
446 }
447
448 void Grow(const unsigned char *pIn, unsigned char *pOut, int w, int h, int OutlineCount) const
449 {
450 for(int y = 0; y < h; y++)
451 {
452 for(int x = 0; x < w; x++)
453 {
454 int c = pIn[y * w + x];
455
456 for(int sy = -OutlineCount; sy <= OutlineCount; sy++)
457 {
458 for(int sx = -OutlineCount; sx <= OutlineCount; sx++)
459 {
460 int GetX = x + sx;
461 int GetY = y + sy;
462 if(GetX >= 0 && GetY >= 0 && GetX < w && GetY < h)
463 {
464 int Index = GetY * w + GetX;
465 float Mask = 1.f - std::clamp(val: length(a: vec2(sx, sy)) - OutlineCount, lo: 0.f, hi: 1.f);
466 c = maximum(a: c, b: int(pIn[Index] * Mask));
467 }
468 }
469 }
470
471 pOut[y * w + x] = c;
472 }
473 }
474 }
475
476 int AdjustOutlineThicknessToFontSize(int OutlineThickness, int FontSize) const
477 {
478 if(FontSize > 48)
479 OutlineThickness *= 4;
480 else if(FontSize >= 18)
481 OutlineThickness *= 2;
482 return OutlineThickness;
483 }
484
485 void UploadGlyph(int TextureIndex, int PosX, int PosY, size_t Width, size_t Height, uint8_t *pData)
486 {
487 for(size_t y = 0; y < Height; ++y)
488 {
489 mem_copy(dest: &m_apTextureData[TextureIndex][PosX + ((y + PosY) * m_TextureDimension)], source: &pData[y * Width], size: Width);
490 }
491 Graphics()->UpdateTextTexture(TextureId: m_aTextures[TextureIndex], x: PosX, y: PosY, Width, Height, pData, IsMovedPointer: true);
492 }
493
494 bool FitGlyph(size_t Width, size_t Height, int &PosX, int &PosY)
495 {
496 return m_TextureAtlas.Add(Width, Height, PosX, PosY);
497 }
498
499 bool RenderGlyph(SGlyph &Glyph)
500 {
501 FT_Set_Pixel_Sizes(face: Glyph.m_Face, pixel_width: 0, pixel_height: Glyph.m_FontSize);
502
503 if(FT_Load_Glyph(face: Glyph.m_Face, glyph_index: Glyph.m_GlyphIndex, FT_LOAD_RENDER | FT_LOAD_NO_BITMAP))
504 {
505 log_debug("textrender", "Error loading glyph. Chr=%d GlyphIndex=%u", Glyph.m_Chr, Glyph.m_GlyphIndex);
506 return false;
507 }
508
509 const FT_Bitmap *pBitmap = &Glyph.m_Face->glyph->bitmap;
510 if(pBitmap->pixel_mode != FT_PIXEL_MODE_GRAY)
511 {
512 log_debug("textrender", "Error loading glyph, unsupported pixel mode. Chr=%d GlyphIndex=%u PixelMode=%d", Glyph.m_Chr, Glyph.m_GlyphIndex, pBitmap->pixel_mode);
513 return false;
514 }
515
516 const unsigned RealWidth = pBitmap->width;
517 const unsigned RealHeight = pBitmap->rows;
518
519 // adjust spacing
520 int OutlineThickness = 0;
521 int x = 0;
522 int y = 0;
523 if(RealWidth > 0)
524 {
525 OutlineThickness = AdjustOutlineThicknessToFontSize(OutlineThickness: 1, FontSize: Glyph.m_FontSize);
526 x += (OutlineThickness + 1);
527 y += (OutlineThickness + 1);
528 }
529
530 const unsigned Width = RealWidth + x * 2;
531 const unsigned Height = RealHeight + y * 2;
532
533 int X = 0;
534 int Y = 0;
535
536 if(Width > 0 && Height > 0)
537 {
538 // find space in atlas, or increase size if necessary
539 while(!FitGlyph(Width, Height, PosX&: X, PosY&: Y))
540 {
541 if(!IncreaseGlyphMapSize())
542 {
543 log_debug("textrender", "Cannot fit glyph into atlas, which is already at maximum size. Chr=%d GlyphIndex=%u", Glyph.m_Chr, Glyph.m_GlyphIndex);
544 return false;
545 }
546 }
547
548 // prepare glyph data
549 const size_t GlyphDataSize = (size_t)Width * Height * sizeof(uint8_t);
550 uint8_t *pGlyphDataFill = static_cast<uint8_t *>(malloc(size: GlyphDataSize));
551 uint8_t *pGlyphDataOutline = static_cast<uint8_t *>(malloc(size: GlyphDataSize));
552 mem_zero(block: pGlyphDataFill, size: GlyphDataSize);
553 for(unsigned py = 0; py < pBitmap->rows; ++py)
554 {
555 mem_copy(dest: &pGlyphDataFill[(py + y) * Width + x], source: &pBitmap->buffer[py * pBitmap->width], size: pBitmap->width);
556 }
557 Grow(pIn: pGlyphDataFill, pOut: pGlyphDataOutline, w: Width, h: Height, OutlineCount: OutlineThickness);
558
559 // upload the glyph
560 UploadGlyph(TextureIndex: FONT_TEXTURE_FILL, PosX: X, PosY: Y, Width, Height, pData: pGlyphDataFill);
561 UploadGlyph(TextureIndex: FONT_TEXTURE_OUTLINE, PosX: X, PosY: Y, Width, Height, pData: pGlyphDataOutline);
562 }
563
564 // set glyph info
565 {
566 Glyph.m_Height = Height;
567 Glyph.m_Width = Width;
568 Glyph.m_CharHeight = RealHeight;
569 Glyph.m_CharWidth = RealWidth;
570 Glyph.m_OffsetX = (Glyph.m_Face->glyph->metrics.horiBearingX >> 6);
571 Glyph.m_OffsetY = -((Glyph.m_Face->glyph->metrics.height >> 6) - (Glyph.m_Face->glyph->metrics.horiBearingY >> 6));
572 Glyph.m_AdvanceX = (Glyph.m_Face->glyph->advance.x >> 6);
573
574 Glyph.m_aUVs[0] = X;
575 Glyph.m_aUVs[1] = Y;
576 Glyph.m_aUVs[2] = Glyph.m_aUVs[0] + Width;
577 Glyph.m_aUVs[3] = Glyph.m_aUVs[1] + Height;
578
579 Glyph.m_State = SGlyph::EState::RENDERED;
580 }
581 return true;
582 }
583
584public:
585 CGlyphMap(IGraphics *pGraphics)
586 {
587 m_pGraphics = pGraphics;
588 for(auto &pTextureData : m_apTextureData)
589 {
590 pTextureData = new uint8_t[m_TextureDimension * m_TextureDimension];
591 mem_zero(block: pTextureData, size: m_TextureDimension * m_TextureDimension * sizeof(uint8_t));
592 }
593
594 m_TextureAtlas.Clear(TextureDimension: m_TextureDimension);
595 UploadTextures();
596 }
597
598 ~CGlyphMap()
599 {
600 UnloadTextures();
601 for(auto &pTextureData : m_apTextureData)
602 {
603 delete[] pTextureData;
604 }
605 }
606
607 FT_Face DefaultFace() const
608 {
609 return m_DefaultFace;
610 }
611
612 FT_Face IconFace() const
613 {
614 return m_IconFace;
615 }
616
617 void AddFace(FT_Face Face)
618 {
619 m_vFtFaces.push_back(x: Face);
620 }
621
622 bool SetDefaultFaceByName(const char *pFamilyName)
623 {
624 m_DefaultFace = GetFaceByName(pFamilyName);
625 if(!m_DefaultFace)
626 {
627 if(!m_vFtFaces.empty())
628 {
629 m_DefaultFace = m_vFtFaces.front();
630 }
631 log_error("textrender", "The default font face '%s' could not be found", pFamilyName);
632 return false;
633 }
634 return true;
635 }
636
637 bool SetIconFaceByName(const char *pFamilyName)
638 {
639 m_IconFace = GetFaceByName(pFamilyName);
640 if(!m_IconFace)
641 {
642 log_error("textrender", "The icon font face '%s' could not be found", pFamilyName);
643 return false;
644 }
645 return true;
646 }
647
648 bool AddFallbackFaceByName(const char *pFamilyName)
649 {
650 FT_Face Face = GetFaceByName(pFamilyName);
651 if(!Face)
652 {
653 log_error("textrender", "The fallback font face '%s' could not be found", pFamilyName);
654 return false;
655 }
656 if(std::find(first: m_vFallbackFaces.begin(), last: m_vFallbackFaces.end(), val: Face) != m_vFallbackFaces.end())
657 {
658 log_warn("textrender", "The fallback font face '%s' was specified multiple times", pFamilyName);
659 return true;
660 }
661 m_vFallbackFaces.push_back(x: Face);
662 return true;
663 }
664
665 bool SetVariantFaceByName(const char *pFamilyName)
666 {
667 FT_Face Face = GetFaceByName(pFamilyName);
668 if(m_VariantFace != Face)
669 {
670 m_VariantFace = Face;
671 Clear(); // rebuild atlas after changing variant font
672 if(!Face && pFamilyName != nullptr)
673 {
674 log_error("textrender", "The variant font face '%s' could not be found", pFamilyName);
675 return false;
676 }
677 }
678 return true;
679 }
680
681 void SetFontPreset(EFontPreset FontPreset)
682 {
683 switch(FontPreset)
684 {
685 case EFontPreset::DEFAULT_FONT:
686 m_SelectedFace = nullptr;
687 break;
688 case EFontPreset::ICON_FONT:
689 m_SelectedFace = m_IconFace;
690 break;
691 }
692 }
693
694 void Clear()
695 {
696 for(size_t TextureIndex = 0; TextureIndex < NUM_FONT_TEXTURES; ++TextureIndex)
697 {
698 mem_zero(block: m_apTextureData[TextureIndex], size: m_TextureDimension * m_TextureDimension * sizeof(uint8_t));
699 Graphics()->UpdateTextTexture(TextureId: m_aTextures[TextureIndex], x: 0, y: 0, Width: m_TextureDimension, Height: m_TextureDimension, pData: m_apTextureData[TextureIndex], IsMovedPointer: false);
700 }
701
702 m_TextureAtlas.Clear(TextureDimension: m_TextureDimension);
703 m_Glyphs.clear();
704 }
705
706 const SGlyph *GetGlyph(int Chr, int FontSize)
707 {
708 FontSize = std::clamp(val: FontSize, lo: MIN_FONT_SIZE, hi: MAX_FONT_SIZE);
709
710 // Find glyph index and most appropriate font face.
711 FT_Face Face;
712 FT_UInt GlyphIndex = GetCharGlyph(Chr, pFace: &Face, AllowReplacementCharacter: false);
713 if(GlyphIndex == 0)
714 {
715 // Use replacement character if glyph could not be found,
716 // also retrieve replacement character from the atlas.
717 return Chr == REPLACEMENT_CHARACTER ? nullptr : GetGlyph(Chr: REPLACEMENT_CHARACTER, FontSize);
718 }
719
720 // Check if glyph for this (font face, character, font size)-combination was already rendered.
721 SGlyph &Glyph = m_Glyphs[std::make_tuple(args&: Face, args&: Chr, args&: FontSize)];
722 if(Glyph.m_State == SGlyph::EState::RENDERED)
723 return &Glyph;
724 else if(Glyph.m_State == SGlyph::EState::ERROR)
725 return nullptr;
726
727 // Else, render it.
728 Glyph.m_FontSize = FontSize;
729 Glyph.m_Face = Face;
730 Glyph.m_Chr = Chr;
731 Glyph.m_GlyphIndex = GlyphIndex;
732 if(RenderGlyph(Glyph))
733 return &Glyph;
734
735 // Use replacement character if the glyph could not be rendered,
736 // also retrieve replacement character from the atlas.
737 const SGlyph *pReplacementCharacter = Chr == REPLACEMENT_CHARACTER ? nullptr : GetGlyph(Chr: REPLACEMENT_CHARACTER, FontSize);
738 if(pReplacementCharacter)
739 {
740 Glyph = *pReplacementCharacter;
741 return &Glyph;
742 }
743
744 // Keep failed glyph in the cache so we don't attempt to render it again,
745 // but set its state to ERROR so we don't return it to the text render.
746 Glyph.m_State = SGlyph::EState::ERROR;
747 return nullptr;
748 }
749
750 vec2 Kerning(const SGlyph *pLeft, const SGlyph *pRight) const
751 {
752 if(pLeft != nullptr && pRight != nullptr && pLeft->m_Face == pRight->m_Face && pLeft->m_FontSize == pRight->m_FontSize)
753 {
754 FT_Vector Kerning = {.x: 0, .y: 0};
755 FT_Set_Pixel_Sizes(face: pLeft->m_Face, pixel_width: 0, pixel_height: pLeft->m_FontSize);
756 FT_Get_Kerning(face: pLeft->m_Face, left_glyph: pLeft->m_Chr, right_glyph: pRight->m_Chr, kern_mode: FT_KERNING_DEFAULT, akerning: &Kerning);
757 return vec2(Kerning.x >> 6, Kerning.y >> 6);
758 }
759 return vec2(0.0f, 0.0f);
760 }
761
762 void UploadEntityLayerText(const CImageInfo &TextImage, int TexSubWidth, int TexSubHeight, const char *pText, int Length, float x, float y, int FontSize)
763 {
764 if(FontSize < 1)
765 return;
766
767 const size_t PixelSize = TextImage.PixelSize();
768 const char *pCurrent = pText;
769 const char *pEnd = pCurrent + Length;
770 int WidthLastChars = 0;
771
772 while(pCurrent < pEnd)
773 {
774 const char *pTmp = pCurrent;
775 const int NextCharacter = str_utf8_decode(ptr: &pTmp);
776
777 if(NextCharacter)
778 {
779 FT_Face Face;
780 FT_UInt GlyphIndex = GetCharGlyph(Chr: NextCharacter, pFace: &Face, AllowReplacementCharacter: true);
781 if(GlyphIndex == 0)
782 {
783 pCurrent = pTmp;
784 continue;
785 }
786
787 FT_Set_Pixel_Sizes(face: Face, pixel_width: 0, pixel_height: FontSize);
788 if(FT_Load_Char(face: Face, char_code: NextCharacter, FT_LOAD_RENDER | FT_LOAD_NO_BITMAP))
789 {
790 log_debug("textrender", "Error loading glyph. Chr=%d GlyphIndex=%u", NextCharacter, GlyphIndex);
791 pCurrent = pTmp;
792 continue;
793 }
794
795 const FT_Bitmap *pBitmap = &Face->glyph->bitmap;
796 if(pBitmap->pixel_mode != FT_PIXEL_MODE_GRAY)
797 {
798 log_debug("textrender", "Error loading glyph, unsupported pixel mode. Chr=%d GlyphIndex=%u PixelMode=%d", NextCharacter, GlyphIndex, pBitmap->pixel_mode);
799 pCurrent = pTmp;
800 continue;
801 }
802
803 for(unsigned OffY = 0; OffY < pBitmap->rows; ++OffY)
804 {
805 for(unsigned OffX = 0; OffX < pBitmap->width; ++OffX)
806 {
807 const int ImgOffX = std::clamp(val: x + OffX + WidthLastChars, lo: x, hi: (x + TexSubWidth) - 1);
808 const int ImgOffY = std::clamp(val: y + OffY, lo: y, hi: (y + TexSubHeight) - 1);
809 const size_t ImageOffset = ImgOffY * (TextImage.m_Width * PixelSize) + ImgOffX * PixelSize;
810 for(size_t i = 0; i < PixelSize - 1; ++i)
811 {
812 TextImage.m_pData[ImageOffset + i] = 255;
813 }
814 TextImage.m_pData[ImageOffset + PixelSize - 1] = pBitmap->buffer[OffY * pBitmap->width + OffX];
815 }
816 }
817
818 WidthLastChars += (pBitmap->width + 1);
819 }
820 pCurrent = pTmp;
821 }
822 }
823
824 size_t TextureDimension() const
825 {
826 return m_TextureDimension;
827 }
828
829 IGraphics::CTextureHandle Texture(size_t TextureIndex) const
830 {
831 return m_aTextures[TextureIndex];
832 }
833};
834
835typedef vector4_base<unsigned char> STextCharQuadVertexColor;
836
837struct STextCharQuadVertex
838{
839 STextCharQuadVertex()
840 {
841 m_Color.r = m_Color.g = m_Color.b = m_Color.a = 255;
842 }
843 float m_X, m_Y;
844 // do not use normalized floats as coordinates, since the texture might grow
845 float m_U, m_V;
846 STextCharQuadVertexColor m_Color;
847};
848
849struct STextCharQuad
850{
851 STextCharQuadVertex m_aVertices[4];
852};
853
854struct SStringInfo
855{
856 int m_QuadBufferObjectIndex;
857 int m_QuadBufferContainerIndex;
858 int m_SelectionQuadContainerIndex;
859
860 std::vector<STextCharQuad> m_vCharacterQuads;
861};
862
863struct STextContainer
864{
865 STextContainer()
866 {
867 Reset();
868 }
869
870 SStringInfo m_StringInfo;
871
872 // keep these values to calculate offsets
873 float m_AlignedStartX;
874 float m_AlignedStartY;
875 float m_X;
876 float m_Y;
877
878 int m_Flags;
879 int m_LineCount;
880 int m_GlyphCount;
881 int m_CharCount;
882 int m_MaxLines;
883 float m_LineWidth;
884
885 unsigned m_RenderFlags;
886
887 bool m_HasCursor;
888 bool m_ForceCursorRendering;
889 bool m_HasSelection;
890
891 bool m_SingleTimeUse;
892
893 STextBoundingBox m_BoundingBox;
894
895 // prefix of the container's text stored for debugging purposes
896 char m_aDebugText[32];
897
898 STextContainerIndex m_ContainerIndex;
899
900 void Reset()
901 {
902 m_StringInfo.m_QuadBufferObjectIndex = m_StringInfo.m_QuadBufferContainerIndex = m_StringInfo.m_SelectionQuadContainerIndex = -1;
903 m_StringInfo.m_vCharacterQuads.clear();
904
905 m_AlignedStartX = m_AlignedStartY = m_X = m_Y = 0.0f;
906 m_Flags = m_LineCount = m_CharCount = m_GlyphCount = 0;
907 m_MaxLines = -1;
908 m_LineWidth = -1.0f;
909
910 m_RenderFlags = 0;
911
912 m_HasCursor = false;
913 m_ForceCursorRendering = false;
914 m_HasSelection = false;
915
916 m_SingleTimeUse = false;
917
918 m_BoundingBox = {.m_X: 0.0f, .m_Y: 0.0f, .m_W: 0.0f, .m_H: 0.0f};
919
920 m_aDebugText[0] = '\0';
921
922 m_ContainerIndex = STextContainerIndex{};
923 }
924};
925
926float CTextCursor::Height() const
927{
928 return m_LineCount * (m_AlignedFontSize + m_AlignedLineSpacing);
929}
930
931STextBoundingBox CTextCursor::BoundingBox() const
932{
933 return {.m_X: m_StartX, .m_Y: m_StartY, .m_W: m_LongestLineWidth, .m_H: Height()};
934}
935
936void CTextCursor::SetPosition(vec2 Position)
937{
938 m_StartX = Position.x;
939 m_StartY = Position.y;
940 m_X = Position.x;
941 m_Y = Position.y;
942}
943
944struct SFontLanguageVariant
945{
946 char m_aLanguageFile[IO_MAX_PATH_LENGTH];
947 char m_aFamilyName[FONT_NAME_SIZE];
948};
949
950class CTextRender : public IEngineTextRender
951{
952 IConsole *m_pConsole;
953 IGraphics *m_pGraphics;
954 IStorage *m_pStorage;
955 IConsole *Console() { return m_pConsole; }
956 IGraphics *Graphics() { return m_pGraphics; }
957 IStorage *Storage() { return m_pStorage; }
958
959 CGlyphMap *m_pGlyphMap;
960 std::vector<void *> m_vpFontData;
961
962 std::vector<SFontLanguageVariant> m_vVariants;
963
964 unsigned m_RenderFlags;
965
966 ColorRGBA m_Color;
967 ColorRGBA m_OutlineColor;
968 ColorRGBA m_SelectionColor;
969
970 FT_Library m_FTLibrary;
971
972 std::vector<STextContainer *> m_vpTextContainers;
973 std::vector<int> m_vTextContainerIndices;
974 int m_FirstFreeTextContainerIndex;
975
976 SBufferContainerInfo m_DefaultTextContainerInfo;
977
978 std::chrono::nanoseconds m_CursorRenderTime;
979
980 int GetFreeTextContainerIndex()
981 {
982 if(m_FirstFreeTextContainerIndex == -1)
983 {
984 const int Index = (int)m_vTextContainerIndices.size();
985 m_vTextContainerIndices.push_back(x: Index);
986 return Index;
987 }
988 else
989 {
990 const int Index = m_FirstFreeTextContainerIndex;
991 m_FirstFreeTextContainerIndex = m_vTextContainerIndices[Index];
992 m_vTextContainerIndices[Index] = Index;
993 return Index;
994 }
995 }
996
997 void FreeTextContainerIndex(STextContainerIndex &Index)
998 {
999 m_vTextContainerIndices[Index.m_Index] = m_FirstFreeTextContainerIndex;
1000 m_FirstFreeTextContainerIndex = Index.m_Index;
1001 Index.Reset();
1002 }
1003
1004 void FreeTextContainer(STextContainerIndex &Index)
1005 {
1006 m_vpTextContainers[Index.m_Index]->Reset();
1007 FreeTextContainerIndex(Index);
1008 }
1009
1010 STextContainer &GetTextContainer(const STextContainerIndex &Index)
1011 {
1012 dbg_assert(Index.Valid(), "Text container index was invalid.");
1013 if(Index.m_Index >= (int)m_vpTextContainers.size())
1014 {
1015 for(int i = 0; i < Index.m_Index + 1 - (int)m_vpTextContainers.size(); ++i)
1016 m_vpTextContainers.push_back(x: new STextContainer());
1017 }
1018
1019 if(m_vpTextContainers[Index.m_Index]->m_ContainerIndex.m_UseCount.get() != Index.m_UseCount.get())
1020 {
1021 m_vpTextContainers[Index.m_Index]->m_ContainerIndex = Index;
1022 }
1023 return *m_vpTextContainers[Index.m_Index];
1024 }
1025
1026 int WordLength(const char *pText) const
1027 {
1028 const char *pCursor = pText;
1029 while(true)
1030 {
1031 if(*pCursor == '\0')
1032 return pCursor - pText;
1033 if(*pCursor == '\n' || *pCursor == '\t' || *pCursor == ' ')
1034 return pCursor - pText + 1;
1035 str_utf8_decode(ptr: &pCursor);
1036 }
1037 }
1038
1039 bool LoadFontCollection(const char *pFontName, const FT_Byte *pFontData, FT_Long FontDataSize)
1040 {
1041 FT_Face FtFace;
1042 FT_Error CollectionLoadError = FT_New_Memory_Face(library: m_FTLibrary, file_base: pFontData, file_size: FontDataSize, face_index: -1, aface: &FtFace);
1043 if(CollectionLoadError)
1044 {
1045 log_error("textrender", "Failed to load font file '%s': %s", pFontName, FT_Error_String(CollectionLoadError));
1046 return false;
1047 }
1048
1049 const FT_Long NumFaces = FtFace->num_faces;
1050 FT_Done_Face(face: FtFace);
1051
1052 bool LoadedAny = false;
1053 for(FT_Long FaceIndex = 0; FaceIndex < NumFaces; ++FaceIndex)
1054 {
1055 FT_Error FaceLoadError = FT_New_Memory_Face(library: m_FTLibrary, file_base: pFontData, file_size: FontDataSize, face_index: FaceIndex, aface: &FtFace);
1056 if(FaceLoadError)
1057 {
1058 log_error("textrender", "Failed to load font face %ld from font file '%s': %s", FaceIndex, pFontName, FT_Error_String(FaceLoadError));
1059 FT_Done_Face(face: FtFace);
1060 continue;
1061 }
1062
1063 m_pGlyphMap->AddFace(Face: FtFace);
1064
1065 log_debug("textrender", "Loaded font face %ld '%s %s' from font file '%s'", FaceIndex, FtFace->family_name, FtFace->style_name, pFontName);
1066 LoadedAny = true;
1067 }
1068
1069 if(!LoadedAny)
1070 {
1071 log_error("textrender", "Failed to load font file '%s': no font faces could be loaded", pFontName);
1072 return false;
1073 }
1074
1075 return true;
1076 }
1077
1078 void SetRenderFlags(unsigned Flags) override
1079 {
1080 m_RenderFlags = Flags;
1081 }
1082
1083 unsigned GetRenderFlags() const override
1084 {
1085 return m_RenderFlags;
1086 }
1087
1088public:
1089 CTextRender()
1090 {
1091 m_pConsole = nullptr;
1092 m_pGraphics = nullptr;
1093 m_pStorage = nullptr;
1094 m_pGlyphMap = nullptr;
1095
1096 m_Color = DefaultTextColor();
1097 m_OutlineColor = DefaultTextOutlineColor();
1098 m_SelectionColor = DefaultTextSelectionColor();
1099
1100 m_FTLibrary = nullptr;
1101
1102 m_RenderFlags = 0;
1103 m_CursorRenderTime = time_get_nanoseconds();
1104 }
1105
1106 void Init() override
1107 {
1108 m_pConsole = Kernel()->RequestInterface<IConsole>();
1109 m_pGraphics = Kernel()->RequestInterface<IGraphics>();
1110 m_pStorage = Kernel()->RequestInterface<IStorage>();
1111 FT_Init_FreeType(alibrary: &m_FTLibrary);
1112 m_pGlyphMap = new CGlyphMap(m_pGraphics);
1113
1114 // print freetype version
1115 {
1116 int LMajor, LMinor, LPatch;
1117 FT_Library_Version(library: m_FTLibrary, amajor: &LMajor, aminor: &LMinor, apatch: &LPatch);
1118 log_info("textrender", "Freetype version %d.%d.%d (compiled = %d.%d.%d)", LMajor, LMinor, LPatch, FREETYPE_MAJOR, FREETYPE_MINOR, FREETYPE_PATCH);
1119 }
1120
1121 m_FirstFreeTextContainerIndex = -1;
1122
1123 m_DefaultTextContainerInfo.m_Stride = sizeof(STextCharQuadVertex);
1124 m_DefaultTextContainerInfo.m_VertBufferBindingIndex = -1;
1125
1126 m_DefaultTextContainerInfo.m_vAttributes.emplace_back();
1127 SBufferContainerInfo::SAttribute *pAttr = &m_DefaultTextContainerInfo.m_vAttributes.back();
1128 pAttr->m_DataTypeCount = 2;
1129 pAttr->m_FuncType = 0;
1130 pAttr->m_Normalized = false;
1131 pAttr->m_pOffset = nullptr;
1132 pAttr->m_Type = GRAPHICS_TYPE_FLOAT;
1133
1134 m_DefaultTextContainerInfo.m_vAttributes.emplace_back();
1135 pAttr = &m_DefaultTextContainerInfo.m_vAttributes.back();
1136 pAttr->m_DataTypeCount = 2;
1137 pAttr->m_FuncType = 0;
1138 pAttr->m_Normalized = false;
1139 pAttr->m_pOffset = (void *)(sizeof(float) * 2);
1140 pAttr->m_Type = GRAPHICS_TYPE_FLOAT;
1141
1142 m_DefaultTextContainerInfo.m_vAttributes.emplace_back();
1143 pAttr = &m_DefaultTextContainerInfo.m_vAttributes.back();
1144 pAttr->m_DataTypeCount = 4;
1145 pAttr->m_FuncType = 0;
1146 pAttr->m_Normalized = true;
1147 pAttr->m_pOffset = (void *)(sizeof(float) * 2 + sizeof(float) * 2);
1148 pAttr->m_Type = GRAPHICS_TYPE_UNSIGNED_BYTE;
1149 }
1150
1151 void Shutdown() override
1152 {
1153 for(auto *pTextCont : m_vpTextContainers)
1154 delete pTextCont;
1155 m_vpTextContainers.clear();
1156
1157 delete m_pGlyphMap;
1158 m_pGlyphMap = nullptr;
1159
1160 if(m_FTLibrary != nullptr)
1161 FT_Done_FreeType(library: m_FTLibrary);
1162 m_FTLibrary = nullptr;
1163
1164 for(auto *pFontData : m_vpFontData)
1165 free(ptr: pFontData);
1166 m_vpFontData.clear();
1167
1168 m_DefaultTextContainerInfo.m_vAttributes.clear();
1169
1170 m_pConsole = nullptr;
1171 m_pGraphics = nullptr;
1172 m_pStorage = nullptr;
1173 }
1174
1175 bool LoadFonts() override
1176 {
1177 // read file data into buffer
1178 const char *pFilename = "fonts/index.json";
1179 void *pFileData;
1180 unsigned JsonFileSize;
1181 if(!Storage()->ReadFile(pFilename, Type: IStorage::TYPE_ALL, ppResult: &pFileData, pResultLen: &JsonFileSize))
1182 {
1183 log_error("textrender", "Failed to open/read font index file '%s'", pFilename);
1184 return false;
1185 }
1186
1187 // parse json data
1188 json_settings JsonSettings{};
1189 char aError[256];
1190 json_value *pJsonData = json_parse_ex(settings: &JsonSettings, json: static_cast<const json_char *>(pFileData), length: JsonFileSize, error: aError);
1191 free(ptr: pFileData);
1192 if(pJsonData == nullptr)
1193 {
1194 log_error("textrender", "Failed to parse font index file '%s': %s", pFilename, aError);
1195 return false;
1196 }
1197 if(pJsonData->type != json_object)
1198 {
1199 log_error("textrender", "Font index malformed: root must be an object in file '%s'", pFilename);
1200 return false;
1201 }
1202
1203 bool Success = true;
1204
1205 // extract font file definitions
1206 const json_value &FontFiles = (*pJsonData)["font files"];
1207 if(FontFiles.type == json_array)
1208 {
1209 for(unsigned FontFileIndex = 0; FontFileIndex < FontFiles.u.array.length; ++FontFileIndex)
1210 {
1211 if(FontFiles[FontFileIndex].type != json_string)
1212 {
1213 log_error("textrender", "Font index malformed: 'font files' must be an array of strings (error at index %d)", FontFileIndex);
1214 Success = false;
1215 continue;
1216 }
1217
1218 char aFontName[IO_MAX_PATH_LENGTH];
1219 str_format(buffer: aFontName, buffer_size: sizeof(aFontName), format: "fonts/%s", FontFiles[FontFileIndex].u.string.ptr);
1220 void *pFontData;
1221 unsigned FontDataSize;
1222 if(Storage()->ReadFile(pFilename: aFontName, Type: IStorage::TYPE_ALL, ppResult: &pFontData, pResultLen: &FontDataSize))
1223 {
1224 if(LoadFontCollection(pFontName: aFontName, pFontData: static_cast<FT_Byte *>(pFontData), FontDataSize: (FT_Long)FontDataSize))
1225 {
1226 m_vpFontData.push_back(x: pFontData);
1227 }
1228 else
1229 {
1230 free(ptr: pFontData);
1231 }
1232 }
1233 else
1234 {
1235 log_error("textrender", "Failed to open/read font file '%s'", aFontName);
1236 Success = false;
1237 }
1238 }
1239 }
1240 else
1241 {
1242 log_error("textrender", "Font index malformed: 'font files' must be an array");
1243 Success = false;
1244 }
1245
1246 // extract default family name
1247 const json_value &DefaultFace = (*pJsonData)["default"];
1248 if(DefaultFace.type == json_string)
1249 {
1250 if(!m_pGlyphMap->SetDefaultFaceByName(DefaultFace.u.string.ptr))
1251 {
1252 Success = false;
1253 }
1254 }
1255 else
1256 {
1257 log_error("textrender", "Font index malformed: 'default' must be a string");
1258 Success = false;
1259 }
1260
1261 // extract language variant family names
1262 const json_value &Variants = (*pJsonData)["language variants"];
1263 if(Variants.type == json_object)
1264 {
1265 m_vVariants.reserve(n: Variants.u.object.length);
1266 for(size_t i = 0; i < Variants.u.object.length; ++i)
1267 {
1268 const json_value *pFamilyName = Variants.u.object.values[i].value;
1269 if(pFamilyName->type != json_string)
1270 {
1271 log_error("textrender", "Font index malformed: 'language variants' entries must have string values (error on entry '%s')", Variants.u.object.values[i].name);
1272 Success = false;
1273 continue;
1274 }
1275
1276 SFontLanguageVariant Variant;
1277 str_format(buffer: Variant.m_aLanguageFile, buffer_size: sizeof(Variant.m_aLanguageFile), format: "languages/%s.txt", Variants.u.object.values[i].name);
1278 str_copy(dst&: Variant.m_aFamilyName, src: pFamilyName->u.string.ptr);
1279 m_vVariants.emplace_back(args&: Variant);
1280 }
1281 }
1282 else
1283 {
1284 log_error("textrender", "Font index malformed: 'language variants' must be an array");
1285 Success = false;
1286 }
1287
1288 // extract fallback family names
1289 const json_value &FallbackFaces = (*pJsonData)["fallbacks"];
1290 if(FallbackFaces.type == json_array)
1291 {
1292 for(unsigned i = 0; i < FallbackFaces.u.array.length; ++i)
1293 {
1294 if(FallbackFaces[i].type != json_string)
1295 {
1296 log_error("textrender", "Font index malformed: 'fallbacks' must be an array of strings (error at index %d)", i);
1297 Success = false;
1298 continue;
1299 }
1300 if(!m_pGlyphMap->AddFallbackFaceByName(pFamilyName: FallbackFaces[i].u.string.ptr))
1301 {
1302 Success = false;
1303 }
1304 }
1305 }
1306 else
1307 {
1308 log_error("textrender", "Font index malformed: 'fallbacks' must be an array");
1309 Success = false;
1310 }
1311
1312 // extract icon font family name
1313 const json_value &IconFace = (*pJsonData)["icon"];
1314 if(IconFace.type == json_string)
1315 {
1316 if(!m_pGlyphMap->SetIconFaceByName(IconFace.u.string.ptr))
1317 {
1318 Success = false;
1319 }
1320 }
1321 else
1322 {
1323 log_error("textrender", "Font index malformed: 'icon' must be a string");
1324 Success = false;
1325 }
1326
1327 json_value_free(pJsonData);
1328 return Success;
1329 }
1330
1331 void SetFontPreset(EFontPreset FontPreset) override
1332 {
1333 m_pGlyphMap->SetFontPreset(FontPreset);
1334 }
1335
1336 void SetFontLanguageVariant(const char *pLanguageFile) override
1337 {
1338 for(const auto &Variant : m_vVariants)
1339 {
1340 if(str_comp(a: pLanguageFile, b: Variant.m_aLanguageFile) == 0)
1341 {
1342 m_pGlyphMap->SetVariantFaceByName(Variant.m_aFamilyName);
1343 return;
1344 }
1345 }
1346 m_pGlyphMap->SetVariantFaceByName(nullptr);
1347 }
1348
1349 void Text(float x, float y, float FontSize, const char *pText, float LineWidth = -1.0f) override
1350 {
1351 CTextCursor Cursor;
1352 Cursor.SetPosition(vec2(x, y));
1353 Cursor.m_FontSize = FontSize;
1354 Cursor.m_LineWidth = LineWidth;
1355 TextEx(pCursor: &Cursor, pText, Length: -1);
1356 }
1357
1358 float TextWidth(float FontSize, const char *pText, int StrLength = -1, float LineWidth = -1.0f, int Flags = 0, const STextSizeProperties &TextSizeProps = {}) override
1359 {
1360 CTextCursor Cursor;
1361 Cursor.m_FontSize = FontSize;
1362 Cursor.m_Flags = Flags;
1363 Cursor.m_LineWidth = LineWidth;
1364 TextEx(pCursor: &Cursor, pText, Length: StrLength);
1365 if(TextSizeProps.m_pHeight != nullptr)
1366 *TextSizeProps.m_pHeight = Cursor.Height();
1367 if(TextSizeProps.m_pAlignedFontSize != nullptr)
1368 *TextSizeProps.m_pAlignedFontSize = Cursor.m_AlignedFontSize;
1369 if(TextSizeProps.m_pMaxCharacterHeightInLine != nullptr)
1370 *TextSizeProps.m_pMaxCharacterHeightInLine = Cursor.m_MaxCharacterHeight;
1371 if(TextSizeProps.m_pLineCount != nullptr)
1372 *TextSizeProps.m_pLineCount = Cursor.m_LineCount;
1373 return Cursor.m_LongestLineWidth;
1374 }
1375
1376 STextBoundingBox TextBoundingBox(float FontSize, const char *pText, int StrLength = -1, float LineWidth = -1.0f, float LineSpacing = 0.0f, int Flags = 0) override
1377 {
1378 CTextCursor Cursor;
1379 Cursor.m_FontSize = FontSize;
1380 Cursor.m_Flags = Flags;
1381 Cursor.m_LineWidth = LineWidth;
1382 Cursor.m_LineSpacing = LineSpacing;
1383 TextEx(pCursor: &Cursor, pText, Length: StrLength);
1384 return Cursor.BoundingBox();
1385 }
1386
1387 void TextColor(float r, float g, float b, float a) override
1388 {
1389 m_Color.r = r;
1390 m_Color.g = g;
1391 m_Color.b = b;
1392 m_Color.a = a;
1393 }
1394
1395 void TextColor(ColorRGBA Color) override
1396 {
1397 m_Color = Color;
1398 }
1399
1400 void TextOutlineColor(float r, float g, float b, float a) override
1401 {
1402 m_OutlineColor.r = r;
1403 m_OutlineColor.g = g;
1404 m_OutlineColor.b = b;
1405 m_OutlineColor.a = a;
1406 }
1407
1408 void TextOutlineColor(ColorRGBA Color) override
1409 {
1410 m_OutlineColor = Color;
1411 }
1412
1413 void TextSelectionColor(float r, float g, float b, float a) override
1414 {
1415 m_SelectionColor.r = r;
1416 m_SelectionColor.g = g;
1417 m_SelectionColor.b = b;
1418 m_SelectionColor.a = a;
1419 }
1420
1421 void TextSelectionColor(ColorRGBA Color) override
1422 {
1423 m_SelectionColor = Color;
1424 }
1425
1426 ColorRGBA GetTextColor() const override
1427 {
1428 return m_Color;
1429 }
1430
1431 ColorRGBA GetTextOutlineColor() const override
1432 {
1433 return m_OutlineColor;
1434 }
1435
1436 ColorRGBA GetTextSelectionColor() const override
1437 {
1438 return m_SelectionColor;
1439 }
1440
1441 void TextEx(CTextCursor *pCursor, const char *pText, int Length = -1) override
1442 {
1443 const unsigned OldRenderFlags = m_RenderFlags;
1444 m_RenderFlags |= TEXT_RENDER_FLAG_ONE_TIME_USE;
1445 STextContainerIndex TextCont;
1446 CreateTextContainer(TextContainerIndex&: TextCont, pCursor, pText, Length);
1447 m_RenderFlags = OldRenderFlags;
1448 if(TextCont.Valid())
1449 {
1450 if((pCursor->m_Flags & TEXTFLAG_RENDER) != 0)
1451 {
1452 ColorRGBA TextColor = DefaultTextColor();
1453 ColorRGBA TextColorOutline = DefaultTextOutlineColor();
1454 RenderTextContainer(TextContainerIndex: TextCont, TextColor, TextOutlineColor: TextColorOutline);
1455 }
1456 DeleteTextContainer(TextContainerIndex&: TextCont);
1457 }
1458 }
1459
1460 bool CreateTextContainer(STextContainerIndex &TextContainerIndex, CTextCursor *pCursor, const char *pText, int Length = -1) override
1461 {
1462 dbg_assert(!TextContainerIndex.Valid(), "Text container index was not cleared.");
1463
1464 TextContainerIndex.Reset();
1465 TextContainerIndex.m_Index = GetFreeTextContainerIndex();
1466
1467 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
1468 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
1469
1470 STextContainer &TextContainer = GetTextContainer(Index: TextContainerIndex);
1471 TextContainer.m_SingleTimeUse = (m_RenderFlags & TEXT_RENDER_FLAG_ONE_TIME_USE) != 0;
1472 const vec2 FakeToScreen = vec2(Graphics()->ScreenWidth() / (ScreenX1 - ScreenX0), Graphics()->ScreenHeight() / (ScreenY1 - ScreenY0));
1473 TextContainer.m_AlignedStartX = round_to_int(f: pCursor->m_X * FakeToScreen.x) / FakeToScreen.x;
1474 TextContainer.m_AlignedStartY = round_to_int(f: pCursor->m_Y * FakeToScreen.y) / FakeToScreen.y;
1475 TextContainer.m_X = pCursor->m_X;
1476 TextContainer.m_Y = pCursor->m_Y;
1477 TextContainer.m_Flags = pCursor->m_Flags;
1478
1479 if(pCursor->m_LineWidth <= 0.0f)
1480 TextContainer.m_RenderFlags = m_RenderFlags | ETextRenderFlags::TEXT_RENDER_FLAG_NO_FIRST_CHARACTER_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_LAST_CHARACTER_ADVANCE;
1481 else
1482 TextContainer.m_RenderFlags = m_RenderFlags;
1483
1484 AppendTextContainer(TextContainerIndex, pCursor, pText, Length);
1485
1486 const bool IsRendered = (pCursor->m_Flags & TEXTFLAG_RENDER) != 0;
1487
1488 if(TextContainer.m_StringInfo.m_vCharacterQuads.empty() && TextContainer.m_StringInfo.m_SelectionQuadContainerIndex == -1 && IsRendered)
1489 {
1490 FreeTextContainer(Index&: TextContainerIndex);
1491 return false;
1492 }
1493 else
1494 {
1495 if(Graphics()->IsTextBufferingEnabled() && IsRendered && !TextContainer.m_StringInfo.m_vCharacterQuads.empty())
1496 {
1497 if((TextContainer.m_RenderFlags & TEXT_RENDER_FLAG_NO_AUTOMATIC_QUAD_UPLOAD) == 0)
1498 {
1499 UploadTextContainer(TextContainerIndex);
1500 }
1501 }
1502
1503 TextContainer.m_LineCount = pCursor->m_LineCount;
1504 TextContainer.m_GlyphCount = pCursor->m_GlyphCount;
1505 TextContainer.m_CharCount = pCursor->m_CharCount;
1506 TextContainer.m_MaxLines = pCursor->m_MaxLines;
1507 TextContainer.m_LineWidth = pCursor->m_LineWidth;
1508 return true;
1509 }
1510 }
1511
1512 void AppendTextContainer(STextContainerIndex TextContainerIndex, CTextCursor *pCursor, const char *pText, int Length = -1) override
1513 {
1514 STextContainer &TextContainer = GetTextContainer(Index: TextContainerIndex);
1515 str_append(dst&: TextContainer.m_aDebugText, src: pText);
1516
1517 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
1518 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
1519
1520 const vec2 FakeToScreen = vec2(Graphics()->ScreenWidth() / (ScreenX1 - ScreenX0), Graphics()->ScreenHeight() / (ScreenY1 - ScreenY0));
1521 const float CursorX = round_to_int(f: pCursor->m_X * FakeToScreen.x) / FakeToScreen.x;
1522 const float CursorY = round_to_int(f: pCursor->m_Y * FakeToScreen.y) / FakeToScreen.y;
1523 const int ActualSize = round_truncate(f: pCursor->m_FontSize * FakeToScreen.y);
1524 pCursor->m_AlignedFontSize = ActualSize / FakeToScreen.y;
1525 pCursor->m_AlignedLineSpacing = round_truncate(f: pCursor->m_LineSpacing * FakeToScreen.y) / FakeToScreen.y;
1526
1527 // string length
1528 if(Length < 0)
1529 Length = str_length(str: pText);
1530 else
1531 Length = minimum(a: Length, b: str_length(str: pText));
1532
1533 const char *pCurrent = pText;
1534 const char *pEnd = pCurrent + Length;
1535 const char *pPrevBatchEnd = nullptr;
1536 const char *pEllipsis = "…";
1537 const SGlyph *pEllipsisGlyph = nullptr;
1538 if(pCursor->m_Flags & TEXTFLAG_ELLIPSIS_AT_END)
1539 {
1540 if(pCursor->m_LineWidth > 0.0f && pCursor->m_LineWidth < TextWidth(FontSize: pCursor->m_FontSize, pText))
1541 {
1542 pEllipsisGlyph = m_pGlyphMap->GetGlyph(Chr: 0x2026, FontSize: ActualSize); // …
1543 if(pEllipsisGlyph == nullptr)
1544 {
1545 // no ellipsis char in font, just stop at end instead
1546 pCursor->m_Flags &= ~TEXTFLAG_ELLIPSIS_AT_END;
1547 pCursor->m_Flags |= TEXTFLAG_STOP_AT_END;
1548 }
1549 }
1550 }
1551
1552 const unsigned RenderFlags = TextContainer.m_RenderFlags;
1553
1554 float DrawX = 0.0f, DrawY = 0.0f;
1555 if((RenderFlags & TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT) != 0)
1556 {
1557 DrawX = pCursor->m_X;
1558 DrawY = pCursor->m_Y;
1559 }
1560 else
1561 {
1562 DrawX = CursorX;
1563 DrawY = CursorY;
1564 }
1565
1566 int LineCount = pCursor->m_LineCount;
1567
1568 const bool IsRendered = (pCursor->m_Flags & TEXTFLAG_RENDER) != 0;
1569
1570 const float CursorInnerWidth = (((ScreenX1 - ScreenX0) / Graphics()->ScreenWidth())) * 2;
1571 const float CursorOuterWidth = CursorInnerWidth * 2;
1572 const float CursorOuterInnerDiff = (CursorOuterWidth - CursorInnerWidth) / 2;
1573
1574 std::vector<IGraphics::CQuadItem> vSelectionQuads;
1575 int SelectionQuadLine = -1;
1576 bool SelectionStarted = false;
1577 bool SelectionUsedPress = false;
1578 bool SelectionUsedRelease = false;
1579 int SelectionStartChar = -1;
1580 int SelectionEndChar = -1;
1581
1582 const auto &&CheckInsideChar = [&](bool CheckOuter, vec2 CursorPos, float LastCharX, float LastCharWidth, float CharX, float CharWidth, float CharY) -> bool {
1583 return (LastCharX - LastCharWidth / 2 <= CursorPos.x &&
1584 CharX + CharWidth / 2 > CursorPos.x &&
1585 CursorPos.y >= CharY - pCursor->m_AlignedFontSize &&
1586 CursorPos.y < CharY + pCursor->m_AlignedLineSpacing) ||
1587 (CheckOuter &&
1588 CursorPos.y <= CharY - pCursor->m_AlignedFontSize);
1589 };
1590 const auto &&CheckSelectionStart = [&](bool CheckOuter, vec2 CursorPos, int &SelectionChar, bool &SelectionUsedCase, float LastCharX, float LastCharWidth, float CharX, float CharWidth, float CharY) {
1591 if(!SelectionStarted && !SelectionUsedCase &&
1592 CheckInsideChar(CheckOuter, CursorPos, LastCharX, LastCharWidth, CharX, CharWidth, CharY))
1593 {
1594 SelectionChar = pCursor->m_GlyphCount;
1595 SelectionStarted = !SelectionStarted;
1596 SelectionUsedCase = true;
1597 }
1598 };
1599 const auto &&CheckOutsideChar = [&](bool CheckOuter, vec2 CursorPos, float CharX, float CharWidth, float CharY) -> bool {
1600 return (CharX + CharWidth / 2 > CursorPos.x &&
1601 CursorPos.y >= CharY - pCursor->m_AlignedFontSize &&
1602 CursorPos.y < CharY + pCursor->m_AlignedLineSpacing) ||
1603 (CheckOuter &&
1604 CursorPos.y >= CharY + pCursor->m_AlignedLineSpacing);
1605 };
1606 const auto &&CheckSelectionEnd = [&](bool CheckOuter, vec2 CursorPos, int &SelectionChar, bool &SelectionUsedCase, float CharX, float CharWidth, float CharY) {
1607 if(SelectionStarted && !SelectionUsedCase &&
1608 CheckOutsideChar(CheckOuter, CursorPos, CharX, CharWidth, CharY))
1609 {
1610 SelectionChar = pCursor->m_GlyphCount;
1611 SelectionStarted = !SelectionStarted;
1612 SelectionUsedCase = true;
1613 }
1614 };
1615
1616 float LastSelX = DrawX;
1617 float LastSelWidth = 0;
1618 float LastCharX = DrawX;
1619 float LastCharWidth = 0;
1620
1621 // Returns true if line was started
1622 const auto &&StartNewLine = [&]() {
1623 if(pCursor->m_MaxLines > 0 && LineCount >= pCursor->m_MaxLines)
1624 return false;
1625
1626 DrawX = pCursor->m_StartX;
1627 DrawY += pCursor->m_AlignedFontSize + pCursor->m_AlignedLineSpacing;
1628 if((RenderFlags & TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT) == 0)
1629 {
1630 DrawX = round_to_int(f: DrawX * FakeToScreen.x) / FakeToScreen.x; // realign
1631 DrawY = round_to_int(f: DrawY * FakeToScreen.y) / FakeToScreen.y;
1632 }
1633 LastSelX = DrawX;
1634 LastSelWidth = 0;
1635 LastCharX = DrawX;
1636 LastCharWidth = 0;
1637 ++LineCount;
1638 return true;
1639 };
1640
1641 if(pCursor->m_CalculateSelectionMode != TEXT_CURSOR_SELECTION_MODE_NONE || pCursor->m_CursorMode != TEXT_CURSOR_CURSOR_MODE_NONE)
1642 {
1643 if(IsRendered)
1644 Graphics()->QuadContainerReset(ContainerIndex: TextContainer.m_StringInfo.m_SelectionQuadContainerIndex);
1645
1646 // if in calculate mode, also calculate the cursor
1647 if(pCursor->m_CursorMode == TEXT_CURSOR_CURSOR_MODE_CALCULATE)
1648 pCursor->m_CursorCharacter = -1;
1649 }
1650
1651 IGraphics::CQuadItem aCursorQuads[2];
1652 bool HasCursor = false;
1653
1654 const SGlyph *pLastGlyph = nullptr;
1655 bool GotNewLineLast = false;
1656
1657 int ColorOption = 0;
1658
1659 while(pCurrent < pEnd && pCurrent != pEllipsis)
1660 {
1661 bool NewLine = false;
1662 const char *pBatchEnd = pEnd;
1663 if(pCursor->m_LineWidth > 0.0f && !(pCursor->m_Flags & TEXTFLAG_STOP_AT_END) && !(pCursor->m_Flags & TEXTFLAG_ELLIPSIS_AT_END))
1664 {
1665 int Wlen = minimum(a: WordLength(pText: pCurrent), b: (int)(pEnd - pCurrent));
1666 CTextCursor Compare = *pCursor;
1667 Compare.m_CalculateSelectionMode = TEXT_CURSOR_SELECTION_MODE_NONE;
1668 Compare.m_CursorMode = TEXT_CURSOR_CURSOR_MODE_NONE;
1669 Compare.m_X = DrawX;
1670 Compare.m_Y = DrawY;
1671 Compare.m_Flags &= ~TEXTFLAG_RENDER;
1672 Compare.m_Flags |= TEXTFLAG_DISALLOW_NEWLINE;
1673 Compare.m_LineWidth = -1.0f;
1674 TextEx(pCursor: &Compare, pText: pCurrent, Length: Wlen);
1675
1676 if(Compare.m_X - DrawX > pCursor->m_LineWidth)
1677 {
1678 // word can't be fitted in one line, cut it
1679 CTextCursor Cutter = *pCursor;
1680 Cutter.m_CalculateSelectionMode = TEXT_CURSOR_SELECTION_MODE_NONE;
1681 Cutter.m_CursorMode = TEXT_CURSOR_CURSOR_MODE_NONE;
1682 Cutter.m_GlyphCount = 0;
1683 Cutter.m_CharCount = 0;
1684 Cutter.m_X = DrawX;
1685 Cutter.m_Y = DrawY;
1686 Cutter.m_Flags &= ~TEXTFLAG_RENDER;
1687 Cutter.m_Flags |= TEXTFLAG_STOP_AT_END | TEXTFLAG_DISALLOW_NEWLINE;
1688
1689 TextEx(pCursor: &Cutter, pText: pCurrent, Length: Wlen);
1690 Wlen = str_utf8_rewind(str: pCurrent, cursor: Cutter.m_CharCount); // rewind once to skip the last character that did not fit
1691 NewLine = true;
1692
1693 if(Cutter.m_GlyphCount <= 3 && !GotNewLineLast) // if we can't place 3 chars of the word on this line, take the next
1694 Wlen = 0;
1695 }
1696 else if(Compare.m_X - pCursor->m_StartX > pCursor->m_LineWidth && !GotNewLineLast)
1697 {
1698 NewLine = true;
1699 Wlen = 0;
1700 }
1701
1702 pBatchEnd = pCurrent + Wlen;
1703 }
1704
1705 const char *pTmp = pCurrent;
1706 int NextCharacter = str_utf8_decode(ptr: &pTmp);
1707
1708 while(pCurrent < pBatchEnd && pCurrent != pEllipsis)
1709 {
1710 const int PrevCharCount = pCursor->m_CharCount;
1711 pCursor->m_CharCount += pTmp - pCurrent;
1712 pCurrent = pTmp;
1713 int Character = NextCharacter;
1714 NextCharacter = str_utf8_decode(ptr: &pTmp);
1715
1716 if(Character == '\n')
1717 {
1718 if((pCursor->m_Flags & TEXTFLAG_DISALLOW_NEWLINE) == 0)
1719 {
1720 pLastGlyph = nullptr;
1721 if(!StartNewLine())
1722 break;
1723 continue;
1724 }
1725 else
1726 {
1727 Character = ' ';
1728 }
1729 }
1730
1731 const SGlyph *pGlyph = m_pGlyphMap->GetGlyph(Chr: Character, FontSize: ActualSize);
1732 if(pGlyph)
1733 {
1734 const float Scale = 1.0f / pGlyph->m_FontSize;
1735
1736 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));
1737 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;
1738
1739 const float OutLineRealDiff = (pGlyph->m_Width - pGlyph->m_CharWidth) * Scale * pCursor->m_AlignedFontSize;
1740
1741 float CharKerning = 0.0f;
1742 if((RenderFlags & TEXT_RENDER_FLAG_KERNING) != 0)
1743 CharKerning = m_pGlyphMap->Kerning(pLeft: pLastGlyph, pRight: pGlyph).x * Scale * pCursor->m_AlignedFontSize;
1744 pLastGlyph = pGlyph;
1745
1746 if(pEllipsisGlyph != nullptr && pCursor->m_Flags & TEXTFLAG_ELLIPSIS_AT_END && pCurrent < pBatchEnd && pCurrent != pEllipsis)
1747 {
1748 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;
1749 float CharKerningEllipsis = 0.0f;
1750 if((RenderFlags & TEXT_RENDER_FLAG_KERNING) != 0)
1751 {
1752 CharKerningEllipsis = m_pGlyphMap->Kerning(pLeft: pGlyph, pRight: pEllipsisGlyph).x * Scale * pCursor->m_AlignedFontSize;
1753 }
1754 if(pCursor->m_LineWidth > 0.0f &&
1755 DrawX + CharKerning + Advance + CharKerningEllipsis + AdvanceEllipsis - pCursor->m_StartX > pCursor->m_LineWidth)
1756 {
1757 // we hit the end, only render ellipsis and finish
1758 pTmp = pEllipsis;
1759 NextCharacter = 0x2026;
1760 pCursor->m_Truncated = true;
1761 continue;
1762 }
1763 }
1764
1765 if(pCursor->m_LineWidth > 0.0f &&
1766 (pCursor->m_Flags & TEXTFLAG_STOP_AT_END) != 0 &&
1767 (DrawX + CharKerning) + Advance - pCursor->m_StartX > pCursor->m_LineWidth)
1768 {
1769 // we hit the end of the line, no more to render or count
1770 pCurrent = pEnd;
1771 pCursor->m_Truncated = true;
1772 break;
1773 }
1774
1775 float BearingX = (!ApplyBearingX ? 0.f : pGlyph->m_OffsetX) * Scale * pCursor->m_AlignedFontSize;
1776 float CharWidth = pGlyph->m_Width * Scale * pCursor->m_AlignedFontSize;
1777
1778 float BearingY = (((RenderFlags & TEXT_RENDER_FLAG_NO_Y_BEARING) != 0) ? 0.f : (pGlyph->m_OffsetY * Scale * pCursor->m_AlignedFontSize));
1779 float CharHeight = pGlyph->m_Height * Scale * pCursor->m_AlignedFontSize;
1780
1781 if((RenderFlags & TEXT_RENDER_FLAG_NO_OVERSIZE) != 0)
1782 {
1783 if(CharHeight + BearingY > pCursor->m_AlignedFontSize)
1784 {
1785 BearingY = 0;
1786 float ScaleChar = (CharHeight + BearingY) / pCursor->m_AlignedFontSize;
1787 CharHeight = pCursor->m_AlignedFontSize;
1788 CharWidth /= ScaleChar;
1789 }
1790 }
1791
1792 const float TmpY = (DrawY + pCursor->m_AlignedFontSize);
1793 const float CharX = (DrawX + CharKerning) + BearingX;
1794 const float CharY = TmpY - BearingY;
1795
1796 // Check if we have any color split
1797 ColorRGBA Color = m_Color;
1798 if(ColorOption < (int)pCursor->m_vColorSplits.size())
1799 {
1800 STextColorSplit &Split = pCursor->m_vColorSplits.at(n: ColorOption);
1801 if(PrevCharCount >= Split.m_CharIndex && (Split.m_Length == -1 || PrevCharCount < Split.m_CharIndex + Split.m_Length))
1802 Color = Split.m_Color;
1803 if(Split.m_Length != -1 && PrevCharCount >= (Split.m_CharIndex + Split.m_Length - 1))
1804 {
1805 ColorOption++;
1806 if(ColorOption < (int)pCursor->m_vColorSplits.size())
1807 { // Handle splits that are
1808 Split = pCursor->m_vColorSplits.at(n: ColorOption);
1809 if(PrevCharCount >= Split.m_CharIndex)
1810 Color = Split.m_Color;
1811 }
1812 }
1813 }
1814
1815 // don't add text that isn't drawn, the color overwrite is used for that
1816 if(Color.a != 0.f && IsRendered)
1817 {
1818 TextContainer.m_StringInfo.m_vCharacterQuads.emplace_back();
1819 STextCharQuad &TextCharQuad = TextContainer.m_StringInfo.m_vCharacterQuads.back();
1820
1821 TextCharQuad.m_aVertices[0].m_X = CharX;
1822 TextCharQuad.m_aVertices[0].m_Y = CharY;
1823 TextCharQuad.m_aVertices[0].m_U = pGlyph->m_aUVs[0];
1824 TextCharQuad.m_aVertices[0].m_V = pGlyph->m_aUVs[3];
1825 TextCharQuad.m_aVertices[0].m_Color.r = (unsigned char)(Color.r * 255.f);
1826 TextCharQuad.m_aVertices[0].m_Color.g = (unsigned char)(Color.g * 255.f);
1827 TextCharQuad.m_aVertices[0].m_Color.b = (unsigned char)(Color.b * 255.f);
1828 TextCharQuad.m_aVertices[0].m_Color.a = (unsigned char)(Color.a * 255.f);
1829
1830 TextCharQuad.m_aVertices[1].m_X = CharX + CharWidth;
1831 TextCharQuad.m_aVertices[1].m_Y = CharY;
1832 TextCharQuad.m_aVertices[1].m_U = pGlyph->m_aUVs[2];
1833 TextCharQuad.m_aVertices[1].m_V = pGlyph->m_aUVs[3];
1834 TextCharQuad.m_aVertices[1].m_Color.r = (unsigned char)(Color.r * 255.f);
1835 TextCharQuad.m_aVertices[1].m_Color.g = (unsigned char)(Color.g * 255.f);
1836 TextCharQuad.m_aVertices[1].m_Color.b = (unsigned char)(Color.b * 255.f);
1837 TextCharQuad.m_aVertices[1].m_Color.a = (unsigned char)(Color.a * 255.f);
1838
1839 TextCharQuad.m_aVertices[2].m_X = CharX + CharWidth;
1840 TextCharQuad.m_aVertices[2].m_Y = CharY - CharHeight;
1841 TextCharQuad.m_aVertices[2].m_U = pGlyph->m_aUVs[2];
1842 TextCharQuad.m_aVertices[2].m_V = pGlyph->m_aUVs[1];
1843 TextCharQuad.m_aVertices[2].m_Color.r = (unsigned char)(Color.r * 255.f);
1844 TextCharQuad.m_aVertices[2].m_Color.g = (unsigned char)(Color.g * 255.f);
1845 TextCharQuad.m_aVertices[2].m_Color.b = (unsigned char)(Color.b * 255.f);
1846 TextCharQuad.m_aVertices[2].m_Color.a = (unsigned char)(Color.a * 255.f);
1847
1848 TextCharQuad.m_aVertices[3].m_X = CharX;
1849 TextCharQuad.m_aVertices[3].m_Y = CharY - CharHeight;
1850 TextCharQuad.m_aVertices[3].m_U = pGlyph->m_aUVs[0];
1851 TextCharQuad.m_aVertices[3].m_V = pGlyph->m_aUVs[1];
1852 TextCharQuad.m_aVertices[3].m_Color.r = (unsigned char)(Color.r * 255.f);
1853 TextCharQuad.m_aVertices[3].m_Color.g = (unsigned char)(Color.g * 255.f);
1854 TextCharQuad.m_aVertices[3].m_Color.b = (unsigned char)(Color.b * 255.f);
1855 TextCharQuad.m_aVertices[3].m_Color.a = (unsigned char)(Color.a * 255.f);
1856 }
1857
1858 // calculate the full width from the last selection point to the end of this selection draw on screen
1859 const float SelWidth = (CharX + maximum(a: Advance, b: CharWidth - OutLineRealDiff / 2)) - (LastSelX + LastSelWidth);
1860 const float SelX = (LastSelX + LastSelWidth);
1861
1862 if(pCursor->m_CursorMode == TEXT_CURSOR_CURSOR_MODE_CALCULATE)
1863 {
1864 if(pCursor->m_CursorCharacter == -1 && CheckInsideChar(pCursor->m_GlyphCount == 0, pCursor->m_ReleaseMouse, pCursor->m_GlyphCount == 0 ? std::numeric_limits<float>::lowest() : LastCharX, LastCharWidth, CharX, CharWidth, TmpY))
1865 {
1866 pCursor->m_CursorCharacter = pCursor->m_GlyphCount;
1867 }
1868 }
1869
1870 if(pCursor->m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_CALCULATE)
1871 {
1872 if(pCursor->m_GlyphCount == 0)
1873 {
1874 CheckSelectionStart(true, pCursor->m_PressMouse, SelectionStartChar, SelectionUsedPress, std::numeric_limits<float>::lowest(), 0, CharX, CharWidth, TmpY);
1875 CheckSelectionStart(true, pCursor->m_ReleaseMouse, SelectionEndChar, SelectionUsedRelease, std::numeric_limits<float>::lowest(), 0, CharX, CharWidth, TmpY);
1876 }
1877
1878 // if selection didn't start and the mouse pos is at least on 50% of the right side of the character start
1879 CheckSelectionStart(false, pCursor->m_PressMouse, SelectionStartChar, SelectionUsedPress, LastCharX, LastCharWidth, CharX, CharWidth, TmpY);
1880 CheckSelectionStart(false, pCursor->m_ReleaseMouse, SelectionEndChar, SelectionUsedRelease, LastCharX, LastCharWidth, CharX, CharWidth, TmpY);
1881 CheckSelectionEnd(false, pCursor->m_ReleaseMouse, SelectionEndChar, SelectionUsedRelease, CharX, CharWidth, TmpY);
1882 CheckSelectionEnd(false, pCursor->m_PressMouse, SelectionStartChar, SelectionUsedPress, CharX, CharWidth, TmpY);
1883 }
1884 if(pCursor->m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_SET)
1885 {
1886 if(pCursor->m_GlyphCount == pCursor->m_SelectionStart)
1887 {
1888 SelectionStarted = !SelectionStarted;
1889 SelectionStartChar = pCursor->m_GlyphCount;
1890 SelectionUsedPress = true;
1891 }
1892 if(pCursor->m_GlyphCount == pCursor->m_SelectionEnd)
1893 {
1894 SelectionStarted = !SelectionStarted;
1895 SelectionEndChar = pCursor->m_GlyphCount;
1896 SelectionUsedRelease = true;
1897 }
1898 }
1899
1900 if(pCursor->m_CursorMode != TEXT_CURSOR_CURSOR_MODE_NONE)
1901 {
1902 if(pCursor->m_GlyphCount == pCursor->m_CursorCharacter)
1903 {
1904 HasCursor = true;
1905 aCursorQuads[0] = IGraphics::CQuadItem(SelX - CursorOuterInnerDiff, DrawY, CursorOuterWidth, pCursor->m_AlignedFontSize);
1906 aCursorQuads[1] = IGraphics::CQuadItem(SelX, DrawY + CursorOuterInnerDiff, CursorInnerWidth, pCursor->m_AlignedFontSize - CursorOuterInnerDiff * 2);
1907 pCursor->m_CursorRenderedPosition = vec2(SelX, DrawY);
1908 }
1909 }
1910
1911 pCursor->m_MaxCharacterHeight = maximum(a: pCursor->m_MaxCharacterHeight, b: CharHeight + BearingY);
1912
1913 if(NextCharacter == 0 && (RenderFlags & TEXT_RENDER_FLAG_NO_LAST_CHARACTER_ADVANCE) != 0 && Character != ' ')
1914 DrawX += BearingX + CharKerning + CharWidth;
1915 else
1916 DrawX += Advance + CharKerning;
1917
1918 pCursor->m_GlyphCount++;
1919
1920 if(SelectionStarted && IsRendered)
1921 {
1922 if(!vSelectionQuads.empty() && SelectionQuadLine == LineCount)
1923 {
1924 vSelectionQuads.back().m_Width += SelWidth;
1925 }
1926 else
1927 {
1928 const float SelectionHeight = pCursor->m_AlignedFontSize + pCursor->m_AlignedLineSpacing;
1929 const float SelectionY = DrawY + (1.0f - pCursor->m_SelectionHeightFactor) * SelectionHeight;
1930 const float ScaledSelectionHeight = pCursor->m_SelectionHeightFactor * SelectionHeight;
1931 vSelectionQuads.emplace_back(args: SelX, args: SelectionY, args: SelWidth, args: ScaledSelectionHeight);
1932 SelectionQuadLine = LineCount;
1933 }
1934 }
1935
1936 LastSelX = SelX;
1937 LastSelWidth = SelWidth;
1938 LastCharX = CharX;
1939 LastCharWidth = CharWidth;
1940 }
1941
1942 pCursor->m_LongestLineWidth = maximum(a: pCursor->m_LongestLineWidth, b: DrawX - pCursor->m_StartX);
1943 }
1944
1945 if(NewLine)
1946 {
1947 if(pPrevBatchEnd == pBatchEnd)
1948 break;
1949 pPrevBatchEnd = pBatchEnd;
1950 if(!StartNewLine())
1951 break;
1952 GotNewLineLast = true;
1953 }
1954 else
1955 GotNewLineLast = false;
1956 }
1957
1958 if(!TextContainer.m_StringInfo.m_vCharacterQuads.empty() && IsRendered)
1959 {
1960 // setup the buffers
1961 if(Graphics()->IsTextBufferingEnabled())
1962 {
1963 const size_t DataSize = TextContainer.m_StringInfo.m_vCharacterQuads.size() * sizeof(STextCharQuad);
1964 void *pUploadData = TextContainer.m_StringInfo.m_vCharacterQuads.data();
1965
1966 if(TextContainer.m_StringInfo.m_QuadBufferObjectIndex != -1 && (TextContainer.m_RenderFlags & TEXT_RENDER_FLAG_NO_AUTOMATIC_QUAD_UPLOAD) == 0)
1967 {
1968 Graphics()->RecreateBufferObject(BufferIndex: TextContainer.m_StringInfo.m_QuadBufferObjectIndex, UploadDataSize: DataSize, pUploadData, CreateFlags: TextContainer.m_SingleTimeUse ? IGraphics::EBufferObjectCreateFlags::BUFFER_OBJECT_CREATE_FLAGS_ONE_TIME_USE_BIT : 0);
1969 Graphics()->IndicesNumRequiredNotify(RequiredIndicesCount: TextContainer.m_StringInfo.m_vCharacterQuads.size() * 6);
1970 }
1971 }
1972 }
1973
1974 if(pCursor->m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_CALCULATE)
1975 {
1976 pCursor->m_SelectionStart = -1;
1977 pCursor->m_SelectionEnd = -1;
1978
1979 if(SelectionStarted)
1980 {
1981 CheckSelectionEnd(true, pCursor->m_ReleaseMouse, SelectionEndChar, SelectionUsedRelease, std::numeric_limits<float>::max(), 0, DrawY + pCursor->m_AlignedFontSize);
1982 CheckSelectionEnd(true, pCursor->m_PressMouse, SelectionStartChar, SelectionUsedPress, std::numeric_limits<float>::max(), 0, DrawY + pCursor->m_AlignedFontSize);
1983 }
1984 }
1985 else if(pCursor->m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_SET)
1986 {
1987 if(pCursor->m_GlyphCount == pCursor->m_SelectionStart)
1988 {
1989 SelectionStarted = !SelectionStarted;
1990 SelectionStartChar = pCursor->m_GlyphCount;
1991 SelectionUsedPress = true;
1992 }
1993 if(pCursor->m_GlyphCount == pCursor->m_SelectionEnd)
1994 {
1995 SelectionStarted = !SelectionStarted;
1996 SelectionEndChar = pCursor->m_GlyphCount;
1997 SelectionUsedRelease = true;
1998 }
1999 }
2000
2001 if(pCursor->m_CursorMode != TEXT_CURSOR_CURSOR_MODE_NONE)
2002 {
2003 if(pCursor->m_CursorMode == TEXT_CURSOR_CURSOR_MODE_CALCULATE && pCursor->m_CursorCharacter == -1 && CheckOutsideChar(true, pCursor->m_ReleaseMouse, std::numeric_limits<float>::max(), 0, DrawY + pCursor->m_AlignedFontSize))
2004 {
2005 pCursor->m_CursorCharacter = pCursor->m_GlyphCount;
2006 }
2007
2008 if(pCursor->m_GlyphCount == pCursor->m_CursorCharacter)
2009 {
2010 HasCursor = true;
2011 aCursorQuads[0] = IGraphics::CQuadItem((LastSelX + LastSelWidth) - CursorOuterInnerDiff, DrawY, CursorOuterWidth, pCursor->m_AlignedFontSize);
2012 aCursorQuads[1] = IGraphics::CQuadItem((LastSelX + LastSelWidth), DrawY + CursorOuterInnerDiff, CursorInnerWidth, pCursor->m_AlignedFontSize - CursorOuterInnerDiff * 2);
2013 pCursor->m_CursorRenderedPosition = vec2(LastSelX + LastSelWidth, DrawY);
2014 }
2015 }
2016
2017 const bool HasSelection = !vSelectionQuads.empty() && SelectionUsedPress && SelectionUsedRelease;
2018 if((HasSelection || HasCursor) && IsRendered)
2019 {
2020 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
2021 if(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex == -1)
2022 TextContainer.m_StringInfo.m_SelectionQuadContainerIndex = Graphics()->CreateQuadContainer(AutomaticUpload: false);
2023 if(HasCursor)
2024 Graphics()->QuadContainerAddQuads(ContainerIndex: TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, pArray: aCursorQuads, Num: std::size(aCursorQuads));
2025 if(HasSelection)
2026 Graphics()->QuadContainerAddQuads(ContainerIndex: TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, pArray: vSelectionQuads.data(), Num: vSelectionQuads.size());
2027 Graphics()->QuadContainerUpload(ContainerIndex: TextContainer.m_StringInfo.m_SelectionQuadContainerIndex);
2028
2029 TextContainer.m_HasCursor = HasCursor;
2030 TextContainer.m_HasSelection = HasSelection;
2031 TextContainer.m_ForceCursorRendering = pCursor->m_ForceCursorRendering;
2032
2033 if(HasSelection)
2034 {
2035 pCursor->m_SelectionStart = SelectionStartChar;
2036 pCursor->m_SelectionEnd = SelectionEndChar;
2037 }
2038 else
2039 {
2040 pCursor->m_SelectionStart = -1;
2041 pCursor->m_SelectionEnd = -1;
2042 }
2043 }
2044
2045 // even if no text is drawn the cursor position will be adjusted
2046 pCursor->m_X = DrawX;
2047 pCursor->m_Y = DrawY;
2048 pCursor->m_LineCount = LineCount;
2049
2050 TextContainer.m_BoundingBox = pCursor->BoundingBox();
2051 }
2052
2053 bool CreateOrAppendTextContainer(STextContainerIndex &TextContainerIndex, CTextCursor *pCursor, const char *pText, int Length = -1) override
2054 {
2055 if(TextContainerIndex.Valid())
2056 {
2057 AppendTextContainer(TextContainerIndex, pCursor, pText, Length);
2058 return true;
2059 }
2060 else
2061 {
2062 return CreateTextContainer(TextContainerIndex, pCursor, pText, Length);
2063 }
2064 }
2065
2066 // just deletes and creates text container
2067 void RecreateTextContainer(STextContainerIndex &TextContainerIndex, CTextCursor *pCursor, const char *pText, int Length = -1) override
2068 {
2069 DeleteTextContainer(TextContainerIndex);
2070 CreateTextContainer(TextContainerIndex, pCursor, pText, Length);
2071 }
2072
2073 void RecreateTextContainerSoft(STextContainerIndex &TextContainerIndex, CTextCursor *pCursor, const char *pText, int Length = -1) override
2074 {
2075 STextContainer &TextContainer = GetTextContainer(Index: TextContainerIndex);
2076 TextContainer.m_StringInfo.m_vCharacterQuads.clear();
2077 // the text buffer gets then recreated by the appended quads
2078 AppendTextContainer(TextContainerIndex, pCursor, pText, Length);
2079 }
2080
2081 void DeleteTextContainer(STextContainerIndex &TextContainerIndex) override
2082 {
2083 if(!TextContainerIndex.Valid())
2084 return;
2085
2086 STextContainer &TextContainer = GetTextContainer(Index: TextContainerIndex);
2087 if(Graphics()->IsTextBufferingEnabled())
2088 Graphics()->DeleteBufferContainer(ContainerIndex&: TextContainer.m_StringInfo.m_QuadBufferContainerIndex, DestroyAllBO: true);
2089 Graphics()->DeleteQuadContainer(ContainerIndex&: TextContainer.m_StringInfo.m_SelectionQuadContainerIndex);
2090 FreeTextContainer(Index&: TextContainerIndex);
2091 }
2092
2093 void UploadTextContainer(STextContainerIndex TextContainerIndex) override
2094 {
2095 if(Graphics()->IsTextBufferingEnabled())
2096 {
2097 STextContainer &TextContainer = GetTextContainer(Index: TextContainerIndex);
2098 size_t DataSize = TextContainer.m_StringInfo.m_vCharacterQuads.size() * sizeof(STextCharQuad);
2099 void *pUploadData = TextContainer.m_StringInfo.m_vCharacterQuads.data();
2100 TextContainer.m_StringInfo.m_QuadBufferObjectIndex = Graphics()->CreateBufferObject(UploadDataSize: DataSize, pUploadData, CreateFlags: TextContainer.m_SingleTimeUse ? IGraphics::EBufferObjectCreateFlags::BUFFER_OBJECT_CREATE_FLAGS_ONE_TIME_USE_BIT : 0);
2101
2102 m_DefaultTextContainerInfo.m_VertBufferBindingIndex = TextContainer.m_StringInfo.m_QuadBufferObjectIndex;
2103
2104 TextContainer.m_StringInfo.m_QuadBufferContainerIndex = Graphics()->CreateBufferContainer(pContainerInfo: &m_DefaultTextContainerInfo);
2105 Graphics()->IndicesNumRequiredNotify(RequiredIndicesCount: TextContainer.m_StringInfo.m_vCharacterQuads.size() * 6);
2106 }
2107 }
2108
2109 void RenderTextContainer(STextContainerIndex TextContainerIndex, const ColorRGBA &TextColor, const ColorRGBA &TextOutlineColor) override
2110 {
2111 const STextContainer &TextContainer = GetTextContainer(Index: TextContainerIndex);
2112
2113 if(!TextContainer.m_StringInfo.m_vCharacterQuads.empty())
2114 {
2115 if(Graphics()->IsTextBufferingEnabled())
2116 {
2117 Graphics()->TextureClear();
2118 // render buffered text
2119 Graphics()->RenderText(BufferContainerIndex: TextContainer.m_StringInfo.m_QuadBufferContainerIndex, TextQuadNum: TextContainer.m_StringInfo.m_vCharacterQuads.size(), TextureSize: m_pGlyphMap->TextureDimension(), TextureTextIndex: m_pGlyphMap->Texture(TextureIndex: CGlyphMap::FONT_TEXTURE_FILL).Id(), TextureTextOutlineIndex: m_pGlyphMap->Texture(TextureIndex: CGlyphMap::FONT_TEXTURE_OUTLINE).Id(), TextColor, TextOutlineColor);
2120 }
2121 else
2122 {
2123 // render tiles
2124 const float UVScale = 1.0f / m_pGlyphMap->TextureDimension();
2125
2126 Graphics()->FlushVertices();
2127 Graphics()->TextureSet(Texture: m_pGlyphMap->Texture(TextureIndex: CGlyphMap::FONT_TEXTURE_OUTLINE));
2128
2129 Graphics()->QuadsBegin();
2130
2131 for(const STextCharQuad &TextCharQuad : TextContainer.m_StringInfo.m_vCharacterQuads)
2132 {
2133 Graphics()->SetColor(r: TextCharQuad.m_aVertices[0].m_Color.r / 255.f * TextOutlineColor.r, g: TextCharQuad.m_aVertices[0].m_Color.g / 255.f * TextOutlineColor.g, b: TextCharQuad.m_aVertices[0].m_Color.b / 255.f * TextOutlineColor.b, a: TextCharQuad.m_aVertices[0].m_Color.a / 255.f * TextOutlineColor.a);
2134 Graphics()->QuadsSetSubset(TopLeftU: TextCharQuad.m_aVertices[0].m_U * UVScale, TopLeftV: TextCharQuad.m_aVertices[0].m_V * UVScale, BottomRightU: TextCharQuad.m_aVertices[2].m_U * UVScale, BottomRightV: TextCharQuad.m_aVertices[2].m_V * UVScale);
2135 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);
2136 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
2137 }
2138
2139 if(TextColor.a != 0)
2140 {
2141 Graphics()->QuadsEndKeepVertices();
2142 Graphics()->TextureSet(Texture: m_pGlyphMap->Texture(TextureIndex: CGlyphMap::FONT_TEXTURE_FILL));
2143
2144 int TextCharQuadIndex = 0;
2145 for(const STextCharQuad &TextCharQuad : TextContainer.m_StringInfo.m_vCharacterQuads)
2146 {
2147 unsigned char CR = (unsigned char)((float)(TextCharQuad.m_aVertices[0].m_Color.r) * TextColor.r);
2148 unsigned char CG = (unsigned char)((float)(TextCharQuad.m_aVertices[0].m_Color.g) * TextColor.g);
2149 unsigned char CB = (unsigned char)((float)(TextCharQuad.m_aVertices[0].m_Color.b) * TextColor.b);
2150 unsigned char CA = (unsigned char)((float)(TextCharQuad.m_aVertices[0].m_Color.a) * TextColor.a);
2151 Graphics()->ChangeColorOfQuadVertices(QuadOffset: TextCharQuadIndex, r: CR, g: CG, b: CB, a: CA);
2152 ++TextCharQuadIndex;
2153 }
2154
2155 // render non outlined
2156 Graphics()->QuadsDrawCurrentVertices(KeepVertices: false);
2157 }
2158 else
2159 Graphics()->QuadsEnd();
2160
2161 // reset
2162 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
2163 }
2164 }
2165
2166 if(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex != -1)
2167 {
2168 if(TextContainer.m_HasSelection)
2169 {
2170 Graphics()->TextureClear();
2171 Graphics()->SetColor(m_SelectionColor);
2172 Graphics()->RenderQuadContainerEx(ContainerIndex: TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, QuadOffset: TextContainer.m_HasCursor ? 2 : 0, QuadDrawNum: -1, X: 0, Y: 0);
2173 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2174 }
2175
2176 if(TextContainer.m_HasCursor)
2177 {
2178 const auto CurTime = time_get_nanoseconds();
2179
2180 Graphics()->TextureClear();
2181 if(TextContainer.m_ForceCursorRendering || (CurTime - m_CursorRenderTime) > 500ms)
2182 {
2183 Graphics()->SetColor(TextOutlineColor);
2184 Graphics()->RenderQuadContainerEx(ContainerIndex: TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, QuadOffset: 0, QuadDrawNum: 1, X: 0, Y: 0);
2185 Graphics()->SetColor(TextColor);
2186 Graphics()->RenderQuadContainerEx(ContainerIndex: TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, QuadOffset: 1, QuadDrawNum: 1, X: 0, Y: 0);
2187 }
2188 if(TextContainer.m_ForceCursorRendering)
2189 m_CursorRenderTime = CurTime - 501ms;
2190 else if((CurTime - m_CursorRenderTime) > 1s)
2191 m_CursorRenderTime = time_get_nanoseconds();
2192 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2193 }
2194 }
2195 }
2196
2197 void RenderTextContainer(STextContainerIndex TextContainerIndex, const ColorRGBA &TextColor, const ColorRGBA &TextOutlineColor, float X, float Y) override
2198 {
2199 STextContainer &TextContainer = GetTextContainer(Index: TextContainerIndex);
2200
2201 // remap the current screen, after render revert the change again
2202 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
2203 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
2204
2205 if((TextContainer.m_RenderFlags & TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT) == 0)
2206 {
2207 const vec2 FakeToScreen = vec2(Graphics()->ScreenWidth() / (ScreenX1 - ScreenX0), Graphics()->ScreenHeight() / (ScreenY1 - ScreenY0));
2208 const float AlignedX = round_to_int(f: (TextContainer.m_X + X) * FakeToScreen.x) / FakeToScreen.x;
2209 const float AlignedY = round_to_int(f: (TextContainer.m_Y + Y) * FakeToScreen.y) / FakeToScreen.y;
2210 X = AlignedX - TextContainer.m_AlignedStartX;
2211 Y = AlignedY - TextContainer.m_AlignedStartY;
2212 }
2213
2214 TextContainer.m_BoundingBox.m_X = X;
2215 TextContainer.m_BoundingBox.m_Y = Y;
2216
2217 Graphics()->MapScreen(TopLeftX: ScreenX0 - X, TopLeftY: ScreenY0 - Y, BottomRightX: ScreenX1 - X, BottomRightY: ScreenY1 - Y);
2218 RenderTextContainer(TextContainerIndex, TextColor, TextOutlineColor);
2219 Graphics()->MapScreen(TopLeftX: ScreenX0, TopLeftY: ScreenY0, BottomRightX: ScreenX1, BottomRightY: ScreenY1);
2220 }
2221
2222 STextBoundingBox GetBoundingBoxTextContainer(STextContainerIndex TextContainerIndex) override
2223 {
2224 const STextContainer &TextContainer = GetTextContainer(Index: TextContainerIndex);
2225 return TextContainer.m_BoundingBox;
2226 }
2227
2228 void UploadEntityLayerText(const CImageInfo &TextImage, int TexSubWidth, int TexSubHeight, const char *pText, int Length, float x, float y, int FontSize) override
2229 {
2230 m_pGlyphMap->UploadEntityLayerText(TextImage, TexSubWidth, TexSubHeight, pText, Length, x, y, FontSize);
2231 }
2232
2233 int AdjustFontSize(const char *pText, int TextLength, int MaxSize, int MaxWidth) const override
2234 {
2235 const int WidthOfText = CalculateTextWidth(pText, TextLength, FontWidth: 0, FontHeight: 100);
2236
2237 int FontSize = 100.0f / ((float)WidthOfText / (float)MaxWidth);
2238 if(MaxSize > 0 && FontSize > MaxSize)
2239 FontSize = MaxSize;
2240
2241 return FontSize;
2242 }
2243
2244 float GetGlyphOffsetX(int FontSize, char TextCharacter) const override
2245 {
2246 if(m_pGlyphMap->DefaultFace() == nullptr)
2247 return -1.0f;
2248
2249 FT_Set_Pixel_Sizes(face: m_pGlyphMap->DefaultFace(), pixel_width: 0, pixel_height: FontSize);
2250 const char *pTmp = &TextCharacter;
2251 const int NextCharacter = str_utf8_decode(ptr: &pTmp);
2252
2253 if(NextCharacter)
2254 {
2255#if FREETYPE_MAJOR >= 2 && FREETYPE_MINOR >= 7 && (FREETYPE_MINOR > 7 || FREETYPE_PATCH >= 1)
2256 const FT_Int32 FTFlags = FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_BITMAP;
2257#else
2258 const FT_Int32 FTFlags = FT_LOAD_RENDER | FT_LOAD_NO_BITMAP;
2259#endif
2260 if(FT_Load_Char(face: m_pGlyphMap->DefaultFace(), char_code: NextCharacter, load_flags: FTFlags))
2261 {
2262 log_debug("textrender", "Error loading glyph. Chr=%d", NextCharacter);
2263 return -1.0f;
2264 }
2265
2266 return (float)(m_pGlyphMap->DefaultFace()->glyph->metrics.horiBearingX >> 6);
2267 }
2268 return 0.0f;
2269 }
2270
2271 int CalculateTextWidth(const char *pText, int TextLength, int FontWidth, int FontHeight) const override
2272 {
2273 if(m_pGlyphMap->DefaultFace() == nullptr)
2274 return 0;
2275
2276 const char *pCurrent = pText;
2277 const char *pEnd = pCurrent + TextLength;
2278
2279 int WidthOfText = 0;
2280 FT_Set_Pixel_Sizes(face: m_pGlyphMap->DefaultFace(), pixel_width: FontWidth, pixel_height: FontHeight);
2281 while(pCurrent < pEnd)
2282 {
2283 const char *pTmp = pCurrent;
2284 const int NextCharacter = str_utf8_decode(ptr: &pTmp);
2285 if(NextCharacter)
2286 {
2287#if FREETYPE_MAJOR >= 2 && FREETYPE_MINOR >= 7 && (FREETYPE_MINOR > 7 || FREETYPE_PATCH >= 1)
2288 const FT_Int32 FTFlags = FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_BITMAP;
2289#else
2290 const FT_Int32 FTFlags = FT_LOAD_RENDER | FT_LOAD_NO_BITMAP;
2291#endif
2292 if(FT_Load_Char(face: m_pGlyphMap->DefaultFace(), char_code: NextCharacter, load_flags: FTFlags))
2293 {
2294 log_debug("textrender", "Error loading glyph. Chr=%d", NextCharacter);
2295 pCurrent = pTmp;
2296 continue;
2297 }
2298
2299 WidthOfText += (m_pGlyphMap->DefaultFace()->glyph->metrics.width >> 6) + 1;
2300 }
2301 pCurrent = pTmp;
2302 }
2303
2304 return WidthOfText;
2305 }
2306
2307 void OnPreWindowResize() override
2308 {
2309 for(auto *pTextContainer : m_vpTextContainers)
2310 {
2311 if(pTextContainer->m_ContainerIndex.Valid() && pTextContainer->m_ContainerIndex.m_UseCount.use_count() <= 1)
2312 {
2313 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);
2314 dbg_assert_failed("Text container was forgotten by the implementation (the index was overwritten).");
2315 }
2316 }
2317 }
2318
2319 void OnWindowResize() override
2320 {
2321 bool HasNonEmptyTextContainer = false;
2322 for(auto *pTextContainer : m_vpTextContainers)
2323 {
2324 if(pTextContainer->m_StringInfo.m_QuadBufferContainerIndex != -1)
2325 {
2326 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);
2327 log_error("textrender", "The text container index was in use by %d ", (int)pTextContainer->m_ContainerIndex.m_UseCount.use_count());
2328 HasNonEmptyTextContainer = true;
2329 }
2330 }
2331
2332 dbg_assert(!HasNonEmptyTextContainer, "text container was not empty");
2333 }
2334};
2335
2336IEngineTextRender *CreateEngineTextRender() { return new CTextRender; }
2337