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