1 /*
2  * This file is part of the Colobot: Gold Edition source code
3  * Copyright (C) 2001-2020, Daniel Roux, EPSITEC SA & TerranovaTeam
4  * http://epsitec.ch; http://colobot.info; http://github.com/colobot
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14  * See the GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program. If not, see http://gnu.org/licenses
18  */
19 
20 
21 #include "graphics/engine/text.h"
22 
23 #include "app/app.h"
24 
25 #include "common/font_loader.h"
26 #include "common/image.h"
27 #include "common/logger.h"
28 #include "common/stringutils.h"
29 
30 #include "common/resources/resourcemanager.h"
31 
32 #include "graphics/engine/engine.h"
33 
34 #include "math/func.h"
35 
36 #include <algorithm>
37 #include <SDL.h>
38 #include <SDL_ttf.h>
39 
40 
41 // Graphics module namespace
42 namespace Gfx
43 {
44 
45 /**
46  * \struct MultisizeFont
47  * \brief Font with multiple possible sizes
48  */
49 struct MultisizeFont
50 {
51     std::string fileName;
52     std::map<int, std::unique_ptr<CachedFont>> fonts;
53 
MultisizeFontGfx::MultisizeFont54     explicit MultisizeFont(const std::string &fn)
55         : fileName(fn) {}
56 };
57 
58 /**
59  * \struct FontTexture
60  * \brief Single texture filled with character textures
61  */
62 struct FontTexture
63 {
64     unsigned int id = 0;
65     Math::IntPoint tileSize;
66     int freeSlots = 0;
67 };
68 
69 /**
70  * \struct CachedFont
71  * \brief Base TTF font with UTF-8 char cache
72  */
73 struct CachedFont
74 {
75     std::unique_ptr<CSDLMemoryWrapper> fontFile;
76     TTF_Font* font = nullptr;
77     std::map<UTF8Char, CharTexture> cache;
78 
CachedFontGfx::CachedFont79     CachedFont(std::unique_ptr<CSDLMemoryWrapper> fontFile, int pointSize)
80         : fontFile(std::move(fontFile))
81     {
82         font = TTF_OpenFontRW(this->fontFile->GetHandler(), 0, pointSize);
83     }
84 
~CachedFontGfx::CachedFont85     ~CachedFont()
86     {
87         if (font != nullptr)
88             TTF_CloseFont(font);
89     }
90 };
91 
92 
93 namespace
94 {
95 const Math::IntPoint REFERENCE_SIZE(800, 600);
96 const Math::IntPoint FONT_TEXTURE_SIZE(256, 256);
97 } // anonymous namespace
98 
99 /// The QuadBatch is responsible for collecting as many quad (aka rectangle) draws as possible and
100 /// sending them to the CDevice in one big batch. This avoids making one CDevice::DrawPrimitive call
101 /// for every CText::DrawCharAndAdjustPos call, which makes text rendering much faster.
102 /// Currently we only collect textured quads (ie. ones using Vertex), not untextured quads (which
103 /// use VertexCol). Untextured quads are only drawn via DrawHighlight, which happens much less often
104 /// than drawing textured quads.
105 class CText::CQuadBatch
106 {
107 public:
CQuadBatch(CEngine & engine)108     explicit CQuadBatch(CEngine& engine)
109         : m_engine(engine)
110     {
111         m_quads.reserve(1024);
112     }
113 
114     /// Add a quad to be rendered.
115     /// This may trigger a call to Flush() if necessary.
Add(Vertex vertices[4],unsigned int texID,EngineRenderState renderState,Color color)116     void Add(Vertex vertices[4], unsigned int texID, EngineRenderState renderState, Color color)
117     {
118         if (texID != m_texID || renderState != m_renderState || color != m_color)
119         {
120             Flush();
121             m_texID = texID;
122             m_renderState = renderState;
123             m_color = color;
124         }
125         m_quads.emplace_back(Quad{{vertices[0], vertices[1], vertices[2], vertices[3]}});
126     }
127 
128     /// Draw all pending quads immediately.
Flush()129     void Flush()
130     {
131         if (m_quads.empty()) return;
132 
133         m_engine.SetState(m_renderState);
134         m_engine.GetDevice()->SetTexture(0, m_texID);
135 
136         assert(m_firsts.size() == m_counts.size());
137         if (m_firsts.size() < m_quads.size())
138         {
139             // m_firsts needs to look like { 0, 4, 8, 12, ... }
140             // m_counts needs to look like { 4, 4, 4,  4, ... }
141             // and both need to be the same length as m_quads
142             m_counts.resize(m_quads.size(), 4);
143             std::size_t begin = m_firsts.size();
144             m_firsts.resize(m_quads.size());
145             for (std::size_t i = begin; i < m_firsts.size(); ++i)
146             {
147                 m_firsts[i] = static_cast<int>(4 * i);
148             }
149         }
150 
151         const Vertex* vertices = m_quads.front().vertices;
152         m_engine.GetDevice()->DrawPrimitives(PRIMITIVE_TRIANGLE_STRIP, vertices, m_firsts.data(),
153                                              m_counts.data(), static_cast<int>(m_quads.size()), m_color);
154         m_engine.AddStatisticTriangle(static_cast<int>(m_quads.size() * 2));
155         m_quads.clear();
156     }
157 private:
158     CEngine& m_engine;
159 
160     struct Quad { Vertex vertices[4]; };
161     std::vector<Quad> m_quads;
162     std::vector<int> m_firsts;
163     std::vector<int> m_counts;
164 
165     Color m_color;
166     unsigned int m_texID{};
167     EngineRenderState m_renderState{};
168 };
169 
170 
CText(CEngine * engine)171 CText::CText(CEngine* engine)
172 {
173     m_device = nullptr;
174     m_engine = engine;
175 
176     m_defaultSize = 12.0f;
177     m_tabSize = 4;
178 
179     m_lastFontType = FONT_COMMON;
180     m_lastFontSize = 0;
181     m_lastCachedFont = nullptr;
182 
183     m_quadBatch = MakeUnique<CQuadBatch>(*engine);
184 }
185 
~CText()186 CText::~CText()
187 {
188     m_device = nullptr;
189     m_engine = nullptr;
190 }
191 
Create()192 bool CText::Create()
193 {
194     if (TTF_Init() != 0)
195     {
196         m_error = std::string("TTF_Init error: ") + std::string(TTF_GetError());
197         return false;
198     }
199 
200     if (!ReloadFonts())
201     {
202         return false;
203     }
204 
205     return true;
206 }
207 
ReloadFonts()208 bool CText::ReloadFonts()
209 {
210     CFontLoader fontLoader;
211     if (!fontLoader.Init())
212     {
213         GetLogger()->Debug("Error on parsing fonts config file: failed to open file\n");
214     }
215 
216     // Backup previous fonts
217     auto fonts = std::move(m_fonts);
218     m_fonts.clear();
219 
220     for (auto type : {FONT_COMMON, FONT_STUDIO, FONT_SATCOM})
221     {
222         m_fonts[static_cast<Gfx::FontType>(type)] = MakeUnique<MultisizeFont>(fontLoader.GetFont(type));
223         m_fonts[static_cast<Gfx::FontType>(type|FONT_BOLD)] = MakeUnique<MultisizeFont>(fontLoader.GetFont(static_cast<Gfx::FontType>(type|FONT_BOLD)));
224         m_fonts[static_cast<Gfx::FontType>(type|FONT_ITALIC)] = MakeUnique<MultisizeFont>(fontLoader.GetFont(static_cast<Gfx::FontType>(type|FONT_ITALIC)));
225     }
226 
227     for (auto it = m_fonts.begin(); it != m_fonts.end(); ++it)
228     {
229         FontType type = (*it).first;
230         CachedFont* cf = GetOrOpenFont(type, m_defaultSize);
231         if (cf == nullptr || cf->font == nullptr)
232         {
233             m_fonts = std::move(fonts);
234             return false;
235         }
236     }
237 
238     return true;
239 }
240 
Destroy()241 void CText::Destroy()
242 {
243     m_fonts.clear();
244 
245     m_lastCachedFont = nullptr;
246     m_lastFontType = FONT_COMMON;
247     m_lastFontSize = 0;
248 
249     TTF_Quit();
250 }
251 
SetDevice(CDevice * device)252 void CText::SetDevice(CDevice* device)
253 {
254     m_device = device;
255 }
256 
GetError()257 std::string CText::GetError()
258 {
259     return m_error;
260 }
261 
FlushCache()262 void CText::FlushCache()
263 {
264     for (auto& fontTexture : m_fontTextures)
265     {
266         Texture tex;
267         tex.id = fontTexture.id;
268         m_device->DestroyTexture(tex);
269     }
270     m_fontTextures.clear();
271 
272     for (auto& multisizeFont : m_fonts)
273     {
274         for (auto& cachedFont : multisizeFont.second->fonts)
275         {
276             cachedFont.second->cache.clear();
277         }
278     }
279 
280     m_lastCachedFont = nullptr;
281     m_lastFontType = FONT_COMMON;
282     m_lastFontSize = 0;
283 }
284 
GetTabSize()285 int CText::GetTabSize()
286 {
287     return m_tabSize;
288 }
289 
SetTabSize(int tabSize)290 void CText::SetTabSize(int tabSize)
291 {
292     m_tabSize = tabSize;
293 }
294 
DrawText(const std::string & text,std::vector<FontMetaChar>::iterator format,std::vector<FontMetaChar>::iterator end,float size,Math::Point pos,float width,TextAlign align,int eol,Color color)295 void CText::DrawText(const std::string &text, std::vector<FontMetaChar>::iterator format,
296                      std::vector<FontMetaChar>::iterator end,
297                      float size, Math::Point pos, float width, TextAlign align,
298                      int eol, Color color)
299 {
300     float sw = 0.0f;
301 
302     if (align == TEXT_ALIGN_CENTER)
303     {
304         sw = GetStringWidth(text, format, end, size);
305         if (sw > width) sw = width;
306         pos.x -= sw / 2.0f;
307     }
308     else if (align == TEXT_ALIGN_RIGHT)
309     {
310         sw = GetStringWidth(text, format, end, size);
311         if (sw > width) sw = width;
312         pos.x -= sw;
313     }
314 
315     Math::IntPoint intPos = m_engine->InterfaceToWindowCoords(pos);
316     int intWidth = width * m_engine->GetWindowSize().x;
317     DrawString(text, format, end, size, intPos, intWidth, eol, color);
318 }
319 
DrawText(const std::string & text,FontType font,float size,Math::Point pos,float width,TextAlign align,int eol,Color color)320 void CText::DrawText(const std::string &text, FontType font,
321                      float size, Math::Point pos, float width, TextAlign align,
322                      int eol, Color color)
323 {
324     float sw = 0.0f;
325 
326     if (align == TEXT_ALIGN_CENTER)
327     {
328         sw = GetStringWidth(text, font, size);
329         if (sw > width) sw = width;
330         pos.x -= sw / 2.0f;
331     }
332     else if (align == TEXT_ALIGN_RIGHT)
333     {
334         sw = GetStringWidth(text, font, size);
335         if (sw > width) sw = width;
336         pos.x -= sw;
337     }
338 
339     Math::IntPoint intPos = m_engine->InterfaceToWindowCoords(pos);
340     int intWidth = width * m_engine->GetWindowSize().x;
341     DrawString(text, font, size, intPos, intWidth, eol, color);
342 }
343 
SizeText(const std::string & text,std::vector<FontMetaChar>::iterator format,std::vector<FontMetaChar>::iterator endFormat,float size,Math::Point pos,TextAlign align,Math::Point & start,Math::Point & end)344 void CText::SizeText(const std::string &text, std::vector<FontMetaChar>::iterator format,
345                      std::vector<FontMetaChar>::iterator endFormat,
346                      float size, Math::Point pos, TextAlign align,
347                      Math::Point &start, Math::Point &end)
348 {
349     start = end = pos;
350 
351     float sw = GetStringWidth(text, format, endFormat, size);
352     end.x += sw;
353     if (align == TEXT_ALIGN_CENTER)
354     {
355         start.x -= sw/2.0f;
356         end.x   -= sw/2.0f;
357     }
358     else if (align == TEXT_ALIGN_RIGHT)
359     {
360         start.x -= sw;
361         end.x   -= sw;
362     }
363 
364     start.y -= GetDescent(FONT_COMMON, size);
365     end.y   += GetAscent(FONT_COMMON, size);
366 }
367 
SizeText(const std::string & text,FontType font,float size,Math::Point pos,TextAlign align,Math::Point & start,Math::Point & end)368 void CText::SizeText(const std::string &text, FontType font,
369                      float size, Math::Point pos, TextAlign align,
370                      Math::Point &start, Math::Point &end)
371 {
372     start = end = pos;
373 
374     float sw = GetStringWidth(text, font, size);
375     end.x += sw;
376     if (align == TEXT_ALIGN_CENTER)
377     {
378         start.x -= sw/2.0f;
379         end.x   -= sw/2.0f;
380     }
381     else if (align == TEXT_ALIGN_RIGHT)
382     {
383         start.x -= sw;
384         end.x   -= sw;
385     }
386 
387     start.y -= GetDescent(font, size);
388     end.y   += GetAscent(font, size);
389 }
390 
GetAscent(FontType font,float size)391 float CText::GetAscent(FontType font, float size)
392 {
393     assert(font != FONT_BUTTON);
394 
395     CachedFont* cf = GetOrOpenFont(font, size);
396     assert(cf != nullptr);
397     Math::IntPoint wndSize;
398     wndSize.y = TTF_FontAscent(cf->font);
399     Math::Point ifSize = m_engine->WindowToInterfaceSize(wndSize);
400     return ifSize.y;
401 }
402 
GetDescent(FontType font,float size)403 float CText::GetDescent(FontType font, float size)
404 {
405     assert(font != FONT_BUTTON);
406 
407     CachedFont* cf = GetOrOpenFont(font, size);
408     assert(cf != nullptr);
409     Math::IntPoint wndSize;
410     wndSize.y = TTF_FontDescent(cf->font);
411     Math::Point ifSize = m_engine->WindowToInterfaceSize(wndSize);
412     return ifSize.y;
413 }
414 
GetHeight(FontType font,float size)415 float CText::GetHeight(FontType font, float size)
416 {
417     assert(font != FONT_BUTTON);
418 
419     CachedFont* cf = GetOrOpenFont(font, size);
420     assert(cf != nullptr);
421     Math::IntPoint wndSize;
422     wndSize.y = TTF_FontHeight(cf->font);
423     Math::Point ifSize = m_engine->WindowToInterfaceSize(wndSize);
424     return ifSize.y;
425 }
426 
GetHeightInt(FontType font,float size)427 int CText::GetHeightInt(FontType font, float size)
428 {
429     assert(font != FONT_BUTTON);
430 
431     CachedFont* cf = GetOrOpenFont(font, size);
432     assert(cf != nullptr);
433     return TTF_FontHeight(cf->font);
434 }
435 
GetStringWidth(const std::string & text,std::vector<FontMetaChar>::iterator format,std::vector<FontMetaChar>::iterator end,float size)436 float CText::GetStringWidth(const std::string &text,
437                             std::vector<FontMetaChar>::iterator format,
438                             std::vector<FontMetaChar>::iterator end, float size)
439 {
440     float width = 0.0f;
441     unsigned int index = 0;
442     unsigned int fmtIndex = 0;
443     while (index < text.length())
444     {
445         FontType font = FONT_COMMON;
446         if (format + fmtIndex != end)
447             font = static_cast<FontType>(*(format + fmtIndex) & FONT_MASK_FONT);
448 
449         UTF8Char ch;
450 
451         int len = GetCharSizeAt(font, text, index);
452         if (len >= 1)
453             ch.c1 = text[index];
454         if (len >= 2)
455             ch.c2 = text[index+1];
456         if (len >= 3)
457             ch.c3 = text[index+2];
458 
459         width += GetCharWidth(ch, font, size, width);
460 
461         index += len;
462         fmtIndex += len;
463     }
464 
465     return width;
466 }
467 
GetStringWidth(std::string text,FontType font,float size)468 float CText::GetStringWidth(std::string text, FontType font, float size)
469 {
470     assert(font != FONT_BUTTON);
471 
472     // Skip special chars
473     for (char& c : text)
474     {
475         if (c < 32 && c >= 0)
476             c = ':';
477     }
478 
479     CachedFont* cf = GetOrOpenFont(font, size);
480     assert(cf != nullptr);
481     Math::IntPoint wndSize;
482     TTF_SizeUTF8(cf->font, text.c_str(), &wndSize.x, &wndSize.y);
483     Math::Point ifSize = m_engine->WindowToInterfaceSize(wndSize);
484     return ifSize.x;
485 }
486 
GetCharWidth(UTF8Char ch,FontType font,float size,float offset)487 float CText::GetCharWidth(UTF8Char ch, FontType font, float size, float offset)
488 {
489     if (font == FONT_BUTTON)
490     {
491         Math::IntPoint windowSize = m_engine->GetWindowSize();
492         float height = GetHeight(FONT_COMMON, size);
493         float width = height*(static_cast<float>(windowSize.y)/windowSize.x);
494         return width;
495     }
496 
497     int width = 1;
498     if (ch.c1 < 32 && ch.c1 >= 0)
499     {
500         if (ch.c1 == '\t')
501             width = m_tabSize;
502 
503         // TODO: tab sizing at intervals?
504 
505         ch.c1 = ':';
506     }
507 
508     CachedFont* cf = GetOrOpenFont(font, size);
509     assert(cf != nullptr);
510 
511     Math::Point charSize;
512     auto it = cf->cache.find(ch);
513     if (it != cf->cache.end())
514     {
515         charSize = m_engine->WindowToInterfaceSize((*it).second.charSize);
516     }
517     else
518     {
519         Math::IntPoint wndSize;
520         std::string text;
521         text.append({ch.c1, ch.c2, ch.c3});
522         TTF_SizeUTF8(cf->font, text.c_str(), &wndSize.x, &wndSize.y);
523         charSize = m_engine->WindowToInterfaceSize(wndSize);
524     }
525 
526     return charSize.x * width;
527 }
528 
GetCharWidthInt(UTF8Char ch,FontType font,float size,float offset)529 int CText::GetCharWidthInt(UTF8Char ch, FontType font, float size, float offset)
530 {
531     if (font == FONT_BUTTON)
532     {
533         Math::IntPoint windowSize = m_engine->GetWindowSize();
534         int height = GetHeightInt(FONT_COMMON, size);
535         int width = height*(static_cast<float>(windowSize.y)/windowSize.x);
536         return width;
537     }
538 
539     int width = 1;
540     if (ch.c1 < 32 && ch.c1 >= 0)
541     {
542         if (ch.c1 == '\t')
543             width = m_tabSize;
544 
545         // TODO: tab sizing at intervals?
546 
547         ch.c1 = ':';
548     }
549 
550     CachedFont* cf = GetOrOpenFont(font, size);
551     assert(cf != nullptr);
552 
553     Math::IntPoint charSize;
554     auto it = cf->cache.find(ch);
555     if (it != cf->cache.end())
556     {
557         charSize = (*it).second.charSize;
558     }
559     else
560     {
561         std::string text;
562         text.append({ch.c1, ch.c2, ch.c3});
563         TTF_SizeUTF8(cf->font, text.c_str(), &charSize.x, &charSize.y);
564     }
565 
566     return charSize.x * width;
567 }
568 
569 
Justify(const std::string & text,std::vector<FontMetaChar>::iterator format,std::vector<FontMetaChar>::iterator end,float size,float width)570 int CText::Justify(const std::string &text, std::vector<FontMetaChar>::iterator format,
571                    std::vector<FontMetaChar>::iterator end,
572                    float size, float width)
573 {
574     float pos = 0.0f;
575     int cut = 0;
576     unsigned int index = 0;
577     unsigned int fmtIndex = 0;
578     while (index < text.length())
579     {
580         FontType font = FONT_COMMON;
581         if (format + fmtIndex != end)
582             font = static_cast<FontType>(*(format + fmtIndex) & FONT_MASK_FONT);
583 
584         UTF8Char ch;
585 
586         int len = GetCharSizeAt(font, text, index);
587         if (len >= 1)
588             ch.c1 = text[index];
589         if (len >= 2)
590             ch.c2 = text[index+1];
591         if (len >= 3)
592             ch.c3 = text[index+2];
593 
594         if (font != FONT_BUTTON)
595         {
596             if (ch.c1 == '\n')
597                 return index+1;
598             if (ch.c1 == ' ')
599                 cut = index+1;
600         }
601 
602         pos += GetCharWidth(ch, font, size, pos);
603         if (pos > width)
604         {
605             if (cut == 0) return index;
606             else          return cut;
607         }
608 
609         index += len;
610         fmtIndex += len;
611     }
612 
613     return index;
614 }
615 
Justify(const std::string & text,FontType font,float size,float width)616 int CText::Justify(const std::string &text, FontType font, float size, float width)
617 {
618     assert(font != FONT_BUTTON);
619 
620     float pos = 0.0f;
621     int cut = 0;
622     unsigned int index = 0;
623     while (index < text.length())
624     {
625         UTF8Char ch;
626 
627         int len = GetCharSizeAt(font, text, index);
628         if (len >= 1)
629             ch.c1 = text[index];
630         if (len >= 2)
631             ch.c2 = text[index+1];
632         if (len >= 3)
633             ch.c3 = text[index+2];
634 
635         if (ch.c1 == '\n')
636         {
637             return index+1;
638         }
639 
640         if (ch.c1 == ' ' )
641             cut = index+1;
642 
643         pos += GetCharWidth(ch, font, size, pos);
644         if (pos > width)
645         {
646             if (cut == 0) return index;
647             else          return cut;
648         }
649         index += len;
650     }
651 
652     return index;
653 }
654 
Detect(const std::string & text,std::vector<FontMetaChar>::iterator format,std::vector<FontMetaChar>::iterator end,float size,float offset)655 int CText::Detect(const std::string &text, std::vector<FontMetaChar>::iterator format,
656                   std::vector<FontMetaChar>::iterator end,
657                   float size, float offset)
658 {
659     float pos = 0.0f;
660     unsigned int index = 0;
661     unsigned int fmtIndex = 0;
662     while (index < text.length())
663     {
664         FontType font = FONT_COMMON;
665 
666         if (format + fmtIndex != end)
667             font = static_cast<FontType>(*(format + fmtIndex) & FONT_MASK_FONT);
668 
669         UTF8Char ch;
670 
671         int len = GetCharSizeAt(font, text, index);
672         if (len >= 1)
673             ch.c1 = text[index];
674         if (len >= 2)
675             ch.c2 = text[index+1];
676         if (len >= 3)
677             ch.c3 = text[index+2];
678 
679         if (ch.c1 == '\n')
680             return index;
681 
682         float width = GetCharWidth(ch, font, size, pos);
683         if (offset <= pos + width/2.0f)
684             return index;
685 
686         pos += width;
687         index += len;
688         fmtIndex += len;
689     }
690 
691     return index;
692 }
693 
Detect(const std::string & text,FontType font,float size,float offset)694 int CText::Detect(const std::string &text, FontType font, float size, float offset)
695 {
696     assert(font != FONT_BUTTON);
697 
698     float pos = 0.0f;
699     unsigned int index = 0;
700     while (index < text.length())
701     {
702         UTF8Char ch;
703 
704         int len = GetCharSizeAt(font, text, index);
705         if (len >= 1)
706             ch.c1 = text[index];
707         if (len >= 2)
708             ch.c2 = text[index+1];
709         if (len >= 3)
710             ch.c3 = text[index+2];
711 
712         index += len;
713 
714         if (ch.c1 == '\n')
715             return index;
716 
717         float width = GetCharWidth(ch, font, size, pos);
718         if (offset <= pos + width/2.0f)
719             return index;
720 
721         pos += width;
722     }
723 
724     return index;
725 }
726 
TranslateSpecialChar(int specialChar)727 UTF8Char CText::TranslateSpecialChar(int specialChar)
728 {
729     UTF8Char ch;
730 
731     switch (specialChar)
732     {
733         case CHAR_TAB:
734             ch.c1 = ':';
735             ch.c2 = 0;
736             ch.c3 = 0;
737             break;
738 
739         case CHAR_NEWLINE:
740             // Unicode: U+21B2
741             ch.c1 = static_cast<char>(0xE2);
742             ch.c2 = static_cast<char>(0x86);
743             ch.c3 = static_cast<char>(0xB2);
744             break;
745 
746         case CHAR_DOT:
747             // Unicode: U+23C5
748             ch.c1 = static_cast<char>(0xE2);
749             ch.c2 = static_cast<char>(0x8F);
750             ch.c3 = static_cast<char>(0x85);
751             break;
752 
753         case CHAR_SQUARE:
754             // Unicode: U+25FD
755             ch.c1 = static_cast<char>(0xE2);
756             ch.c2 = static_cast<char>(0x97);
757             ch.c3 = static_cast<char>(0xBD);
758             break;
759 
760         case CHAR_SKIP_RIGHT:
761             // Unicode: U+25B6
762             ch.c1 = static_cast<char>(0xE2);
763             ch.c2 = static_cast<char>(0x96);
764             ch.c3 = static_cast<char>(0xB6);
765             break;
766 
767         case CHAR_SKIP_LEFT:
768             // Unicode: U+25C0
769             ch.c1 = static_cast<char>(0xE2);
770             ch.c2 = static_cast<char>(0x97);
771             ch.c3 = static_cast<char>(0x80);
772             break;
773 
774         default:
775             ch.c1 = '?';
776             ch.c2 = 0;
777             ch.c3 = 0;
778             break;
779     }
780 
781     return ch;
782 }
783 
DrawString(const std::string & text,std::vector<FontMetaChar>::iterator format,std::vector<FontMetaChar>::iterator end,float size,Math::IntPoint pos,int width,int eol,Color color)784 void CText::DrawString(const std::string &text, std::vector<FontMetaChar>::iterator format,
785                        std::vector<FontMetaChar>::iterator end,
786                        float size, Math::IntPoint pos, int width, int eol, Color color)
787 {
788     m_engine->SetWindowCoordinates();
789 
790     int start = pos.x;
791 
792     unsigned int fmtIndex = 0;
793 
794     std::vector<UTF8Char> chars;
795     StringToUTFCharList(text, chars, format, end);
796     for (auto it = chars.begin(); it != chars.end(); ++it)
797     {
798         FontType font = FONT_COMMON;
799         if (format + fmtIndex != end)
800             font = static_cast<FontType>(*(format + fmtIndex) & FONT_MASK_FONT);
801 
802         UTF8Char ch = *it;
803 
804         int offset = pos.x - start;
805         int cw = GetCharWidthInt(ch, font, size, offset);
806         if (offset + cw > width)  // exceeds the maximum width?
807         {
808             ch = TranslateSpecialChar(CHAR_SKIP_RIGHT);
809             cw = GetCharWidthInt(ch, font, size, offset);
810             pos.x = start + width - cw;
811             color = Color(1.0f, 0.0f, 0.0f);
812             DrawCharAndAdjustPos(ch, font, size, pos, color);
813             break;
814         }
815 
816         Color c = color;
817         FontHighlight hl = static_cast<FontHighlight>(format[fmtIndex] & FONT_MASK_HIGHLIGHT);
818         if (hl == FONT_HIGHLIGHT_TOKEN)
819         {
820             c = Color(0.490f, 0.380f, 0.165f, 1.0f); // #7D612A
821         }
822         else if (hl == FONT_HIGHLIGHT_TYPE)
823         {
824             c = Color(0.31f, 0.443f, 0.196f, 1.0f); // #4F7132
825         }
826         else if (hl == FONT_HIGHLIGHT_CONST)
827         {
828             c = Color(0.882f, 0.176f, 0.176f, 1.0f); // #E12D2D
829         }
830         else if (hl == FONT_HIGHLIGHT_THIS)
831         {
832             c = Color(0.545f, 0.329f, 0.608f, 1.0f); // #8B549B
833         }
834         else if (hl == FONT_HIGHLIGHT_COMMENT)
835         {
836             c = Color(0.251f, 0.271f, 0.306f, 1.0f); // #40454E
837         }
838         else if (hl == FONT_HIGHLIGHT_KEYWORD)
839         {
840             c = Color(0.239f, 0.431f, 0.588f, 1.0f); // #3D6E96
841         }
842         else if (hl == FONT_HIGHLIGHT_STRING)
843         {
844             c = Color(0.239f, 0.384f, 0.341f, 1.0f); // #3D6257
845         }
846 
847         // draw highlight background or link underline
848         if (font != FONT_BUTTON)
849         {
850             Math::IntPoint charSize;
851             charSize.x = GetCharWidthInt(ch, font, size, offset);
852             charSize.y = GetHeightInt(font, size);
853             // NB. for quad batching to improve highlight drawing performance, this code would have
854             // to be rearranged to draw all highlights before any characters are drawn.
855             DrawHighlight(format[fmtIndex], pos, charSize);
856         }
857 
858         DrawCharAndAdjustPos(ch, font, size, pos, c);
859 
860         // increment fmtIndex for each byte in multibyte character
861         if ( ch.c1 != 0 )
862             fmtIndex++;
863         if ( ch.c2 != 0 )
864             fmtIndex++;
865         if ( ch.c3 != 0 )
866             fmtIndex++;
867     }
868 
869     if (eol != 0)
870     {
871         FontType font = FONT_COMMON;
872         UTF8Char ch = TranslateSpecialChar(eol);
873         color = Color(1.0f, 0.0f, 0.0f);
874         DrawCharAndAdjustPos(ch, font, size, pos, color);
875     }
876     m_quadBatch->Flush();
877     m_engine->SetInterfaceCoordinates();
878 }
879 
StringToUTFCharList(const std::string & text,std::vector<UTF8Char> & chars)880 void CText::StringToUTFCharList(const std::string &text, std::vector<UTF8Char> &chars)
881 {
882     unsigned int index = 0;
883     unsigned int totalLength = text.length();
884     while (index < totalLength)
885     {
886         UTF8Char ch;
887 
888         int len = StrUtils::Utf8CharSizeAt(text, index);
889         if (len >= 1)
890             ch.c1 = text[index];
891         if (len >= 2)
892             ch.c2 = text[index+1];
893         if (len >= 3)
894             ch.c3 = text[index+2];
895 
896         index += len;
897 
898         chars.push_back(ch);
899     }
900 }
901 
StringToUTFCharList(const std::string & text,std::vector<UTF8Char> & chars,std::vector<FontMetaChar>::iterator format,std::vector<FontMetaChar>::iterator end)902 void CText::StringToUTFCharList(const std::string &text, std::vector<UTF8Char> &chars,
903                                 std::vector<FontMetaChar>::iterator format,
904                                 std::vector<FontMetaChar>::iterator end)
905 {
906     unsigned int index = 0;
907     unsigned int totalLength = text.length();
908     while (index < totalLength)
909     {
910         UTF8Char ch;
911 
912         FontType font = FONT_COMMON;
913         if (format + index != end)
914             font = static_cast<FontType>(*(format + index) & FONT_MASK_FONT);
915 
916         int len = GetCharSizeAt(font, text, index);
917 
918         if (len >= 1)
919             ch.c1 = text[index];
920         if (len >= 2)
921             ch.c2 = text[index+1];
922         if (len >= 3)
923             ch.c3 = text[index+2];
924 
925         index += len;
926 
927         chars.push_back(ch);
928     }
929 }
930 
GetCharSizeAt(Gfx::FontType font,const std::string & text,unsigned int index) const931 int CText::GetCharSizeAt(Gfx::FontType font, const std::string& text, unsigned int index) const
932 {
933     int len = 0;
934     if (font == FONT_BUTTON)
935     {
936         len = 1;
937     }
938     else
939     {
940         len = StrUtils::Utf8CharSizeAt(text, index);
941     }
942     return len;
943 }
944 
DrawString(const std::string & text,FontType font,float size,Math::IntPoint pos,int width,int eol,Color color)945 void CText::DrawString(const std::string &text, FontType font,
946                        float size, Math::IntPoint pos, int width, int eol, Color color)
947 {
948     assert(font != FONT_BUTTON);
949 
950     std::vector<UTF8Char> chars;
951     StringToUTFCharList(text, chars);
952     m_engine->SetWindowCoordinates();
953     for (auto it = chars.begin(); it != chars.end(); ++it)
954     {
955         DrawCharAndAdjustPos(*it, font, size, pos, color);
956     }
957     m_quadBatch->Flush();
958     m_engine->SetInterfaceCoordinates();
959 }
960 
DrawHighlight(FontMetaChar hl,Math::IntPoint pos,Math::IntPoint size)961 void CText::DrawHighlight(FontMetaChar hl, Math::IntPoint pos, Math::IntPoint size)
962 {
963     // Gradient colors
964     Color grad[4];
965 
966     // TODO: switch to alpha factors
967 
968     if ((hl & FONT_MASK_LINK) != 0)
969     {
970         grad[0] = grad[1] = grad[2] = grad[3] = Color(0.0f, 0.0f, 1.0f, 0.5f);
971     }
972     else if ((hl & FONT_MASK_HIGHLIGHT) == FONT_HIGHLIGHT_KEY)
973     {
974         grad[0] = grad[1] = grad[2] = grad[3] =
975             Color(192.0f / 256.0f, 192.0f / 256.0f, 192.0f / 256.0f, 0.5f);
976     }
977     else
978     {
979         return;
980     }
981 
982     m_quadBatch->Flush();
983 
984     Math::IntPoint vsize = m_engine->GetWindowSize();
985     float h = 0.0f;
986     if (vsize.y <= 768.0f)    // 1024x768 or less?
987         h = 1.01f;  // 1 pixel
988     else                      // more than 1024x768?
989         h = 2.0f;   // 2 pixels
990 
991     Math::Point p1, p2;
992     p1.x = pos.x;
993     p1.y = pos.y - size.y;
994     p2.x = pos.x + size.x;
995     p2.y = pos.y;
996 
997     if ((hl & FONT_MASK_LINK) != 0)
998     {
999         p1.y = pos.y - h;  // just emphasized
1000     }
1001 
1002     m_device->SetTextureEnabled(0, false);
1003 
1004     VertexCol quad[] =
1005     {
1006         VertexCol(Math::Vector(p1.x, p2.y, 0.0f), grad[3]),
1007         VertexCol(Math::Vector(p1.x, p1.y, 0.0f), grad[0]),
1008         VertexCol(Math::Vector(p2.x, p2.y, 0.0f), grad[2]),
1009         VertexCol(Math::Vector(p2.x, p1.y, 0.0f), grad[1])
1010     };
1011 
1012     m_device->DrawPrimitive(PRIMITIVE_TRIANGLE_STRIP, quad, 4);
1013     m_engine->AddStatisticTriangle(2);
1014 
1015     m_device->SetTextureEnabled(0, true);
1016 }
1017 
DrawCharAndAdjustPos(UTF8Char ch,FontType font,float size,Math::IntPoint & pos,Color color)1018 void CText::DrawCharAndAdjustPos(UTF8Char ch, FontType font, float size, Math::IntPoint &pos, Color color)
1019 {
1020     if (font == FONT_BUTTON)
1021     {
1022         Math::IntPoint windowSize = m_engine->GetWindowSize();
1023         int height = GetHeightInt(FONT_COMMON, size);
1024         int width = height * (static_cast<float>(windowSize.y)/windowSize.x);
1025 
1026         Math::IntPoint p1(pos.x, pos.y - height);
1027         Math::IntPoint p2(pos.x + width, pos.y);
1028 
1029         Math::Vector n(0.0f, 0.0f, -1.0f);  // normal
1030 
1031         // For whatever reason ch.c1 is a SIGNED char, we need to fix that
1032         unsigned char icon = static_cast<unsigned char>(ch.c1);
1033 
1034         // TODO: A bit of code duplication, see CControl::SetButtonTextureForIcon()
1035         unsigned int texID = m_engine->LoadTexture("textures/interface/button" + StrUtils::ToString<int>((icon/64) + 1) + ".png").id;
1036         icon = icon%64;
1037 
1038         Math::Point uv1, uv2;
1039         uv1.x = (32.0f / 256.0f) * (icon%8);
1040         uv1.y = (32.0f / 256.0f) * (icon/8);
1041         uv2.x = (32.0f / 256.0f) + uv1.x;
1042         uv2.y = (32.0f / 256.0f) + uv1.y;
1043 
1044         float dp = 0.5f / 256.0f;
1045         uv1.x += dp;
1046         uv1.y += dp;
1047         uv2.x -= dp;
1048         uv2.y -= dp;
1049 
1050         Vertex quad[4] =
1051         {
1052             Vertex(Math::Vector(p1.x, p2.y, 0.0f), n, Math::Point(uv1.x, uv2.y)),
1053             Vertex(Math::Vector(p1.x, p1.y, 0.0f), n, Math::Point(uv1.x, uv1.y)),
1054             Vertex(Math::Vector(p2.x, p2.y, 0.0f), n, Math::Point(uv2.x, uv2.y)),
1055             Vertex(Math::Vector(p2.x, p1.y, 0.0f), n, Math::Point(uv2.x, uv1.y))
1056         };
1057 
1058         m_quadBatch->Add(quad, texID, ENG_RSTATE_TTEXTURE_WHITE, color);
1059 
1060         pos.x += width;
1061     }
1062     else
1063     {
1064         int width = 1;
1065         if (ch.c1 > 0 && ch.c1 < 32)
1066         {
1067             if (ch.c1 == '\t')
1068             {
1069                 color = Color(1.0f, 0.0f, 0.0f, 1.0f);
1070                 width = m_tabSize;
1071             }
1072 
1073             ch = TranslateSpecialChar(ch.c1);
1074         }
1075 
1076         CharTexture tex = GetCharTexture(ch, font, size);
1077 
1078         Math::Point p1(pos.x, pos.y - tex.charSize.y);
1079         Math::Point p2(pos.x + tex.charSize.x, pos.y);
1080 
1081         const float halfPixelMargin = 0.5f;
1082         Math::Point texCoord1(static_cast<float>(tex.charPos.x + halfPixelMargin) / FONT_TEXTURE_SIZE.x,
1083                               static_cast<float>(tex.charPos.y + halfPixelMargin) / FONT_TEXTURE_SIZE.y);
1084         Math::Point texCoord2(static_cast<float>(tex.charPos.x + tex.charSize.x - halfPixelMargin) / FONT_TEXTURE_SIZE.x,
1085                               static_cast<float>(tex.charPos.y + tex.charSize.y - halfPixelMargin) / FONT_TEXTURE_SIZE.y);
1086         Math::Vector n(0.0f, 0.0f, -1.0f);  // normal
1087 
1088         Vertex quad[4] =
1089         {
1090             Vertex(Math::Vector(p1.x, p2.y, 0.0f), n, Math::Point(texCoord1.x, texCoord2.y)),
1091             Vertex(Math::Vector(p1.x, p1.y, 0.0f), n, Math::Point(texCoord1.x, texCoord1.y)),
1092             Vertex(Math::Vector(p2.x, p2.y, 0.0f), n, Math::Point(texCoord2.x, texCoord2.y)),
1093             Vertex(Math::Vector(p2.x, p1.y, 0.0f), n, Math::Point(texCoord2.x, texCoord1.y))
1094         };
1095 
1096         m_quadBatch->Add(quad, tex.id, ENG_RSTATE_TEXT, color);
1097 
1098         pos.x += tex.charSize.x * width;
1099     }
1100 }
1101 
GetOrOpenFont(FontType font,float size)1102 CachedFont* CText::GetOrOpenFont(FontType font, float size)
1103 {
1104     Math::IntPoint windowSize = m_engine->GetWindowSize();
1105     int pointSize = static_cast<int>(size * (windowSize.Length() / REFERENCE_SIZE.Length()));
1106 
1107     if (m_lastCachedFont != nullptr &&
1108         m_lastFontType == font &&
1109         m_lastFontSize == pointSize)
1110     {
1111         return m_lastCachedFont;
1112     }
1113 
1114     auto it = m_fonts.find(font);
1115     if (it == m_fonts.end())
1116     {
1117         m_error = std::string("Invalid font type ") + StrUtils::ToString<int>(static_cast<int>(font));
1118         return nullptr;
1119     }
1120 
1121     MultisizeFont* mf = it->second.get();
1122 
1123     auto jt = mf->fonts.find(pointSize);
1124     if (jt != mf->fonts.end())
1125     {
1126         m_lastCachedFont = jt->second.get();
1127         m_lastFontType = font;
1128         m_lastFontSize = pointSize;
1129         return m_lastCachedFont;
1130     }
1131 
1132     auto file = CResourceManager::GetSDLMemoryHandler(mf->fileName);
1133     if (!file->IsOpen())
1134     {
1135         m_error = std::string("Unable to open file '") + mf->fileName + "' (font size = " + StrUtils::ToString<float>(size) + ")";
1136         return nullptr;
1137     }
1138     GetLogger()->Debug("Loaded font file %s (font size = %.1f)\n", mf->fileName.c_str(), size);
1139 
1140     auto newFont = MakeUnique<CachedFont>(std::move(file), pointSize);
1141     if (newFont->font == nullptr)
1142     {
1143         m_error = std::string("TTF_OpenFont error ") + std::string(TTF_GetError());
1144         return nullptr;
1145     }
1146 
1147     m_lastCachedFont = newFont.get();
1148     mf->fonts[pointSize] = std::move(newFont);
1149     return m_lastCachedFont;
1150 }
1151 
GetCharTexture(UTF8Char ch,FontType font,float size)1152 CharTexture CText::GetCharTexture(UTF8Char ch, FontType font, float size)
1153 {
1154     CachedFont* cf = GetOrOpenFont(font, size);
1155 
1156     if (cf == nullptr)
1157         return CharTexture();
1158 
1159     auto it = cf->cache.find(ch);
1160     CharTexture tex;
1161     if (it != cf->cache.end())
1162     {
1163         tex = (*it).second;
1164     }
1165     else
1166     {
1167         tex = CreateCharTexture(ch, cf);
1168 
1169         if (tex.id == 0) // invalid
1170             return CharTexture();
1171 
1172         cf->cache[ch] = tex;
1173     }
1174     return tex;
1175 }
1176 
GetFontTextureSize()1177 Math::IntPoint CText::GetFontTextureSize()
1178 {
1179     return FONT_TEXTURE_SIZE;
1180 }
1181 
CreateCharTexture(UTF8Char ch,CachedFont * font)1182 CharTexture CText::CreateCharTexture(UTF8Char ch, CachedFont* font)
1183 {
1184     CharTexture texture;
1185 
1186     SDL_Surface* textSurface = nullptr;
1187     SDL_Color white = {255, 255, 255, 0};
1188     char str[] = { ch.c1, ch.c2, ch.c3, '\0' };
1189     textSurface = TTF_RenderUTF8_Blended(font->font, str, white);
1190 
1191     if (textSurface == nullptr)
1192     {
1193         m_error = "TTF_Render error";
1194         return texture;
1195     }
1196 
1197     const int pixelMargin = 1;
1198     Math::IntPoint tileSize(Math::Max(16, Math::NextPowerOfTwo(textSurface->w)) + pixelMargin,
1199                             Math::Max(16, Math::NextPowerOfTwo(textSurface->h)) + pixelMargin);
1200 
1201     FontTexture* fontTexture = GetOrCreateFontTexture(tileSize);
1202 
1203     if (fontTexture == nullptr)
1204     {
1205         m_error = "Texture create error";
1206     }
1207     else
1208     {
1209         texture.id = fontTexture->id;
1210         texture.charPos = GetNextTilePos(*fontTexture);
1211         texture.charSize = Math::IntPoint(textSurface->w, textSurface->h);
1212 
1213         ImageData imageData;
1214         imageData.surface = textSurface;
1215 
1216         Texture tex;
1217         tex.id = texture.id;
1218         m_device->UpdateTexture(tex, texture.charPos, &imageData, TEX_IMG_RGBA);
1219 
1220         imageData.surface = nullptr;
1221 
1222         --fontTexture->freeSlots;
1223     }
1224 
1225     SDL_FreeSurface(textSurface);
1226 
1227     return texture;
1228 }
1229 
GetOrCreateFontTexture(Math::IntPoint tileSize)1230 FontTexture* CText::GetOrCreateFontTexture(Math::IntPoint tileSize)
1231 {
1232     for (auto& fontTexture : m_fontTextures)
1233     {
1234        if (fontTexture.tileSize == tileSize && fontTexture.freeSlots > 0)
1235            return &fontTexture;
1236     }
1237 
1238     FontTexture newFontTexture = CreateFontTexture(tileSize);
1239     if (newFontTexture.id == 0)
1240     {
1241         return nullptr;
1242     }
1243 
1244     m_fontTextures.push_back(newFontTexture);
1245     return &m_fontTextures.back();
1246 }
1247 
CreateFontTexture(Math::IntPoint tileSize)1248 FontTexture CText::CreateFontTexture(Math::IntPoint tileSize)
1249 {
1250     SDL_Surface* textureSurface = SDL_CreateRGBSurface(0, FONT_TEXTURE_SIZE.x, FONT_TEXTURE_SIZE.y, 32,
1251                                                        0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000);
1252     ImageData data;
1253     data.surface = textureSurface;
1254 
1255     TextureCreateParams createParams;
1256     createParams.format = TEX_IMG_RGBA;
1257     createParams.filter = TEX_FILTER_NEAREST;
1258     createParams.mipmap = false;
1259 
1260     Texture tex = m_device->CreateTexture(&data, createParams);
1261 
1262     data.surface = nullptr;
1263     SDL_FreeSurface(textureSurface);
1264 
1265     FontTexture fontTexture;
1266     fontTexture.id = tex.id;
1267     fontTexture.tileSize = tileSize;
1268     int horizontalTiles = FONT_TEXTURE_SIZE.x / tileSize.x;
1269     int verticalTiles = FONT_TEXTURE_SIZE.y / tileSize.y;
1270     fontTexture.freeSlots = horizontalTiles * verticalTiles;
1271     return fontTexture;
1272 }
1273 
GetNextTilePos(const FontTexture & fontTexture)1274 Math::IntPoint CText::GetNextTilePos(const FontTexture& fontTexture)
1275 {
1276     int horizontalTiles = FONT_TEXTURE_SIZE.x / std::max(1, fontTexture.tileSize.x); //this should prevent crashes in some combinations of resolution and font size, see issue #1128
1277     int verticalTiles = FONT_TEXTURE_SIZE.y / std::max(1, fontTexture.tileSize.y);
1278 
1279     int totalTiles = horizontalTiles * verticalTiles;
1280     int tileNumber = totalTiles - fontTexture.freeSlots;
1281 
1282     int verticalTileIndex = tileNumber / std::max(1, horizontalTiles);
1283     int horizontalTileIndex = tileNumber % horizontalTiles;
1284 
1285     return Math::IntPoint(horizontalTileIndex * fontTexture.tileSize.x,
1286                           verticalTileIndex * fontTexture.tileSize.y);
1287 }
1288 
1289 } // namespace Gfx
1290