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