1 /++
2 Module for rendering text using the `FreeType` library.
3 
4 Macros:
5     LREF = <a href="#$1">$1</a>
6     HREF = <a href="$1">$2</a>
7     PHOBREF = <a href="https://dlang.org/phobos/$1.html#$2">$2</a>
8 
9 Authors: $(HREF https://github.com/TodNaz,TodNaz)
10 Copyright: Copyright (c) 2020 - 2021, TodNaz.
11 License: $(HREF https://github.com/TodNaz/Tida/blob/master/LICENSE,MIT)
12 +/
13 module tida.text;
14 
15 import bindbc.freetype;
16 import std.traits;
17 import tida.drawable;
18 
19 __gshared FT_Library _FTLibrary;
20 
21 /++
22 Loads a library for loading and rendering fonts.
23 
24 $(PHOBREF object,Exception) if the library was not found on the system.
25 +/
26 void initFontLibrary()
27 {
28     import std.exception : enforce;
29 
30     enforce!Exception(loadFreeType() != FTSupport.noLibrary, "Not find FreeType library!");
31     enforce!Exception(!FT_Init_FreeType(&_FTLibrary), "Not a initialize FreeType Library!");
32 }
33 
34 /++
35 The object to load and use the font.
36 +/
37 class Font
38 {
39     import std.exception : enforce;
40     import std..string : toStringz;
41     import std.file : exists;
42 
43 private:
44     FT_Face _face;
45     size_t _size;
46 
47 public @trusted:
48     /// Font face object.
49     @property FT_Face face()
50     {
51         return _face;
52     }
53 
54     /// Font size.
55     @property size_t size()
56     {
57         return _size;
58     }
59 
60     /++
61     Loads a font.
62 
63     Params:
64         path = The path to the font.
65         size = Font size.
66 
67     Throws:
68     $(PHOBREF object,Exception) if the font was not found in the file system,
69     or if the font is damaged.
70     +/
71     Font load(string path, size_t size)
72     {
73         enforce!Exception(exists(path), "Not file file `"~path~"`");
74 
75         enforce!Exception(!FT_New_Face(_FTLibrary, path.toStringz, 0, &_face), "Font damaged!");
76 
77         FT_CharMap found;
78 
79         foreach(i; 0 .. _face.num_charmaps)
80         {
81             FT_CharMap charmap = _face.charmaps[i];
82             if ((charmap.platform_id == 3 && charmap.encoding_id == 1)
83              || (charmap.platform_id == 3 && charmap.encoding_id == 0)
84              || (charmap.platform_id == 2 && charmap.encoding_id == 1)
85              || (charmap.platform_id == 0)) {
86                 found = charmap;
87                 break;
88             }
89         }
90 
91         FT_Set_Charmap(_face, found);
92         FT_Set_Char_Size(_face, 0, cast(int) size*32, 300, 300);
93         FT_Set_Pixel_Sizes(_face, 0, cast(int) size*2);
94 
95         this._size = size;
96 
97         return this;
98     }
99 
100     /++
101     Changes the font size to the specified parameter.
102 
103     Params:
104         newSize = Font new size.
105     +/
106     void resize(size_t newSize) @trusted
107     {
108         this._size = newSize;
109 
110         FT_Set_Char_Size(_face, 0, cast(int) _size*32, 300, 300);
111         FT_Set_Pixel_Sizes(_face, 0, cast(int) size*2);
112     }
113 
114     /// Free memory
115     void free() @trusted
116     {
117         FT_Done_Face(_face);
118     }
119 
120     ~this() @safe
121     {
122         free();
123     }
124 }
125 
126 /++
127 Determining the type of character depending on the type of the string.
128 +/
129 template TypeChar(T)
130 {
131     static if (is(T : string))
132         alias TypeChar = char;
133     else
134     static if (is(T : wstring))
135         alias TypeChar = wchar;
136     else
137     static if (is(T : dstring))
138         alias Typechar = dchar;
139 }
140 
141 /// The unit for rendering the text.
142 class Symbol
143 {
144     import tida.image;
145     import tida.color;
146     import tida.vector;
147 
148 public:
149     Image image; /// Symbol image.
150     Vecf position; /// Symbol releative position.
151     Color!ubyte color; /// Symbol color.
152     Vecf advance; /// Symbol releative position.
153     size_t size; /// Symbol size.
154     float offsetY = 0.0f; /// Offset symbol
155 
156 @safe:
157     this(   Image image,
158             Vecf position,
159             Vecf rel,
160             size_t size,
161             Color!ubyte color = rgb(255,255,255))
162     {
163         this.image = image;
164         this.position = position;
165         this.color = color;
166         this.advance = rel;
167         this.size = size;
168 
169         if (this.image !is null)
170         if (this.image.texture is null)
171             this.image.toTexture();
172     }
173 }
174 
175 /++
176 Cuts off formatting blocks.
177 
178 Params:
179     symbols = Symbols.
180 +/
181 T cutFormat(T)(T symbols) @safe
182 {
183     static assert(isSomeString!T, T.stringof ~ " is not a string!");
184     for(int i = 0; i < symbols.length; i++)
185     {
186         if(symbols[i] == '$')
187         {
188             if(symbols[i+1] == '<') {
189                 __symEachCutter: for(int j = i; j < symbols.length; j++)
190                 {
191                     if(symbols[j] == '>')
192                     {
193                         symbols = symbols[0 .. i] ~ symbols[j .. $];
194                         break __symEachCutter;
195                     }
196                 }
197             }
198         }
199     }
200 
201     return symbols;
202 }
203 
204 /++
205 Returns the size of the rendered text for the given font.
206 
207 Params:
208     T = String type.
209     text = Text.
210     font = Font.
211 +/
212 int widthText(T)(T text, Font font) @safe
213 {
214     import std.algorithm : reduce;
215     static assert(isSomeString!T, T.stringof ~ " is not a string!");
216 
217     text = cutFormat!T(text);
218 
219     return new Text(font)
220         .toSymbols!T(text)
221         .widthSymbols;
222 }
223 
224 /++
225 Shows the width of the displayed characters.
226 
227 Params:
228     text = Displayed characters.
229 +/
230 int widthSymbols(Symbol[] text) @safe
231 {
232     import std.algorithm : reduce;
233     import std.conv : to;
234 
235     int width = int.init;
236 
237     foreach(s; text)
238         width += s.advance.x.to!int >> 6;
239 
240     return width;
241 }
242 
243 /++
244 Draw text object. An array of letters and offset attributes already rendered
245 into the image is fed into the constructor, and the text is output
246 from these data.
247 +/
248 class SymbolRender : IDrawable, IDrawableEx
249 {
250     import tida.render;
251     import tida.vector;
252     import tida.color;
253     import tida.shader;
254     import std.conv : to;
255 
256 private:
257     Symbol[] symbols;
258 
259 public @safe:
260 
261     /++
262     Constructor of the text rendering object.
263 
264     Params:
265         symbols =   Already drawn ready-made in image text with offset
266                     parameters for correct presentation.
267     +/
268     this(Symbol[] symbols)
269     {
270         this.symbols = symbols;
271     }
272 
273     override void draw(IRenderer render, Vecf position)
274     {
275         Shader!Program currShader;
276 
277         if (render.type != RenderType.software)
278         {
279             if (render.currentShader !is null)
280             {
281                 currShader = render.currentShader;
282             }
283         }
284 
285         position.y += (symbols[0].size + (symbols[0].size / 2));
286 
287         foreach (s; symbols)
288         {
289             if (s.image !is null)
290             {
291                 if (render.type != RenderType.software)
292                     render.currentShader = currShader;
293 
294                 render.drawEx(  s.image, position - vecf(0, s.position.y), 0.0f,
295                 vecfNaN, vecfNaN, ubyte.max, s.color);
296             }
297 
298             position = position + vecf(s.advance.x.to!int >> 6, s.offsetY);
299         }
300     }
301 
302     override void drawEx(   IRenderer render,
303                             Vecf position,
304                             float angle,
305                             Vecf center,
306                             Vecf size,
307                             ubyte alpha,
308                             Color!ubyte color)
309     {
310         Shader!Program currShader;
311 
312         if (render.type != RenderType.software)
313         {
314             if (render.currentShader !is null)
315             {
316                 currShader = render.currentShader;
317             }
318         }
319 
320         position.y += (symbols[0].size + (symbols[0].size / 2));
321 
322         foreach (s; symbols)
323         {
324             if (s.image !is null)
325             {
326                 if (render.type != RenderType.software)
327                     render.currentShader = currShader;
328 
329                 render.drawEx(s.image, position, angle, center, vecfNaN, alpha,
330                 s.color);
331             }
332 
333             position = position + vecf(s.advance.x.to!int >> 6, 0);
334         }
335     }
336 }
337 
338 /++
339 An object for rendering drawing symbols.
340 +/
341 class Text
342 {
343     import tida.color;
344     import tida.image;
345     import tida.vector;
346 
347 private:
348     Font font;
349 
350 public @trusted:
351     this(Font font)
352     {
353         this.font = font;
354     }
355 
356     /++
357     Draws each character and gives them a relative position to position
358     them correctly in the text.
359 
360     Params:
361         T = String type.
362         text = Text.
363         color = Color text.
364     +/
365     Symbol[] toSymbols(T)(T text, Color!ubyte color = rgb(0, 0, 0))
366     {
367         static assert(isSomeString!T, T.stringof ~ " is not a string!");
368 
369         Symbol[] symbols;
370 
371         for (size_t i = 0; i < text.length; ++i)
372         {
373             TypeChar!T s = text[i];
374             Image image;
375             FT_GlyphSlot glyph;
376 
377             FT_Load_Char(font.face, s, FT_LOAD_RENDER | FT_LOAD_TARGET_NORMAL);
378             const glyphIndex = FT_Get_Char_Index(font.face, s);
379 
380             FT_Load_Glyph(font.face, glyphIndex, FT_LOAD_DEFAULT);
381             FT_Render_Glyph(font.face.glyph, FT_RENDER_MODE_NORMAL);
382 
383             glyph = font.face.glyph;
384 
385             if (s != ' ' && glyph.bitmap.width > 0 && glyph.bitmap.rows > 0)
386             {
387                 auto bitmap = glyph.bitmap;
388 
389                 image = new Image();
390                 image.allocatePlace(bitmap.width,bitmap.rows);
391 
392                 auto pixels = image.pixels;
393 
394                 foreach(j; 0 .. bitmap.width * bitmap.rows)
395                 {
396                     pixels[j] = rgba(255, 255, 255, bitmap.buffer[j]);
397                 }
398             }
399 
400             symbols ~= new Symbol(image,
401                 Vecf(glyph.bitmap_left, glyph.bitmap_top),
402                 Vecf(glyph.advance.x, 0),
403                 font.size,
404                 color);
405         }
406 
407         return symbols;
408     }
409 
410     /++
411     Outputs text with color and positional formatting.
412 
413     Using the special block `$ <...>`, you can highlight text with color,
414     for example:
415     ---
416     new Text(font).toSymbolsFormat("Black text! $<ffffff> white text!\nNew line!");
417     ---
418 
419     There must be a hex color inside the special block, which will be used later.
420     Also, line wrapping is supported.
421 
422     Params:
423         T = string type.
424         symbols = Text.
425         color = Default color.
426     +/
427     Symbol[] toSymbolsFormat(T)(T symbols,
428                                 Color!ubyte defaultColor = rgba(255,255,255,255))
429     {
430         static assert(isSomeString!T, T.stringof ~ " is not a string!");
431 
432         Symbol[] result;
433         int previous = 0;
434         int j = 0;
435 
436         Color!ubyte color = defaultColor;
437 
438         for (int i = 0; i < symbols.length; i++)
439         {
440             if (symbols[i] == '$')
441             {
442                 if (symbols[i+1] == '<') {
443                     T colorText;
444 
445                     __symEach: for (j = i; j < symbols.length; j++)
446                     {
447                         if (symbols[j] == '>')
448                         {
449                             colorText = symbols[i+2 .. j];
450                             break __symEach;
451                         }
452                     }
453 
454                     if (colorText != "")
455                     {
456                         result ~= this.toSymbols(symbols[previous .. i], color);
457 
458                         color = Color!ubyte(colorText);
459                         i = j + 1;
460                         previous = i;
461                     }
462                 }
463             } else
464             if (symbols[i] == '\n')
465             {
466                 Symbol[] temp = this.toSymbols(symbols[previous .. i], color);
467                 Symbol last = temp[$ - 1];
468                 last.offsetY += last.size * 2;
469                 float tc = -(result.widthSymbols + widthSymbols(temp));
470                 int ic = cast(int) tc;
471                 last.advance = vecf((ic << 6) + last.advance.x , 0);
472 
473                 previous = i + 1;
474 
475                 result ~= temp;
476             }
477         }
478 
479         result ~= this.toSymbols(symbols[previous .. $], color);
480 
481         return result;
482     }
483 
484     /++
485     Creates symbols for rendering and immediately returns the object of
486     their renderer.
487 
488     Params:
489         T = String type.
490         text = Text.
491         color = Color text.
492     +/
493     SymbolRender renderSymbols(T)(T text, Color!ubyte color = rgb(0, 0, 0))
494     {
495         static assert(isSomeString!T, T.stringof ~ " is not a string!");
496         return new SymbolRender(toSymbols(text, color));
497     }
498 
499     /++
500     Outputs characters with color formatting and sequel, renders such text.
501 
502     Params:
503         T = String type.
504         text = Text.
505         color = Color text.
506     +/
507     SymbolRender renderFormat(T)(T text, Color!ubyte color = rgb(0, 0, 0))
508     {
509         static assert(isSomeString!T, T.stringof ~ " is not a string!");
510         return new SymbolRender(toSymbolsFormat(text, color));
511     }
512 }