1 /*
2  *******************************************************************************************
3  * Dgame (a D game framework) - Copyright (c) Randy Schütt
4  * 
5  * This software is provided 'as-is', without any express or implied warranty.
6  * In no event will the authors be held liable for any damages arising from
7  * the use of this software.
8  * 
9  * Permission is granted to anyone to use this software for any purpose,
10  * including commercial applications, and to alter it and redistribute it
11  * freely, subject to the following restrictions:
12  * 
13  * 1. The origin of this software must not be misrepresented; you must not claim
14  *    that you wrote the original software. If you use this software in a product,
15  *    an acknowledgment in the product documentation would be appreciated but is
16  *    not required.
17  * 
18  * 2. Altered source versions must be plainly marked as such, and must not be
19  *    misrepresented as being the original software.
20  * 
21  * 3. This notice may not be removed or altered from any source distribution.
22  *******************************************************************************************
23  */
24 module Dgame.Graphics.Text;
25 
26 private {
27 	import std..string : format, toStringz;
28 	
29 	import derelict.opengl3.gl;
30 	import derelict.sdl2.sdl; // because of SDL_Surface and SDL_FreeSurface
31 	import derelict.sdl2.ttf;
32 	
33 	import Dgame.Internal.Log;
34 	import Dgame.Graphics.Drawable;
35 	import Dgame.Graphics.Transformable;
36 	import Dgame.Graphics.Color;
37 	import Dgame.Graphics.Font;
38 	import Dgame.Graphics.Texture;
39 	import Dgame.Graphics.Shape;
40 	import Dgame.Graphics.Blend;
41 	import Dgame.Math.Vector2;
42 	import Dgame.Math.Rect;
43 	import Dgame.System.VertexRenderer;
44 }
45 
46 private Font*[] _FontFinalizer;
47 
48 static ~this() {
49 	debug Log.info("Text: Finalize Font");
50 	
51 	for (size_t i = 0; i < _FontFinalizer.length; ++i) {
52 		if (_FontFinalizer[i] !is null) {
53 			debug Log.info(" -> Finalize font i = %d, ptr = %x", i, _FontFinalizer[i].ptr);
54 			_FontFinalizer[i].free();
55 		}
56 	}
57 	
58 	_FontFinalizer = null;
59 	
60 	debug Log.info("Font finalized");
61 }
62 
63 /**
64  * Text defines a graphical 2D text, that can be drawn on screen.
65  *	- The default foreground color is <code>Color.Black</code>
66  *	- The default background color is <code>Color.White</code>
67  *
68  * Author: rschuett
69  */
70 class Text : Transformable, Drawable, Blendable {
71 protected:
72 	string _text;
73 	bool _needUpdate;
74 	
75 	Color _fg = Color.Black;
76 	Color _bg = Color.White;
77 	
78 	Font _font;
79 	Texture _tex;
80 	Blend _blend;
81 	
82 private:
83 	void _storePixel(SDL_Surface* rhs, Texture.Format fmt) in {
84 		assert(this._tex !is null, "No Texture!");
85 		assert(rhs !is null, "No Surface!");
86 	} body {
87 		this._tex.loadFromMemory(rhs.pixels,
88 		                         cast(ushort) rhs.w, cast(ushort) rhs.h,
89 		                         rhs.format.BitsPerPixel, fmt);
90 	}
91 	
92 	void _update() in {
93 		assert(this._tex !is null, "No Texture!");
94 	} body {
95 		this._needUpdate = false;
96 		
97 		SDL_Surface* srfc;
98 		scope(exit) SDL_FreeSurface(srfc);
99 		
100 		immutable char* cstr = toStringz(this._text);
101 
102 		SDL_Color fg = void;
103 		SDL_Color bg = void;
104 
105 		this._fg.transferTo(&fg);
106 		this._bg.transferTo(&bg);
107 		
108 		const Font.Mode fmode = this._font.getMode();
109 		
110 		final switch (fmode) {
111 			case Font.Mode.Solid:
112 				srfc = TTF_RenderUTF8_Solid(this._font.ptr, cstr, fg);
113 				break;
114 			case Font.Mode.Shaded:
115 				srfc = TTF_RenderUTF8_Shaded(this._font.ptr, cstr, fg, bg);
116 				break;
117 			case Font.Mode.Blended:
118 				srfc = TTF_RenderUTF8_Blended(this._font.ptr, cstr, fg);
119 				break;
120 		}
121 		
122 		enforce(srfc !is null, "Surface is null.");
123 		
124 		if (fmode != Font.Mode.Blended) {
125 			/// Adapt PixelFormat
126 			SDL_PixelFormat fmt;
127 			fmt.BitsPerPixel = 24;
128 			
129 			SDL_Surface* opt = SDL_ConvertSurface(srfc, &fmt, 0);
130 			scope(exit) SDL_FreeSurface(opt);
131 			
132 			enforce(opt !is null, "Optimized is null.");
133 			enforce(opt.pixels !is null, "Optimized pixels is null.");
134 			
135 			Texture.Format t_fmt = Texture.Format.None;
136 			if (opt.format.Rmask != 0x000000ff)
137 				t_fmt = opt.format.BitsPerPixel == 24 ? Texture.Format.BGR : Texture.Format.BGRA;
138 			
139 			this._storePixel(opt, t_fmt);
140 		} else {
141 			Texture.Format t_fmt = Texture.Format.None;
142 			if (srfc.format.Rmask != 0x000000ff)
143 				t_fmt = srfc.format.BitsPerPixel == 24 ? Texture.Format.BGR : Texture.Format.BGRA;
144 			
145 			this._storePixel(srfc, t_fmt);
146 		}
147 	}
148 	
149 protected:
150 	void _render() in {
151 		assert(this._tex !is null, "No valid Texture.");
152 	} body {
153 		glPushMatrix();
154 		scope(exit) glPopMatrix();
155 		
156 		super._applyTranslation();
157 		
158 		if (this._needUpdate)
159 			this._update();
160 
161 		if (!glIsEnabled(GL_TEXTURE_2D))
162 			glEnable(GL_TEXTURE_2D);
163 		
164 		glPushAttrib(GL_CURRENT_BIT | GL_COLOR_BUFFER_BIT);
165 		scope(exit) glPopAttrib();
166 
167 		float dx = 0f;
168 		float dy = 0f;
169 		float dw = this._tex.width;
170 		float dh = this._tex.height;
171 
172 		float[12] vertices = [
173 			dx,      dy,      0f,
174 			dx + dw, dy,      0f,
175 			dx + dw, dy + dh, 0f,
176 			dx,      dy + dh, 0f
177 		];
178 
179 		float[8] texCoords = [0f, 0f, 1f, 0f, 1f, 1f, 0f, 1f];
180 
181 		VertexRenderer.pointTo(Target.Vertex, &vertices[0]);
182 		VertexRenderer.pointTo(Target.TexCoords, &texCoords[0]);
183 		
184 		scope(exit) {
185 			VertexRenderer.disableAllStates();
186 			this._tex.unbind();
187 		}
188 		
189 		this._tex.bind();
190 
191 		if (this._blend !is null)
192 			this._blend.applyBlending();
193 		
194 		VertexRenderer.drawArrays(Shape.Type.TriangleFan, vertices.length);
195 	}
196 
197 public:
198 	/**
199 	 * CTor
200 	 */
201 	this(ref Font font, string text = null) {
202 		this.replaceFont(font);
203 		
204 		this._text = text;
205 		this._needUpdate = true;
206 		this._tex = new Texture();
207 	}
208 	
209 	/**
210 	 * CTor: Rvalue version
211 	 */
212 	this(Font font, string text = null) {
213 		this(font, text);
214 	}
215 
216 	/**
217 	 * Calculate, store and return the center point.
218 	 */
219 	override ref const(Vector2s) calculateCenter() pure nothrow {
220 		if (this._tex is null)
221 			return super.getCenter();
222 
223 		super.setCenter(this._tex.width / 2, this._tex.height / 2);
224 
225 		return super.getCenter();
226 	}
227 
228 	/**
229 	 * Check whether the bounding box of this Text collide
230 	 * with the bounding box of another Text
231 	 */
232 	bool collideWith(const Text rhs) const {
233 		return this.collideWith(rhs.getBoundingBox());
234 	}
235 	
236 	/**
237 	 * Check whether the bounding box of this Sprite collide
238 	 * with the given Rect
239 	 */
240 	bool collideWith(ref const FloatRect rect) const {
241 		return this.getBoundingBox().intersects(rect);
242 	}
243 	
244 	/**
245 	 * Rvalue version
246 	 */
247 	bool collideWith(const FloatRect rect) const {
248 		return this.collideWith(rect);
249 	}
250 	
251 final:
252 	/**
253 	 * Set (or reset) the current Blend instance.
254 	 */
255 	void setBlend(Blend blend) pure nothrow {
256 		this._blend = blend;
257 	}
258 
259 	/**
260 	 * Returns the current Blend instance, or null.
261 	 */
262 	inout(Blend) getBlend() inout pure nothrow {
263 		return this._blend;
264 	}
265 
266 	/**
267 	 * Returns the bounding box, the area which will be drawn on the screen.
268 	 */
269 	FloatRect getBoundingBox() const pure nothrow in {
270 		assert(this._tex !is null);
271 	} body {
272 		return FloatRect(super.getPosition(), this._tex.width, this._tex.height);
273 	}
274 	
275 	/**
276 	 * Returns the width of the Text Texture
277 	 */
278 	@property
279 	ushort width() const pure nothrow {
280 		return this._tex !is null ? this._tex.width : 0;
281 	}
282 	
283 	/**
284 	 * Returns the height of the Text Texture
285 	 */
286 	@property
287 	ushort height() const pure nothrow {
288 		return this._tex !is null ? this._tex.height : 0;
289 	}
290 
291 	/**
292 	 * Replace the current Font.
293 	 */
294 	void replaceFont(ref Font font) {
295 		this._font = font;
296 		_FontFinalizer ~= &this._font;
297 		
298 		this._needUpdate = true;
299 	}
300 	
301 	/**
302 	 * Rvalue version
303 	 */
304 	void replaceFont(Font font) {
305 		this.replaceFont(font);
306 	}
307 	
308 	/**
309 	 * Get the image containing the rendered characters.
310 	 */
311 	inout(Texture) getTexture() inout pure nothrow {
312 		return this._tex;
313 	}
314 	
315 	/**
316 	 * Returns the current Font object.
317 	 */
318 	ref inout(Font) getFont() inout pure nothrow {
319 		return this._font;
320 	}
321 	
322 	/**
323 	 * Activate an update.
324 	 * The current image will be updated.
325 	 * In most cases, this happens automatically,
326 	 * but sometimes it is usefull.
327 	 */
328 	void forceUpdate() pure nothrow {
329 		this._needUpdate = true;
330 	}
331 	
332 	/**
333 	 * Format a given string and draw it then on the image
334 	 */
335 	void format(Args...)(string text, Args args) {
336 		string formated = .format(text, args);
337 		
338 		if (formated != this._text) {
339 			this._text = formated;
340 			this._needUpdate = true;
341 		}
342 	}
343 	
344 	/**
345 	 * Replace the current string.
346 	 * 
347 	 * Examples:
348 	 * ---
349 	 * Font fnt = new Font("samples/font/arial.ttf", 12);
350 	 * Text t = new Text(font);
351 	 * t("My new string");
352 	 * ---
353 	 */
354 	void opCall(string text) pure nothrow {
355 		if (text != this._text) {
356 			this._text = text;
357 			this._needUpdate = true;
358 		}
359 	}
360 	
361 	/**
362 	 * Replace the current string.
363 	 * 
364 	 * Examples:
365 	 * ---
366 	 * Font fnt = new Font("samples/font/arial.ttf", 12);
367 	 * Text t1 = new Text(font);
368 	 * Text t2 = new Text(font);
369 	 * t1("My new string");
370 	 * t2(t1); // now both t's draw 'My new string' on screen.
371 	 * ---
372 	 */
373 	void opCall(ref const Text t) pure nothrow {
374 		return this.opCall(t.getString());
375 	}
376 	
377 	/**
378 	 * Concatenate the current string with another.
379 	 *
380 	 * Examples:
381 	 * ---
382 	 * Font fnt = new Font("samples/font/arial.ttf", 12);
383 	 * Text t = new Text(font);
384 	 * t("My new string");
385 	 * t ~= "is great!"; // t draws now 'My new string is great' on screen.
386 	 * ---
387 	 * The example above is the same as if you do:
388 	 * ---
389 	 * t += "is great!";
390 	 * ---
391 	 * Both operators (~ and +) are allowed.
392 	 */
393 	ref Text opBinary(string op)(string text) pure nothrow
394 		if (op == "~" || op == "+")
395 	{
396 		this._text ~= text;
397 		this._needUpdate = true;
398 		
399 		return this;
400 	}
401 	
402 	/**
403 	 * Concatenate the current string with another.
404 	 */
405 	ref Text opBinary(string op)(ref const Text t) pure nothrow 
406 		if (op == "~" || op == "+")
407 	{
408 		return this.opBinary!(op)(t.getString());
409 	}
410 	
411 	/**
412 	 * Returns the current string.
413 	 */
414 	string getString() const pure nothrow {
415 		return this._text;
416 	}
417 	
418 	/**
419 	 * Set the (foreground) color.
420 	 */
421 	void setColor(ref const Color col) {
422 		this._needUpdate = true;
423 		this._fg = col;
424 	}
425 	
426 	/**
427 	 * Rvalue version
428 	 */
429 	void setColor(const Color col) {
430 		this.setColor(col);
431 	}
432 	
433 	/**
434 	 * Returns the (foreground) color.
435 	 */
436 	ref const(Color) getColor() const pure nothrow {
437 		return this._fg;
438 	}
439 	
440 	/**
441 	 * Set the background color.
442 	 * Only needed if your Font.Mode is not Font.Mode.Solid.
443 	 */
444 	void setBackgroundColor(ref const Color col) {
445 		this._needUpdate = true;
446 		this._bg = col;
447 	}
448 	
449 	/**
450 	 * Rvalue version
451 	 */
452 	void setBackgroundColor(const Color col) {
453 		this.setBackgroundColor(col);
454 	}
455 	
456 	/**
457 	 * Returns the background color.
458 	 */
459 	ref const(Color) getBackgroundColor() const pure nothrow {
460 		return this._bg;
461 	}
462 }