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