1 /**
2 Grimoire
3 Copyright (c) 2017 Enalye
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
7 from 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
11 it freely, subject to the following restrictions:
12 
13 	1. The origin of this software must not be misrepresented;
14 	   you must not claim that you wrote the original software.
15 	   If you use this software in a product, an acknowledgment
16 	   in the product documentation would be appreciated but
17 	   is not required.
18 
19 	2. Altered source versions must be plainly marked as such,
20 	   and must not be misrepresented as being the original software.
21 
22 	3. This notice may not be removed or altered from any source distribution.
23 */
24 
25 module ui.text;
26 
27 import std.utf;
28 import std.conv: to;
29 
30 import derelict.sdl2.sdl;
31 import derelict.sdl2.ttf;
32 
33 import core.all;
34 import render.all;
35 import common.all;
36 
37 import ui.widget;
38 
39 private {
40 	ITextCache _standardCache, _italicCache, _boldCache, _italicBoldCache;
41 }
42 
43 interface ITextCache {
44 	Sprite get(dchar c);
45 }
46 
47 class FontCache: ITextCache {
48 	private {
49 		Font _font;
50 		Sprite[dchar] _cache;
51 	}
52 
53 	this(Font newFont) {
54 		_font = newFont;
55 	}
56 
57 	Sprite get(dchar c) {
58 		Sprite* cachedSprite = (c in _cache);
59 		if(cachedSprite is null) {
60 			auto texture = new Texture;
61 			texture.loadFromSurface(TTF_RenderUTF8_Blended(_font.font, toUTFz!(const char*)([c].toUTF8), Color.white.toSDL()));
62 			Sprite sprite = texture;
63 			_cache[c] = sprite;
64 			return sprite;
65 		}
66 		else {
67 			return *cachedSprite;
68 		}
69 	}
70 }
71 
72 void setTextStandardFont(ITextCache cache) {
73 	_standardCache = cache;
74 }
75 
76 void setTextItalicFont(ITextCache cache) {
77 	_italicCache = cache;
78 }
79 
80 void setTextBoldFont(ITextCache cache) {
81 	_boldCache = cache;
82 }
83 
84 void setTextItalicBoldFont(ITextCache cache) {
85 	_italicBoldCache = cache;
86 }
87 
88 private {
89 	enum TextTokenType {
90 		CharacterType,
91 		NewLineType,
92 		ColorType,
93 		StandardType,
94 		ItalicType,
95 		BoldType,
96 		ItalicBoldType
97 	}
98 	
99 	struct TextToken {
100 		TextTokenType type;
101 
102 		Sprite charSprite;
103 		Color color;
104 
105 		this(TextTokenType newType) {
106 			type = newType;
107 		}
108 	}
109 }
110 
111 class Text: Widget {
112 	private {
113 		dstring _text;
114 		TextToken[] _tokens;
115 		int[] _lineLengths;
116 		int _rowLength, _maxLineLength, _lineLimit;
117 		Vec2f charSize = Vec2f.zero;
118 		ITextCache _currentCache;
119 	}
120 
121 	@property {
122 		string text() const { return to!string(_text); }
123 		string text(string newText) {
124 			_text = to!dstring(newText);
125 			reload();
126 			return newText;
127 		}
128 	}
129 
130 	this(int newLineLimit = 0) {
131 		_lineLimit = newLineLimit;
132 	}
133 
134 	this(string newText, int newLineLimit = 0) {
135 		_lineLimit = newLineLimit;
136 		text(newText);
137 	}
138 
139 	private uint parseTag(uint i) {
140 		dstring tag;
141 		if(_text[i] != '{')
142 			throw new Exception("Text: A tag must start with a \'{\'");
143 		//'{'
144 		i ++;
145 		while(_text[i] != '}') {
146 			tag ~= _text[i];
147 			i ++;
148 			if(i >= _text.length)
149 				throw new Exception("Text: An opened tag must be closed with \'}\'");
150 		}
151 		TextToken token;
152 		switch(tag) {
153 		case "n"d:
154 			token.type = TextTokenType.NewLineType;
155 			_lineLengths.length ++;
156 			_rowLength ++;
157 			_lineLengths[_rowLength] = 0;
158 			break;
159 		case "s"d:
160 			token.type = TextTokenType.StandardType;
161 			_currentCache = _standardCache;
162 			break;
163 		case "b"d:
164 			token.type = TextTokenType.BoldType;
165 			_currentCache = _boldCache;
166 			break;
167 		case "i"d:
168 			token.type = TextTokenType.ItalicType;
169 			_currentCache = _italicCache;
170 			break;
171 		case "bi"d:
172 			token.type = TextTokenType.ItalicBoldType;
173 			_currentCache = _italicBoldCache;
174 			break;
175 		case "white"d:
176 			token.type = TextTokenType.ColorType;
177 			token.color = Color.white;
178 			break;
179 		case "red"d:
180 			token.type = TextTokenType.ColorType;
181 			token.color = Color.red;
182 			break;
183 		case "blue"d:
184 			token.type = TextTokenType.ColorType;
185 			token.color = Color.blue;
186 			break;
187 		case "green"d:
188 			token.type = TextTokenType.ColorType;
189 			token.color = Color.green;
190 			break;
191 		default:
192 			throw new Exception("Text: The tag \'" ~ to!string(tag) ~ "\' does not exist");
193 		}
194 		_tokens ~= token;
195 		//'}'
196 		i ++;
197 		return i;
198 	}
199 
200 	private void reload() {
201 		if(!_text.length)
202 			return;
203 		_tokens.length = 0L;
204 		_lineLengths.length = 1;
205 		_lineLengths[0] = 0;
206 		_rowLength = 0;
207 		_maxLineLength = 0;
208 		_currentCache = _standardCache;
209 		uint i = 0U;
210 		while(i < _text.length) {
211 			if(_text[i] == '{')
212 				i = parseTag(i);
213 			else {
214 				if(_lineLimit > 0) {
215 					//Auto carriage return.
216 					if(_lineLengths[_rowLength] > _lineLimit) {
217 						TextToken token;
218 						token.type = TextTokenType.NewLineType;
219 						_lineLengths.length ++;
220 						_rowLength ++;
221 						_lineLengths[_rowLength] = 0;
222 						_tokens ~= token;
223 					}
224 				}
225 				auto token = TextToken(TextTokenType.CharacterType);
226 				token.charSprite = _currentCache.get(_text[i]);
227 				_tokens ~= token;
228 				_lineLengths[_rowLength] ++;
229 				if(_lineLengths[_rowLength] > _maxLineLength)
230 					_maxLineLength = _lineLengths[_rowLength];
231 				i ++;
232 			}
233 		}
234 		if(!_maxLineLength)
235 			throw new Exception("Error while fetching cached characters");
236 		charSize = _standardCache.get('a').size;
237 
238 		_size = Vec2f(charSize.x * _maxLineLength, charSize.y * (_rowLength + 1));
239 	}
240 
241 	override void onEvent(Event event) {}
242 
243 	override void update(float deltaTime) {}
244 
245 	override void draw() {
246 		Color currentColor = Color.white;
247 		Vec2i currentPos = Vec2i.zero;
248 		foreach(uint i, token; _tokens) {
249 			switch(token.type) with(TextTokenType) {
250 			case CharacterType:
251 				token.charSprite.color = currentColor;
252 				token.charSprite.draw(_position + charSize * Vec2f(
253 					to!float(currentPos.x) - _maxLineLength / 2f,
254 					to!float(currentPos.y) - _rowLength / 2f));
255 				token.charSprite.color = Color.white;
256 				currentPos.x ++;
257 				break;
258 			case NewLineType:
259 				currentPos.x = 0;
260 				currentPos.y ++;
261 				break;
262 			case StandardType:
263 				break;
264 			case BoldType:
265 				break;
266 			case ItalicType:
267 				break;
268 			case ItalicBoldType:
269 				break;
270 			case ColorType:
271 				currentColor = token.color;
272 				break;
273 			default:
274 				throw new Exception("Text: Invalid token");
275 			}
276 		}
277 	}
278 }