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