1 /**
2     Input field
3 
4     Copyright: (c) Enalye 2017
5     License: Zlib
6     Authors: Enalye
7 */
8 
9 module atelier.ui.inputfield;
10 
11 import std.utf;
12 import std.conv : to;
13 import std..string : indexOf;
14 import atelier.core, atelier.render, atelier.common;
15 import atelier.ui.gui_element, atelier.ui.label;
16 
17 /// Editable field.
18 class InputField : GuiElement {
19     private {
20         Label _label;
21         Color _borderColor;
22         float _caretAlpha = 1f;
23         dstring _text, _allowedCharacters;
24         Timer _timer;
25         uint _caretIndex = 0U, _selectionIndex = 0u;
26         Vec2f _caretPosition = Vec2f.zero, _selectionPosition = Vec2f.zero;
27         uint _limit = 80u;
28     }
29 
30     @property {
31         /// The displayed text.
32         string text() const {
33             return to!string(_text);
34         }
35         /// Ditto
36         string text(string text_) {
37             _text = to!dstring(text_);
38             _caretIndex = to!uint(_text.length);
39             _selectionIndex = _caretIndex;
40             _label.text = text_;
41             return text_;
42         }
43 
44         /// Label font
45         Font font() {
46             return _label.font;
47         }
48         /// Ditto
49         Font font(Font font_) {
50             return _label.font = font_;
51         }
52 
53         /// Max number of characters.
54         uint limit() const {
55             return _limit;
56         }
57         /// Ditto
58         uint limit(uint newLimit) {
59             _limit = newLimit;
60             if (_text.length > _limit) {
61                 _text.length = _limit;
62                 _label.text = to!string(_text);
63             }
64             if (_caretIndex > _limit)
65                 _caretIndex = _limit;
66             return _limit;
67         }
68     }
69 
70     /// Color of the inputfield's selection area
71     Color selectionColor = Color(.23f, .30f, .37f);
72     /// Color of the inputfield's cursor
73     Color caretColor = Color.white;
74 
75     /// The size is used to setup a canvas, avoid resizing too often. \
76     /// Set startWithFocus to true if you want the inputfield to accept inputs immediatly.
77     this(Vec2f size_, string defaultText = "", bool startWithFocus = false) {
78         size = size_;
79         _label = new Label(defaultText, new TrueTypeFont(veraMonoFontData));
80         _label.setAlign(GuiAlignX.left, GuiAlignY.center);
81         appendChild(_label);
82         hasFocus = startWithFocus;
83         _text = to!dstring(defaultText);
84         _caretIndex = to!uint(_text.length);
85         _selectionIndex = _caretIndex;
86 
87         _borderColor = Color.white;
88         _caretAlpha = 1f;
89         _timer.mode = Timer.Mode.bounce;
90         _timer.start(1f);
91         hasCanvas(true);
92     }
93 
94     private void insertText(dstring textInput) {
95         if (_caretIndex == _selectionIndex) {
96             if (_caretIndex == _text.length)
97                 _text ~= textInput;
98             else if (_caretIndex == 0U)
99                 _text = textInput ~ _text;
100             else
101                 _text = _text[0U .. _caretIndex] ~ textInput ~ _text[_caretIndex .. $];
102             _caretIndex += textInput.length;
103         }
104         else {
105             const int minSelect = min(_caretIndex, _selectionIndex);
106             const int maxSelect = max(_caretIndex, _selectionIndex);
107             if (minSelect == 0U && maxSelect == _text.length) {
108                 _text = textInput;
109                 _caretIndex = cast(uint) _text.length;
110             }
111             else if (minSelect == 0U) {
112                 _text = textInput ~ _text[maxSelect .. $];
113                 _caretIndex = cast(uint) textInput.length;
114             }
115             else if (maxSelect == _text.length) {
116                 _text = _text[0U .. minSelect] ~ textInput;
117                 _caretIndex = cast(uint) _text.length;
118             }
119             else {
120                 _text = _text[0U .. minSelect] ~ textInput ~ _text[maxSelect .. $];
121                 _caretIndex = minSelect + cast(uint) textInput.length;
122             }
123         }
124         _label.text = to!string(_text);
125         _selectionIndex = _caretIndex;
126         triggerCallback();
127     }
128 
129     private void removeSelection(int direction) {
130         if (_text.length) {
131             if (_caretIndex == _selectionIndex) {
132                 if (direction > 0) {
133                     if (_caretIndex == 0U)
134                         _text = _text[1U .. $];
135                     else if (_caretIndex != _text.length) {
136                         _text = _text[0U .. _caretIndex] ~ _text[_caretIndex + 1 .. $];
137                     }
138                 }
139                 else {
140                     if (_caretIndex == _text.length) {
141                         _text.length--;
142                         _caretIndex--;
143                     }
144                     else if (_caretIndex != 0U) {
145                         _text = _text[0U .. _caretIndex - 1] ~ _text[_caretIndex .. $];
146                         _caretIndex--;
147                     }
148                 }
149             }
150             else {
151                 const int minSelect = min(_caretIndex, _selectionIndex);
152                 const int maxSelect = max(_caretIndex, _selectionIndex);
153                 if (minSelect == 0 && maxSelect == _text.length) {
154                     _text.length = 0;
155                     _caretIndex = 0;
156                 }
157                 else if (minSelect == 0) {
158                     _text = _text[maxSelect .. $];
159                     _caretIndex = 0;
160                 }
161                 else if (maxSelect == _text.length) {
162                     _text = _text[0 .. minSelect];
163                     _caretIndex = minSelect;
164                 }
165                 else {
166                     _text = _text[0 .. minSelect] ~ _text[maxSelect .. $];
167                     _caretIndex = minSelect;
168                 }
169             }
170             _label.text = to!string(_text);
171             _selectionIndex = _caretIndex;
172         }
173         triggerCallback();
174     }
175 
176     private string getSelection() {
177         dstring txt = to!dstring(_label.text);
178         if (_selectionIndex == _caretIndex || (txt.length == 0)) {
179             return "";
180         }
181         const int minIndex = min(_selectionIndex, _caretIndex);
182         const int maxIndex = max(_selectionIndex, _caretIndex);
183         txt = txt[minIndex .. maxIndex];
184         return to!string(txt);
185     }
186 
187     override void onEvent(Event event) {
188         if (hasFocus) {
189             switch (event.type) with (Event.Type) {
190             case keyInput:
191                 if (_caretIndex >= _limit)
192                     break;
193                 const auto textInput = to!dstring(event.input.text);
194                 if (_allowedCharacters.length) {
195                     if (indexOf(_allowedCharacters, textInput) == -1)
196                         break;
197                 }
198                 insertText(textInput);
199                 break;
200             case keyDown:
201                 switch (event.key.button) with (KeyButton) {
202                 case right:
203                     if (_caretIndex < _text.length)
204                         _caretIndex++;
205                     if (!isButtonDown(KeyButton.leftShift) && !isButtonDown(KeyButton.rightShift)) {
206                         _selectionIndex = _caretIndex;
207                     }
208                     break;
209                 case left:
210                     if (_caretIndex > 0U)
211                         _caretIndex--;
212                     if (!isButtonDown(KeyButton.leftShift) && !isButtonDown(KeyButton.rightShift)) {
213                         _selectionIndex = _caretIndex;
214                     }
215                     break;
216                 case remove:
217                     removeSelection(1);
218                     break;
219                 case backspace:
220                     removeSelection(-1);
221                     break;
222                 case end:
223                     _caretIndex = cast(uint) _text.length;
224                     if (!isButtonDown(KeyButton.leftShift) && !isButtonDown(KeyButton.rightShift)) {
225                         _selectionIndex = _caretIndex;
226                     }
227                     break;
228                 case home:
229                     _caretIndex = 0;
230                     if (!isButtonDown(KeyButton.leftShift) && !isButtonDown(KeyButton.rightShift)) {
231                         _selectionIndex = _caretIndex;
232                     }
233                     break;
234                 case v:
235                     if (isButtonDown(KeyButton.leftControl) || isButtonDown(KeyButton.rightControl)) {
236                         if (hasClipboard()) {
237                             insertText(to!dstring(getClipboard()));
238                         }
239                     }
240                     break;
241                 case c:
242                     if (isButtonDown(KeyButton.leftControl) || isButtonDown(KeyButton.rightControl)) {
243                         setClipboard(getSelection());
244                     }
245                     break;
246                 case x:
247                     if (isButtonDown(KeyButton.leftControl) || isButtonDown(KeyButton.rightControl)) {
248                         setClipboard(getSelection());
249                         removeSelection(0);
250                     }
251                     break;
252                 case a:
253                     if (isButtonDown(KeyButton.leftControl) || isButtonDown(KeyButton.rightControl)) {
254                         _caretIndex = cast(uint) _text.length;
255                         _selectionIndex = 0U;
256                     }
257                     break;
258                 default:
259                     break;
260                 }
261                 break;
262             default:
263                 break;
264             }
265         }
266     }
267 
268     override void update(float deltaTime) {
269         _caretPosition = Vec2f(_label.position.x + (_label.size.x / _text.length) * _caretIndex,
270                 _label.origin.y);
271         _selectionPosition = Vec2f(_label.position.x + (
272                 _label.size.x / _text.length) * _selectionIndex, _label.origin.y);
273         _label.position = Vec2f(10f, 0f);
274 
275         if (_caretPosition.x > canvas.position.x + canvas.size.x / 2f - 10f)
276             canvas.position.x = _caretPosition.x - canvas.size.x / 2f + 10f;
277         else if (_caretPosition.x < canvas.position.x - canvas.size.x / 2f + 10f)
278             canvas.position.x = _caretPosition.x + canvas.size.x / 2f - 10f;
279 
280         if (hasFocus)
281             _borderColor = lerp(_borderColor, Color.white, deltaTime * .25f);
282         else
283             _borderColor = lerp(_borderColor, Color.white * .21f, deltaTime * .1f);
284 
285         _timer.update(deltaTime);
286         _caretAlpha = lerp(1f, .21f, _timer.value01);
287     }
288 
289     override void draw() {
290         if (_text.length && hasFocus) {
291             if (_caretIndex != _selectionIndex) {
292                 const float minPos = min(_selectionPosition.x, _caretPosition.x);
293                 const float selectionSize = abs(_selectionPosition.x - _caretPosition.x);
294                 drawFilledRect(Vec2f(minPos, _selectionPosition.y),
295                         Vec2f(selectionSize, _label.size.y), selectionColor, .7f);
296             }
297             drawFilledRect(_caretPosition, Vec2f(2f, _label.size.y), caretColor, _caretAlpha);
298         }
299     }
300 
301     override void drawOverlay() {
302         drawRect(origin, size, _borderColor);
303     }
304 
305     /// Resets the text.
306     void clear() {
307         _text.length = 0L;
308         _caretIndex = 0u;
309         _selectionIndex = 0u;
310         _label.text = "";
311     }
312 
313     /// It will discard any characters from keyboard that aren't in this list.
314     void setAllowedCharacters(dstring allowedCharacters) {
315         _allowedCharacters = allowedCharacters;
316     }
317 }