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 }