1 /**
2 * Copyright © Novelate 2020
3 * License: MIT (https://github.com/Novelate/NovelateEngine/blob/master/LICENSE)
4 * Author: Jacob Jensen (bausshf)
5 * Website: https://novelate.com/
6 * ------
7 * Novelate is a free and open-source visual novel engine and framework written in the D programming language.
8 * It can be used freely for both personal and commercial projects.
9 * ------
10 * Module Description:
11 * A timed text component is useful for displaying text that is rendered over time looking like an automated typing animation.
12 */
13 module novelate.ui.timedtext;
14 
15 import novelate.ui.component;
16 import novelate.ui.textwrap;
17 import novelate.fonts;
18 import novelate.config;
19 import novelate.queue;
20 import novelate.buildstate;
21 
22 /// Alias for delegate handler.
23 private alias _DELEGATE = void delegate(MouseButton button, ref bool stopEvent);
24 
25 /// Wrapper around timed text, which is text that is gradually rendered over time.
26 final class TimedText : Component
27 {
28   private:
29   /// The text component.
30   ExternalText _textComponent;
31   /// The original text without wrapping.
32   dstring _originalText;
33   /// The text with wrapping.
34   dstring _text;
35   /// The text count.
36   size_t _textCount;
37   /// The original text speed.
38   size_t _originalTextSpeed;
39   /// The text speed.
40   size_t _textSpeed;
41   /// The font name.
42   string _fontName;
43   /// The font.
44   ExternalFont _font;
45   /// The font size.
46   size_t _fontSize;
47   /// The box width.
48   size_t _boxWidth;
49   /// Boolean determining whether the text has finished rendering.
50   bool _hasFinished;
51   /// The color.
52   Paint _color;
53   /// Boolean determining whether shift is held or not.
54   bool _shiftHeld;
55 
56   public:
57   final:
58   /// Creates a new timed text component.
59   this()
60   {
61     super();
62 
63     globalMouseReleaseTextFinished = new Queue!_DELEGATE;
64 
65     _fontName = config.defaultFont;
66     _font = retrieveFont(_fontName, FontStyle.normal);
67     _fontSize = config.defaultFontSize;
68 
69     _textComponent = new ExternalText;
70     _textComponent.setFont(_font);
71     _textComponent.setString("");
72     _textComponent.setCharacterSize(_fontSize);
73 
74     _textCount = 1;
75     _textSpeed = 1;
76 
77     super.globalKeyPress = (k, ref s)
78     {
79       if (k == KeyboardKey.LShift)
80       {
81         _originalTextSpeed = _textSpeed;
82 
83         _textSpeed *= 3;
84 
85         _shiftHeld = true;
86       }
87     };
88 
89     super.globalKeyRelease = (k, ref s)
90     {
91       if (k == KeyboardKey.returnKey || k == KeyboardKey.space)
92       {
93         super.globalMouseRelease(MouseButton.left, s);
94       }
95       else if (k == KeyboardKey.LShift)
96       {
97         _textSpeed = _originalTextSpeed;
98 
99         _shiftHeld = false;
100       }
101     };
102 
103     super.globalMouseRelease = (b, ref s)
104     {
105       if (b != MouseButton.left)
106       {
107         return;
108       }
109 
110       if (_hasFinished)
111       {
112         if (globalMouseReleaseTextFinished.has)
113         {
114           auto action = globalMouseReleaseTextFinished.dequeue();
115 
116           if (action)
117           {
118             action(b, s);
119           }
120         }
121       }
122       else
123       {
124         if (!_text || !_text.length)
125         {
126           return;
127         }
128 
129         _textComponent.setString(_text);
130         _textCount = _text.length;
131         _hasFinished = true;
132       }
133     };
134   }
135 
136   /// A queue of handlers for when the text has finished rendering by mouse release.
137   Queue!_DELEGATE globalMouseReleaseTextFinished;
138 
139   @property
140   {
141     /// Gets the text.
142     dstring text() { return _text; }
143 
144     /// Sets the text.
145     void text(dstring newText)
146     {
147       _originalText = newText;
148 
149       _text = wrapableText(_originalText, _fontName, _fontSize, _boxWidth);
150 
151       _textCount = 1;
152       _hasFinished = false;
153     }
154 
155     /// Gets the font name.
156     string fontName() { return _fontName; }
157 
158     /// Sets the font name.
159     void fontName(string newFontName)
160     {
161       _fontName = newFontName;
162 
163       _font = retrieveFont(_fontName, FontStyle.normal);
164       _textComponent.setFont(_font);
165     }
166 
167     /// Gets the font size.
168     size_t fontSize() { return _fontSize; }
169 
170     /// Sets the font size.
171     void fontSize(size_t newFontSize)
172     {
173       _fontSize = newFontSize;
174 
175       _textComponent.setCharacterSize(_fontSize);
176     }
177 
178     /// Gets the color.
179     Paint color() { return _color; }
180 
181     /// Sets the color.
182     void color(Paint newColor)
183     {
184       _color = newColor;
185 
186       _textComponent.setColor(_color);
187     }
188 
189     /// Gets the text speed.
190     size_t textSpeed() { return _textSpeed; }
191 
192     /// Sets the text speed.
193     void textSpeed(size_t newTextSpeed)
194     {
195       _textSpeed = newTextSpeed;
196       _originalTextSpeed = _textSpeed;
197 
198       if (_shiftHeld)
199       {
200         _textSpeed *= 3;
201       }
202     }
203   }
204 
205   /// Fps counter for when the text is rendering.
206   private size_t _fpsCounter = 0;
207 
208   /// See: Component.render()
209   override void render(ExternalWindow window)
210   {
211     if (!_hasFinished)
212     {
213       _fpsCounter++;
214 
215       if (_fpsCounter >= 2)
216       {
217         _fpsCounter = 0;
218 
219         if (_text && _text.length)
220         {
221           if (_textCount > _text.length)
222           {
223             _textCount = _text.length;
224           }
225 
226           _textComponent.setString(_text[0 .. _textCount]);
227 
228           _textCount += _textSpeed;
229 
230           _hasFinished = _textCount >= _text.length;
231 
232           if (_hasFinished)
233           {
234             _textComponent.setString(_text);
235           }
236         }
237       }
238     }
239 
240     _textComponent.draw(window);
241   }
242 
243   /// See: Component.refresh()
244   override void refresh(size_t width, size_t height)
245   {
246     _boxWidth = width;
247     _boxWidth -= ((config.defaultDialoguePadding * 2) + (config.defaultDialogueMargin * 2));
248 
249     size_t boxHeight;
250     if (width == 800)
251     {
252       boxHeight = config.defaultDialogueHeight800;
253     }
254     else if (width == 1024)
255     {
256       boxHeight = config.defaultDialogueHeight1024;
257     }
258     else if (width == 1280)
259     {
260       boxHeight = config.defaultDialogueHeight1280;
261     }
262 
263      boxHeight -= cast(size_t)(cast(double)config.defaultDialogueNameFontSize * 1.5);
264 
265     _textComponent.position = FloatVector(config.defaultDialogueMargin + config.defaultDialoguePadding, ((height + config.defaultDialoguePadding) - boxHeight));
266 
267     _text = wrapableText(_originalText, _fontName, _fontSize, _boxWidth);
268 
269     if (_text && _text.length)
270     {
271       if (_hasFinished)
272       {
273         _textComponent.setString(_text);
274       }
275       else
276       {
277         if (_textCount > _text.length)
278         {
279           _textCount = _text.length;
280         }
281 
282         _textComponent.setString(_text[0 .. _textCount]);
283       }
284     }
285 
286     updateInternalPosition(_textComponent.position);
287   }
288 
289   /// See: Component.updateSize()
290   override void updateSize()
291   {
292   }
293 
294   /// See: Component.updatePosition()
295   override void updatePosition()
296   {
297     _textComponent.position = super.position;
298   }
299 
300   static if (isManualMemory)
301   {
302     /// See: Component.clean()
303     override void clean()
304     {
305       if (_textComponent)
306       {
307         _textComponent.clean();
308       }
309     }
310   }
311 }