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 }