1 /*
2  * DSFML - The Simple and Fast Multimedia Library for D
3  *
4  * Copyright (c) 2013 - 2017 Jeremy DeHaan (dehaan.jeremiah@gmail.com)
5  *
6  * This software is provided 'as-is', without any express or implied warranty.
7  * In no event will the authors be held liable for any damages arising from the
8  * use of this software.
9  *
10  * Permission is granted to anyone to use this software for any purpose,
11  * including commercial applications, and to alter it and redistribute it
12  * freely, subject to the following restrictions:
13  *
14  * 1. The origin of this software must not be misrepresented; you must not claim
15  * that you wrote the original software. If you use this software in a product,
16  * an acknowledgment in the product documentation would be appreciated but is
17  * not required.
18  *
19  * 2. Altered source versions must be plainly marked as such, and must not be
20  * misrepresented as being the original software.
21  *
22  * 3. This notice may not be removed or altered from any source distribution
23  */
24 
25 /**
26  * $(U Text) is a drawable class that allows one to easily display some text
27  * with a custom style and color on a render target.
28  *
29  * It inherits all the functions from $(TRANSFORMABLE_LINK): position, rotation,
30  * scale, origin. It also adds text-specific properties such as the font to use,
31  * the character size, the font style (bold, italic, underlined), the global
32  * color and the text to display of course. It also provides convenience
33  * functions to calculate the graphical size of the text, or to get the global
34  * position of a given character.
35  *
36  * $(U Text) works in combination with the $(FONT_LINK) class, which loads and
37  * provides the glyphs (visual characters) of a given font.
38  *
39  * The separation of $(FONT_LINK) and $(U Text) allows more flexibility and
40  * better performances: indeed a $(FONT_LINK) is a heavy resource, and any
41  * operation on it is slow (often too slow for real-time applications). On the
42  * other side, a $(U Text) is a lightweight object which can combine the glyphs
43  * data and metrics of a $(FONT_LINK) to display any text on a render target.
44  *
45  * It is important to note that the $(U Text) instance doesn't copy the font
46  * that it uses, it only keeps a reference to it. Thus, a $(FONT_LINK) must not
47  * be destructed while it is used by a $(U Text).
48  *
49  * See also the note on coordinates and undistorted rendering in
50  * $(TRANSFORMABLE_LINK).
51  *
52  * example:
53  * ---
54  * // Declare and load a font
55  * auto font = new Font();
56  * font.loadFromFile("arial.ttf");
57  *
58  * // Create a text
59  * auto text = new Text("hello", font);
60  * text.setCharacterSize(30);
61  * text.setStyle(Text.Style.Bold);
62  * text.setColor(Color.Red);
63  *
64  * // Draw it
65  * window.draw(text);
66  * ---
67  *
68  * See_Also:
69  * $(FONT_LINK), $(TRANSFORMABLE_LINK)
70  */
71 module dsfml.graphics.text;
72 
73 import dsfml.graphics.font;
74 import dsfml.graphics.glyph;
75 import dsfml.graphics.color;
76 import dsfml.graphics.rect;
77 import dsfml.graphics.transformable;
78 import dsfml.graphics.drawable;
79 import dsfml.graphics.texture;
80 import dsfml.graphics.vertexarray;
81 import dsfml.graphics.vertex;
82 import dsfml.graphics.rendertarget;
83 import dsfml.graphics.renderstates;
84 import dsfml.graphics.primitivetype;
85 
86 import dsfml.system.vector2;
87 
88 import std.typecons: Rebindable;
89 
90 /**
91  * Graphical text that can be drawn to a render target.
92  */
93 class Text : Drawable, Transformable
94 {
95     /// Enumeration of the string drawing styles.
96     enum Style
97     {
98         /// Regular characters, no style
99         Regular = 0,
100         /// Bold characters
101         Bold = 1 << 0,
102         /// Italic characters
103         Italic = 1 << 1,
104         /// Underlined characters
105         Underlined = 1 << 2
106     }
107 
108     mixin NormalTransformable;
109 
110     private
111     {
112         dstring m_string;
113         Rebindable!(const(Font)) m_font;
114         uint m_characterSize;
115         Style m_style;
116         Color m_color;
117         VertexArray m_vertices;
118         FloatRect m_bounds;
119 
120         //used for caching the font texture
121         uint lastSizeUsed = 0;
122         Rebindable!(const(Texture)) lastTextureUsed;
123     }
124 
125     /**
126      * Default constructor
127      *
128      * Creates an empty text.
129      */
130     this()
131     {
132         m_string = "";
133         m_characterSize = 30;
134         m_style = Style.Regular;
135         m_color = Color(255,255,255);
136         m_vertices = new VertexArray(PrimitiveType.Quads,0);
137         m_bounds = FloatRect();
138     }
139 
140     /**
141      * Construct the text from a string, font and size
142      *
143      * Note that if the used font is a bitmap font, it is not scalable, thus not
144      * all requested sizes will be available to use. This needs to be taken into
145      * consideration when setting the character size. If you need to display
146      * text of a certain size, make sure the corresponding bitmap font that
147      * supports that size is used.
148      *
149      * Params:
150      *	text          = Text assigned to the string
151      *	font          = Font used to draw the string
152      *	characterSize = Base size of characters, in pixels
153      */
154     this(T)(immutable(T)[] text, const(Font) font, uint characterSize = 30)
155         if (is(T == dchar)||is(T == wchar)||is(T == char))
156     {
157         import dsfml.system..string;
158 
159         m_string = stringConvert!(T, dchar)(text);
160         m_characterSize = characterSize;
161         m_style = Style.Regular;
162         m_color = Color(255,255,255);
163         m_vertices = new VertexArray(PrimitiveType.Quads,0);
164         m_bounds = FloatRect();
165         m_font = font;
166         updateGeometry();
167     }
168 
169     /// Destructor.
170     ~this()
171     {
172         import dsfml.system.config;
173         mixin(destructorOutput);
174     }
175 
176     /**
177      * Get the character size.
178      *
179      * Returns: Size of the characters, in pixels.
180      */
181     uint getCharacterSize() const
182     {
183         return m_characterSize;
184     }
185 
186     /**
187      * Get the global color of the text.
188      *
189      * Returns: Global color of the text.
190      */
191     Color getColor() const
192     {
193         return m_color;
194     }
195 
196     /**
197      * Get thet text's font.
198      *
199      * If the text has no font attached, a NULL pointer is returned.
200      * The returned reference is const, which means that you cannot modify the
201      * font when you get it from this function.
202      *
203      * Returns: Text's font.
204      */
205     const(Font) getFont() const
206     {
207         if(m_font is null)
208         {
209             return null;
210         }
211         else
212         {
213             return m_font;
214         }
215     }
216 
217     /**
218      * Get the global bounding rectangle of the entity.
219      *
220      * The returned rectangle is in global coordinates, which means that it
221      * takes in account the transformations (translation, rotation, scale, ...)
222      * that are applied to the entity. In other words, this function returns the
223      * bounds of the sprite in the global 2D world's coordinate system.
224      *
225      * Returns: Global bounding rectangle of the entity.
226      */
227     FloatRect getGlobalBounds()
228     {
229         return getTransform().transformRect(getLocalBounds());
230     }
231 
232     /**
233      * Get the local bounding rectangle of the entity.
234      *
235      * The returned rectangle is in local coordinates, which means that it
236      * ignores the transformations (translation, rotation, scale, ...) that are
237      * applied to the entity. In other words, this function returns the bounds
238      * of the entity in the entity's coordinate system.
239      *
240      * Returns: Local bounding rectangle of the entity.
241      */
242     FloatRect getLocalBounds() const
243     {
244         return m_bounds;
245     }
246 
247     //TODO: maybe a getString!dstring or getString!wstring etc template would be appropriate?
248     /**
249      * Get the text's string.
250      *
251      * The returned string is a dstring, a unicode type.
252      */
253     immutable(T)[] getString(T=char)() const
254         if (is(T == dchar)||is(T == wchar)||is(T == char))
255     {
256         import dsfml.system..string;
257         return stringConvert!(dchar, T)(m_string);
258     }
259 
260     /**
261      * Get the text's style.
262      *
263      * Returns: Text's style.
264      */
265     Style getStyle() const
266     {
267         return m_style;
268     }
269 
270     /**
271      * Set the character size.
272      *
273      * The default size is 30.
274      *
275      * Params:
276      * 		size	= New character size, in pixels.
277      */
278     void setCharacterSize(uint size)
279     {
280         m_characterSize = size;
281         updateGeometry();
282     }
283 
284     /**
285      * Set the global color of the text.
286      *
287      * By default, the text's color is opaque white.
288      *
289      * Params:
290      * 		color	= New color of the text.
291      */
292     void setColor(Color color)
293     {
294         m_color = color;
295         updateGeometry();
296     }
297 
298     /**
299      * Set the text's font.
300      *
301      * The font argument refers to a font that must exist as long as the text
302      * uses it. Indeed, the text doesn't store its own copy of the font, but
303      * rather keeps a pointer to the one that you passed to this function. If
304      * the font is destroyed and the text tries to use it, the behaviour is
305      * undefined.
306      *
307      * Params:
308      * 		font	= New font
309      */
310     void setFont(const(Font) font)
311     {
312         m_font = font;
313         updateGeometry();
314     }
315 
316     /**
317      * Set the text's string.
318      *
319      * A text's string is empty by default.
320      *
321      * Params:
322      * 		text	= New string
323      */
324     void setString(T)(immutable(T)[] text)
325         if (is(T == dchar)||is(T == wchar)||is(T == char))
326     {
327         import dsfml.system..string;
328         m_string = stringConvert!(T,dchar)(text);
329         updateGeometry();
330     }
331 
332     /**
333      * Set the text's style.
334      *
335      * You can pass a combination of one or more styles, for example
336      * Style.Bold | Text.Italic.
337      */
338     void setStyle(Style style)
339     {
340         m_style = style;
341         updateGeometry();
342     }
343 
344     /**
345      * Draw the object to a render target.
346      *
347      * Params:
348      *  		renderTarget =	Render target to draw to
349      *  		renderStates =	Current render states
350      */
351     void draw(RenderTarget renderTarget, RenderStates renderStates)
352     {
353         import std.stdio;
354 
355         if ((m_font !is null) && (m_characterSize>0))
356         {
357             renderStates.transform *= getTransform();
358 
359             //only call getTexture if the size has changed
360             if(m_characterSize != lastSizeUsed)
361             {
362                 //update the size
363                 lastSizeUsed = m_characterSize;
364                 //grab the new texture
365                 lastTextureUsed = m_font.getTexture(m_characterSize);
366             }
367 
368             updateGeometry();
369 
370             renderStates.texture =  m_font.getTexture(m_characterSize);
371 
372             renderTarget.draw(m_vertices, renderStates);
373         }
374     }
375 
376     /**
377      * Return the position of the index-th character.
378      *
379      * This function computes the visual position of a character from its index
380      * in the string. The returned position is in global coordinates
381      * (translation, rotation, scale and origin are applied). If index is out of
382      * range, the position of the end of the string is returned.
383      *
384      * Params:
385      * 		index	= Index of the character
386      *
387      * Returns: Position of the character.
388      */
389     Vector2f findCharacterPos(size_t index)
390     {
391         // Make sure that we have a valid font
392         if(m_font is null)
393         {
394             return Vector2f(0,0);
395         }
396 
397         // Adjust the index if it's out of range
398         if(index > m_string.length)
399         {
400             index = m_string.length;
401         }
402 
403         bool bold  = (m_style & Style.Bold) != 0;
404 
405         float hspace = cast(float)(m_font.getGlyph(' ', m_characterSize, bold).advance);
406         float vspace = cast(float)(m_font.getLineSpacing(m_characterSize));
407 
408         Vector2f position;
409         dchar prevChar = 0;
410         for (size_t i = 0; i < index; ++i)
411         {
412             dchar curChar = m_string[i];
413 
414             // Apply the kerning offset
415             position.x += cast(float)(m_font.getKerning(prevChar, curChar, m_characterSize));
416             prevChar = curChar;
417 
418             // Handle special characters
419             switch (curChar)
420             {
421                 case ' ' : position.x += hspace; continue;
422                 case '\t' : position.x += hspace * 4; continue;
423                 case '\n' : position.y += vspace; position.x = 0; continue;
424                 case '\v' : position.y += vspace * 4; continue;
425                 default:
426                     break;
427             }
428 
429             // For regular characters, add the advance offset of the glyph
430             position.x += cast(float)(m_font.getGlyph(curChar, m_characterSize, bold).advance);
431         }
432 
433         // Transform the position to global coordinates
434         position = getTransform().transformPoint(position);
435 
436         return position;
437     }
438 
439 private:
440     void updateGeometry()
441     {
442         import std.algorithm;
443 
444         // Clear the previous geometry
445         m_vertices.clear();
446         m_bounds = FloatRect();
447 
448         // No font: nothing to draw
449         if (m_font is null)
450             return;
451 
452         // No text: nothing to draw
453         if (m_string.length == 0)
454             return;
455         // Compute values related to the text style
456         bool bold = (m_style & Style.Bold) != 0;
457         bool underlined = (m_style & Style.Underlined) != 0;
458         float italic = (m_style & Style.Italic) ? 0.208f : 0f; // 12 degrees
459         float underlineOffset = m_characterSize * 0.1f;
460         float underlineThickness = m_characterSize * (bold ? 0.1f : 0.07f);
461 
462         // Precompute the variables needed by the algorithm
463         float hspace = cast(float)(m_font.getGlyph(' ', m_characterSize, bold).advance);
464         float vspace = cast(float)(m_font.getLineSpacing(m_characterSize));
465         float x = 0f;
466         float y = cast(float)(m_characterSize);
467 
468         // Create one quad for each character
469         float minX = m_characterSize, minY = m_characterSize, maxX = 0, maxY = 0;
470         dchar prevChar = 0;
471         for (size_t i = 0; i < m_string.length; ++i)
472         {
473             dchar curChar = m_string[i];
474 
475 
476             // Apply the kerning offset
477             x += cast(float)(m_font.getKerning(prevChar, curChar, m_characterSize));
478 
479             prevChar = curChar;
480 
481             // If we're using the underlined style and there's a new line, draw a line
482             if (underlined && (curChar == '\n'))
483             {
484                 float top = y + underlineOffset;
485                 float bottom = top + underlineThickness;
486 
487                 m_vertices.append(Vertex(Vector2f(0, top), m_color, Vector2f(1, 1)));
488                 m_vertices.append(Vertex(Vector2f(x, top), m_color, Vector2f(1, 1)));
489                 m_vertices.append(Vertex(Vector2f(x, bottom), m_color, Vector2f(1, 1)));
490                 m_vertices.append(Vertex(Vector2f(0, bottom), m_color, Vector2f(1, 1)));
491             }
492 
493             // Handle special characters
494             if ((curChar == ' ') || (curChar == '\t') || (curChar == '\n') || (curChar == '\v'))
495             {
496                 // Update the current bounds (min coordinates)
497                 minX = min(minX, x);
498                 minY = min(minY, y);
499 
500                 final switch (curChar)
501                 {
502                     case ' ' : x += hspace; break;
503                     case '\t' : x += hspace * 4; break;
504                     case '\n' : y += vspace; x = 0; break;
505                     case '\v' : y += vspace * 4; break;
506                 }
507 
508                 // Update the current bounds (max coordinates)
509                 maxX = max(maxX, x);
510                 maxY = max(maxY, y);
511 
512                 // Next glyph, no need to create a quad for whitespace
513                 continue;
514             }
515 
516 
517 
518             // Extract the current glyph's description
519             Glyph glyph = m_font.getGlyph(curChar, m_characterSize, bold);
520 
521             float left = glyph.bounds.left;
522             float top = glyph.bounds.top;
523             float right = glyph.bounds.left + glyph.bounds.width;
524             float bottom = glyph.bounds.top + glyph.bounds.height;
525 
526             float u1 = cast(float)(glyph.textureRect.left);
527             float v1 = cast(float)(glyph.textureRect.top);
528             float u2 = cast(float)(glyph.textureRect.left + glyph.textureRect.width);
529             float v2 = cast(float)(glyph.textureRect.top + glyph.textureRect.height);
530 
531             // Add a quad for the current character
532             m_vertices.append(Vertex(Vector2f(x + left - italic * top, y + top), m_color, Vector2f(u1, v1)));
533             m_vertices.append(Vertex(Vector2f(x + right - italic * top, y + top), m_color, Vector2f(u2, v1)));
534             m_vertices.append(Vertex(Vector2f(x + right - italic * bottom, y + bottom), m_color, Vector2f(u2, v2)));
535             m_vertices.append(Vertex(Vector2f(x + left - italic * bottom, y + bottom), m_color, Vector2f(u1, v2)));
536 
537             // Update the current bounds
538             minX = min(minX, x + left - italic * bottom);
539             maxX = max(maxX, x + right - italic * top);
540             minY = min(minY, y + top);
541             maxY = max(maxY, y + bottom);
542 
543             // Advance to the next character
544             x += glyph.advance;
545         }
546 
547         // If we're using the underlined style, add the last line
548         if (underlined)
549         {
550             float top = y + underlineOffset;
551             float bottom = top + underlineThickness;
552 
553             m_vertices.append(Vertex(Vector2f(0, top), m_color, Vector2f(1, 1)));
554             m_vertices.append(Vertex(Vector2f(x, top), m_color, Vector2f(1, 1)));
555             m_vertices.append(Vertex(Vector2f(x, bottom), m_color, Vector2f(1, 1)));
556             m_vertices.append(Vertex(Vector2f(0, bottom), m_color, Vector2f(1, 1)));
557         }
558 
559         // Update the bounding rectangle
560         m_bounds.left = minX;
561         m_bounds.top = minY;
562         m_bounds.width = maxX - minX;
563         m_bounds.height = maxY - minY;
564     }
565 }
566 
567 unittest
568 {
569     version(DSFML_Unittest_Graphics)
570     {
571         import std.stdio;
572         import dsfml.graphics.rendertexture;
573 
574         writeln("Unit test for Text");
575 
576         auto renderTexture = new RenderTexture();
577 
578         renderTexture.create(100,100);
579 
580         auto font = new Font();
581         assert(font.loadFromFile("res/Warenhaus-Standard.ttf"));
582 
583         Text text;
584         text = new Text("Sample String", font);
585 
586         string temp = text.getString();
587 
588         writeln(temp);
589 
590         renderTexture.clear();
591 
592         renderTexture.draw(text);
593 
594         renderTexture.display();
595 
596         //grab that texture for usage
597         auto texture = renderTexture.getTexture();
598 
599         texture.copyToImage().saveToFile("Text.png");
600 
601         writeln();
602     }
603 }