1 /** 2 * Copyright: Enalye 3 * License: Zlib 4 * Authors: Enalye 5 */ 6 module atelier.ui.text; 7 8 import std.regex; 9 import std.algorithm.comparison : min; 10 import std.utf, std.random; 11 import std.conv : to; 12 import std..string; 13 import atelier.core, atelier.render, atelier.common; 14 import atelier.ui.gui_element; 15 16 /// Dynamic text rendering 17 final class Text : GuiElement { 18 private { 19 Font _font; 20 Timer _timer, _effectTimer; 21 dstring _text; 22 size_t _currentIndex; 23 Token[] _tokens; 24 float _delay = 0f; 25 int _charSpacing = 0; 26 } 27 28 @property { 29 /// Text 30 string text() const { 31 return to!string(_text); 32 } 33 /// Ditto 34 string text(string text_) { 35 _text = to!dstring(text_); 36 restart(); 37 tokenize(); 38 return text_; 39 } 40 41 /// Font 42 Font font() const { 43 return cast(Font) _font; 44 } 45 /// Ditto 46 Font font(Font font_) { 47 _font = font_; 48 restart(); 49 tokenize(); 50 return _font; 51 } 52 53 /// Is the text still being displayed ? 54 bool isPlaying() const { 55 return _timer.isRunning() || (_currentIndex < _tokens.length); 56 } 57 58 /// Default delay between each character 59 float delay() const { 60 return _delay; 61 } 62 /// Ditto 63 float delay(float delay_) { 64 return _delay = delay_; 65 } 66 67 /// Characters per second 68 int cps() const { 69 return (_delay <= 0f) ? 0 : cast(int)(1f / _delay); 70 } 71 /// Ditto 72 int cps(int cps_) { 73 _delay = (cps_ == 0) ? 0f : (1f / cps_); 74 return cps_; 75 } 76 77 /// Default additionnal spacing between each character 78 int charSpacing() const { 79 return _charSpacing; 80 } 81 /// Ditto 82 int charSpacing(int charSpacing_) { 83 return _charSpacing = charSpacing_; 84 } 85 } 86 87 /// Build text with default font 88 this(string text_ = "", Font font_ = getDefaultFont()) { 89 setInitFlags(Init.notInteractable); 90 _font = font_; 91 _text = to!dstring(text_); 92 tokenize(); 93 _effectTimer.mode = Timer.Mode.loop; 94 _effectTimer.start(1f); 95 } 96 97 /// Restart the reading from the beginning 98 void restart() { 99 _currentIndex = 0; 100 _timer.reset(); 101 } 102 103 /// Add to current text 104 void append(string text_) { 105 _text ~= to!dstring(text_); 106 tokenize(); 107 } 108 109 private struct Token { 110 enum Type { 111 character, 112 line, 113 scale, 114 charSpacing, 115 color, 116 delay, 117 pause, 118 effect 119 } 120 121 Type type; 122 123 union { 124 CharToken character; 125 ScaleToken scale; 126 SpacingToken charSpacing; 127 ColorToken color; 128 DelayToken delay; 129 PauseToken pause; 130 EffectToken effect; 131 } 132 133 struct CharToken { 134 dchar character; 135 } 136 137 struct ScaleToken { 138 float scale; 139 } 140 141 struct SpacingToken { 142 int charSpacing; 143 } 144 145 struct ColorToken { 146 Color color; 147 } 148 149 struct DelayToken { 150 float duration; 151 } 152 153 struct PauseToken { 154 float duration; 155 } 156 157 struct EffectToken { 158 enum Type { 159 none, 160 wave, 161 bounce, 162 shake, 163 rainbow 164 } 165 166 Type type; 167 } 168 } 169 170 private void tokenize() { 171 size_t current = 0; 172 _tokens.length = 0; 173 while (current < _text.length) { 174 if (_text[current] == '\n') { 175 current++; 176 Token token; 177 token.type = Token.Type.line; 178 _tokens ~= token; 179 } 180 else if (_text[current] == '{') { 181 current++; 182 size_t endOfBrackets = indexOf(_text, "}", current); 183 if (endOfBrackets == -1) 184 break; 185 dstring brackets = _text[current .. endOfBrackets]; 186 current = endOfBrackets + 1; 187 188 foreach (modifier; brackets.split(",")) { 189 if (!modifier.length) 190 continue; 191 auto parameters = splitter(modifier, regex("[:=]"d)); 192 if (parameters.empty) 193 continue; 194 const dstring cmd = parameters.front; 195 parameters.popFront(); 196 switch (cmd) { 197 case "c": 198 case "color": 199 Token token; 200 token.type = Token.Type.color; 201 if (!parameters.empty) { 202 if (!parameters.front.length) 203 continue; 204 if (parameters.front[0] == '#') { 205 continue; 206 // TODO: #FFFFFF RGB color format 207 } 208 else { 209 switch (parameters.front) { 210 case "red": 211 token.color.color = Color.red; 212 break; 213 case "blue": 214 token.color.color = Color.blue; 215 break; 216 case "white": 217 token.color.color = Color.white; 218 break; 219 case "black": 220 token.color.color = Color.black; 221 break; 222 case "yellow": 223 token.color.color = Color.yellow; 224 break; 225 case "cyan": 226 token.color.color = Color.cyan; 227 break; 228 case "magenta": 229 token.color.color = Color.magenta; 230 break; 231 case "silver": 232 token.color.color = Color.silver; 233 break; 234 case "gray": 235 case "grey": 236 token.color.color = Color.gray; 237 break; 238 case "maroon": 239 token.color.color = Color.maroon; 240 break; 241 case "olive": 242 token.color.color = Color.olive; 243 break; 244 case "green": 245 token.color.color = Color.green; 246 break; 247 case "purple": 248 token.color.color = Color.purple; 249 break; 250 case "teal": 251 token.color.color = Color.teal; 252 break; 253 case "navy": 254 token.color.color = Color.navy; 255 break; 256 case "pink": 257 token.color.color = Color.pink; 258 break; 259 case "orange": 260 token.color.color = Color.orange; 261 break; 262 default: 263 continue; 264 } 265 } 266 } 267 else 268 continue; 269 _tokens ~= token; 270 break; 271 case "s": 272 case "scale": 273 case "size": 274 case "sz": 275 Token token; 276 token.type = Token.Type.scale; 277 if (!parameters.empty) 278 token.scale.scale = parameters.front.to!float; 279 else 280 continue; 281 _tokens ~= token; 282 break; 283 case "l": 284 case "ln": 285 case "line": 286 case "br": 287 Token token; 288 token.type = Token.Type.line; 289 _tokens ~= token; 290 break; 291 case "w": 292 case "wait": 293 case "p": 294 case "pause": 295 Token token; 296 token.type = Token.Type.pause; 297 if (!parameters.empty) 298 token.pause.duration = parameters.front.to!float; 299 else 300 continue; 301 _tokens ~= token; 302 break; 303 case "fx": 304 case "effect": 305 Token token; 306 token.type = Token.Type.effect; 307 token.effect.type = Token.EffectToken.Type.none; 308 if (!parameters.empty) { 309 switch (parameters.front) { 310 case "wave": 311 token.effect.type = Token.EffectToken.Type.wave; 312 break; 313 case "bounce": 314 token.effect.type = Token.EffectToken.Type.bounce; 315 break; 316 case "shake": 317 token.effect.type = Token.EffectToken.Type.shake; 318 break; 319 case "rainbow": 320 token.effect.type = Token.EffectToken.Type.rainbow; 321 break; 322 default: 323 token.effect.type = Token.EffectToken.Type.none; 324 break; 325 } 326 } 327 else 328 continue; 329 _tokens ~= token; 330 break; 331 case "d": 332 case "dl": 333 case "delay": 334 Token token; 335 token.type = Token.Type.delay; 336 if (!parameters.empty) 337 token.delay.duration = parameters.front.to!float; 338 else 339 continue; 340 _tokens ~= token; 341 break; 342 case "cps": 343 Token token; 344 token.type = Token.Type.delay; 345 if (!parameters.empty) { 346 const int cps = parameters.front.to!int; 347 token.delay.duration = (cps == 0) ? 0f : (1f / cps); 348 } 349 else 350 continue; 351 _tokens ~= token; 352 break; 353 default: 354 continue; 355 } 356 } 357 } 358 else { 359 Token token; 360 token.type = Token.Type.character; 361 token.character.character = _text[current]; 362 _tokens ~= token; 363 current++; 364 } 365 } 366 reload(); 367 } 368 369 private void reload() { 370 Vec2f totalSize_ = Vec2f(0f, _font.ascent - _font.descent); 371 float lineWidth = 0f; 372 dchar prevChar; 373 int charSpacing_ = _charSpacing; 374 float charScale_ = min(cast(int) scale.x, cast(int) scale.y); 375 foreach (Token token; _tokens) { 376 final switch (token.type) with (Token.Type) { 377 case line: 378 lineWidth = 0f; 379 totalSize_.y += _font.lineSkip; 380 break; 381 case character: 382 const Glyph metrics = _font.getMetrics(token.character.character); 383 lineWidth += _font.getKerning(prevChar, token.character.character) * charScale_; 384 lineWidth += (metrics.advance + charSpacing_) * charScale_; 385 if (lineWidth > totalSize_.x) 386 totalSize_.x = lineWidth; 387 prevChar = token.character.character; 388 break; 389 case charSpacing: 390 charSpacing_ = token.charSpacing.charSpacing; 391 break; 392 case scale: 393 charScale_ = token.scale.scale; 394 break; 395 case pause: 396 case delay: 397 case color: 398 case effect: 399 break; 400 } 401 } 402 size = totalSize_; 403 } 404 405 override void update(float deltaTime) { 406 _timer.update(deltaTime); 407 _effectTimer.update(deltaTime); 408 } 409 410 override void draw() { 411 Vec2f pos = origin; 412 dchar prevChar; 413 Color charColor_ = color; 414 float charDelay_ = _delay; 415 float charScale_ = min(cast(int) scale.x, cast(int) scale.y); 416 int charSpacing_ = _charSpacing; 417 Token.EffectToken.Type charEffect_ = Token.EffectToken.Type.none; 418 Vec2f totalSize_ = Vec2f.zero; 419 Timer waveTimer = _effectTimer; 420 foreach (size_t index, Token token; _tokens) { 421 final switch (token.type) with (Token.Type) { 422 case character: 423 if (_currentIndex == index) { 424 if (_timer.isRunning) 425 break; 426 if (charDelay_ > 0f) 427 _timer.start(charDelay_); 428 _currentIndex++; 429 } 430 Glyph metrics = _font.getMetrics(token.character.character); 431 pos.x += _font.getKerning(prevChar, token.character.character) * charScale_; 432 Vec2f drawPos = Vec2f(pos.x + metrics.offsetX * charScale_, 433 pos.y - metrics.offsetY * charScale_); 434 435 final switch (charEffect_) with (Token.EffectToken.Type) { 436 case none: 437 break; 438 case wave: 439 waveTimer.update(1f); 440 waveTimer.update(1f); 441 waveTimer.update(1f); 442 waveTimer.update(1f); 443 waveTimer.update(1f); 444 waveTimer.update(1f); 445 if (waveTimer.value01 < .5f) 446 drawPos.y -= lerp!float(_font.descent, _font.ascent, 447 easeInOutSine(waveTimer.value01 * 2f)); 448 else 449 drawPos.y -= lerp!float(_font.ascent, _font.descent, 450 easeInOutSine((waveTimer.value01 - .5f) * 2f)); 451 break; 452 case bounce: 453 if (_effectTimer.value01 < .5f) 454 drawPos.y -= lerp!float(_font.descent, _font.ascent, 455 easeOutSine(_effectTimer.value01 * 2f)); 456 else 457 drawPos.y -= lerp!float(_font.ascent, _font.descent, 458 easeInSine((_effectTimer.value01 - .5f) * 2f)); 459 break; 460 case shake: 461 drawPos += Vec2f(uniform01(), uniform01()) * charScale_ * 5f; 462 break; 463 case rainbow: 464 break; 465 } 466 467 metrics.draw(drawPos, charScale_, charColor_, 1f); 468 pos.x += (metrics.advance + charSpacing_) * charScale_; 469 prevChar = token.character.character; 470 if ((pos.x - origin.x) > totalSize_.x) { 471 totalSize_.x = (pos.x - origin.x); 472 } 473 if (((_font.ascent - _font.descent) * charScale_) > totalSize_.y) { 474 totalSize_.y = (_font.ascent - _font.descent) * charScale_; 475 } 476 break; 477 case line: 478 if (_currentIndex == index) 479 _currentIndex++; 480 pos.x = origin.x; 481 pos.y += _font.lineSkip * charScale_; 482 if ((pos.y - origin.y) > totalSize_.y) { 483 totalSize_.y = (pos.y - origin.y); 484 } 485 break; 486 case scale: 487 if (_currentIndex == index) 488 _currentIndex++; 489 charScale_ = token.scale.scale; 490 break; 491 case charSpacing: 492 if (_currentIndex == index) 493 _currentIndex++; 494 charSpacing_ = token.charSpacing.charSpacing; 495 break; 496 case color: 497 if (_currentIndex == index) 498 _currentIndex++; 499 charColor_ = token.color.color; 500 break; 501 case delay: 502 if (_currentIndex == index) 503 _currentIndex++; 504 charDelay_ = token.delay.duration; 505 break; 506 case pause: 507 if (_currentIndex == index) { 508 if (_timer.isRunning) 509 break; 510 if (token.pause.duration > 0f) 511 _timer.start(token.pause.duration); 512 _currentIndex++; 513 } 514 break; 515 case effect: 516 if (_currentIndex == index) 517 _currentIndex++; 518 charEffect_ = token.effect.type; 519 break; 520 } 521 if (index == _currentIndex) 522 break; 523 } 524 } 525 }