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 | |
24 | using namespace std::chrono_literals; |
25 | |
26 | enum |
27 | { |
28 | FONT_NAME_SIZE = 128, |
29 | }; |
30 | |
31 | struct 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 | |
59 | struct 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 | |
71 | struct 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 | |
79 | class 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 | |
174 | public: |
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 | |
274 | class CGlyphMap |
275 | { |
276 | public: |
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 | |
284 | private: |
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 | |
582 | public: |
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 | |
813 | typedef vector4_base<unsigned char> STextCharQuadVertexColor; |
814 | |
815 | struct 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 | |
827 | struct STextCharQuad |
828 | { |
829 | STextCharQuadVertex m_aVertices[4]; |
830 | }; |
831 | |
832 | struct SStringInfo |
833 | { |
834 | int m_QuadBufferObjectIndex; |
835 | int m_QuadBufferContainerIndex; |
836 | int m_SelectionQuadContainerIndex; |
837 | |
838 | std::vector<STextCharQuad> m_vCharacterQuads; |
839 | }; |
840 | |
841 | struct 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 | |
904 | struct SFontLanguageVariant |
905 | { |
906 | char m_aLanguageFile[IO_MAX_PATH_LENGTH]; |
907 | char m_aFamilyName[FONT_NAME_SIZE]; |
908 | }; |
909 | |
910 | class 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 | |
1056 | public: |
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_CharCount; |
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(pCursor->m_GlyphCount == pCursor->m_SelectionStart) |
1842 | { |
1843 | SelectionStarted = !SelectionStarted; |
1844 | SelectionStartChar = pCursor->m_GlyphCount; |
1845 | SelectionUsedPress = true; |
1846 | } |
1847 | if(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(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(pCursor->m_GlyphCount == pCursor->m_SelectionStart) |
1940 | { |
1941 | SelectionStarted = !SelectionStarted; |
1942 | SelectionStartChar = pCursor->m_GlyphCount; |
1943 | SelectionUsedPress = true; |
1944 | } |
1945 | if(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(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 | |
2288 | IEngineTextRender *CreateEngineTextRender() { return new CTextRender; } |
2289 | |