1 module evael.graphics.gui.controls.TextBox;
2 
3 import std.math : round;
4 import std.array : insertInPlace; 
5 import std.range;
6 import std.string : toStringz;
7 import std.conv : to;
8 
9 import evael.graphics.gui.controls.Control;
10 
11 import evael.system.Input;
12 
13 import evael.utils.Math;
14 import evael.utils.Size;
15 import evael.utils.Color;
16 
17 extern (Windows) short GetKeyState(int nVirtKey);
18 
19 class TextBox : Control
20 {
21 	/// Text
22 	private wstring m_text;
23 
24 	private wstring m_visibleText;
25 
26 	/// Text position
27 	private vec2 m_textPosition;
28 
29 	private int m_textAlign;
30 
31 	/// Caret position
32 	private vec2 m_caretPosition;
33 
34 	/// Caret index in text
35 	private uint m_caretIndex;
36 
37 	/// Characters limit
38 	private uint m_maxCharactersCount;
39 
40 	private bool m_inSelectMode;
41 
42 	private float m_textWidth;
43 
44 	private float m_padding;
45 
46 	public this(in float x, in float y, in int width, in int height)
47 	{
48 		this(vec2(x, y,), Size!int(width, height));
49 	}
50 
51 	public this()(in auto ref vec2 position, in auto ref Size!int size)
52 	{
53 		super(position, size);
54 
55 		this.m_name = "textBox";
56 		this.m_padding = 4.0f;		
57 		this.m_textPosition = vec2(0.0f, 0.0f);
58 		this.m_caretPosition = vec2(this.m_padding, 3.0f);
59 		this.m_inSelectMode = false;
60 		this.m_maxCharactersCount = 500;
61 		this.m_textAlign = NVGalign.NVG_ALIGN_LEFT | NVGalign.NVG_ALIGN_TOP;
62 		this.m_isFocusable = true;
63 	}
64 
65 	/**
66 	 * Renders the textbox
67 	 */
68 	public override void draw(in float deltaTime)
69 	{		
70 		if(!this.m_isVisible)
71 		{
72 			return;
73 		}
74 
75 		super.draw(deltaTime);
76 
77 		immutable x = this.m_realPosition.x;
78 		immutable y = this.m_realPosition.y;
79 		immutable w = this.m_size.width;
80 		immutable h = this.m_size.height;
81 
82 		auto vg = this.m_nvg;
83 
84 		if(this.m_theme.borderType == Theme.BorderType.Solid)
85 		{
86 			nvgBeginPath(vg);
87 			nvgRoundedRect(vg, x, y, w, h, this.m_theme.cornerRadius);
88 			nvgStrokeColor(vg, this.m_theme.borderColor.asNvg);
89 			nvgStroke(vg);
90 		}
91 
92 		nvgSave(vg);
93 
94 		nvgIntersectScissor(vg, x + this.m_padding, y, this.m_size.width - (this.m_padding * 2), this.m_size.height);
95 		
96 		this.m_theme.font.draw(this.m_text, vec2(x + this.m_padding, y) + this.m_textPosition, 
97 			this.m_theme.fontColor, this.m_theme.fontSize, this.m_theme.drawTextShadow);
98 		
99 		nvgRestore(vg);
100 
101 		// Caret
102 		if(this.m_hasFocus)
103 		{
104 			nvgBeginPath(vg);
105 			nvgMoveTo(vg, this.m_caretPosition.x + x, this.m_caretPosition.y + y);
106 			nvgLineTo(vg, this.m_caretPosition.x + x, this.m_caretPosition.y + y + h - 5.5f);
107 			nvgStrokeColor(vg, this.m_theme.fontColor.asNvg);
108 			nvgStroke(vg);
109 		}
110 	}
111 
112 	/**
113 	 * Char event
114 	 * Params: 
115 	 */
116 	public override void onText(in int key)
117 	{
118 		if(this.m_maxCharactersCount != 0 && this.m_text.length >= this.m_maxCharactersCount)
119 			return;
120 
121 		super.onText(key);
122 
123 		immutable newChar = cast(char)key;
124 
125 		// Return button
126 		if(key == 13)
127 		{
128 			return;
129 		}
130 
131 		this.m_text.insertInPlace(this.m_caretIndex, newChar);
132 		this.moveCaretToRight();
133 	}
134 
135 	/**
136 	 * Key pressed event
137 	 * Params: 
138 	 *		 key : key pressed
139 	 */
140 	public override void onKey(in int key)
141 	{
142 		super.onKey(key);
143 
144 		if(this.m_text.length == 0)
145 		{
146 			return;
147 		}
148 
149 		switch(key)
150 		{
151 			// Delete
152 			case Key.Back:
153 			
154 				if(this.m_inSelectMode)
155 				{
156 					this.clear();
157 				}
158 				else
159 				{
160 					if(this.m_caretIndex == 0)
161 					{
162 						return;
163 					}
164 
165 					this.moveCaretToLeft();
166 
167 					auto rest = this.m_text.save();
168 					rest.popFrontN(this.m_caretIndex + 1);
169 					this.m_text = this.m_text[0 .. this.m_caretIndex] ~ rest;
170 				}
171 
172 				break;
173 
174 			// Left
175 			case Key.Left:
176 				if(this.m_caretIndex == 0)
177 				{
178 					return;
179 				}
180 
181 				if(this.m_inSelectMode)
182 				{
183 					this.m_inSelectMode = false;
184 				}
185 
186 				this.moveCaretToLeft();
187 
188 				break;
189 
190 			// Right
191 			case Key.Right:
192 
193 				if(this.m_caretIndex == this.m_text.length)
194 				{
195 					return;
196 				}
197 
198 				if(this.m_inSelectMode)
199 				{
200 					this.m_inSelectMode = false;
201 				}
202 
203 				this.moveCaretToRight();
204 
205 				break;
206 
207 			// A
208 			case Key.A:
209 				if(GetKeyState(0x11) < 0)
210 				{
211 					// TODO: 
212 					/*this.m_inSelectMode = true;
213 
214 					this.m_buffer.bind();
215 
216 					ubyte[16] data;
217 
218 					this.initializeArrayFromColor(data.ptr, Color.red);
219 
220 					glBufferSubData(GL_ARRAY_BUFFER, 48, data.sizeof, data.ptr);*/
221 				}
222 
223 				break;
224 			
225 			// Delete
226 			case Key.Delete:
227 
228 				break;
229 
230 			// Home
231 			case Key.Home:
232 				this.m_caretIndex = 0;
233 				this.m_textPosition.x = 0;
234 				this.m_caretPosition.x = this.m_padding;
235 				break;
236 			
237 			// End
238 			case Key.End:
239 				this.m_caretIndex = this.m_text.length;
240 				this.m_textPosition.x = 0;
241 				this.m_caretPosition.x = this.m_size.width - this.m_padding;
242 				break;
243 
244 			default:
245 				break;
246 		}
247 	}
248 
249 	/**
250 	 * Mouse click
251 	 */
252 	public override void onMouseClick(in MouseButton mouseButton, in ref vec2 mousePosition)
253 	{
254 		super.onMouseClick(mouseButton, mousePosition);
255 		this.switchState!(State.Clicked);
256 
257 		// this.m_theme.borderColor = Color.Orange;
258 
259 		this.m_hasFocus = true;
260 	}
261 
262 	/**
263 	 * Mouse enters control's rect
264 	 * Params:
265 	 * 		 mousePosition : mouse's position
266 	 */
267 	public override void onMouseMove(in ref vec2 mousePosition)
268 	{
269 		this.switchState!(State.Hovered);
270 	}
271 
272 	/**
273 	 * Mouse leaves control's rect
274 	 */
275 	public override void onMouseLeave()
276 	{
277 		this.switchState!(State.Normal);
278 	}
279 
280 
281 	/**
282 	 * Moves caret to left
283 	 */
284 	private void moveCaretToLeft()
285 	{
286 		auto i = --this.m_caretIndex;
287 		
288 		auto glyph = this.m_theme.font.getGlyphPosition(i, this.m_padding, this.m_text, this.m_theme.fontSize);
289 
290 		// Explanation of : glyph.x - (-this.m_textPosition.x);
291 		// We get glyph position with x = 0
292 		// If the text position has been moved, glyph.x become invalid cause text is not at coord x = 0 anymore, but we still get glyph with x = 0
293 		// So we substract this value from glyph.x
294 		this.m_caretPosition.x = glyph.x - (-this.m_textPosition.x);
295 
296 		// We check if the new displayed character is gonna be displayed outside of textbox
297 		if(this.m_caretPosition.x < this.m_padding)
298 		{
299 			// Yes, we need to move text position to the right
300 			// We just need to add abs(caretPosition.x) to text.x
301 			this.m_textPosition.x = this.m_textPosition.x + (- this.m_caretPosition.x ) + this.m_padding;
302 			
303 			this.m_caretPosition.x = this.m_padding;
304 		}
305 	}
306 
307 	/**
308 	 * Moves caret to right
309 	 */
310 	private void moveCaretToRight()
311 	{
312 		auto i = ++this.m_caretIndex;
313 
314 		NVGglyphPosition glyph;
315 
316 		if(this.m_caretIndex < this.m_text.length)
317 		{
318 			glyph = this.m_theme.font.getGlyphPosition(i, this.m_padding, this.m_text, this.m_theme.fontSize);
319 			// Explanation of : glyph.x - (-this.m_textPosition.x);
320 			// We get glyph position with x = 0 (if padding = 0)
321 			// If the text position has been moved, glyph.x become invalid cause text is not at coord x = 0 anymore, but we still get glyph with x = 0
322 			// So we substract this value from glyph.x
323 			this.m_caretPosition.x = glyph.x - (-this.m_textPosition.x);
324 		}
325 		else
326 		{
327 			glyph = this.m_theme.font.getGlyphPosition(i - 1, this.m_padding, this.m_text, this.m_theme.fontSize);
328 			
329 			// Adding text at the end, we need to do that
330 			glyph.x = glyph.maxx;
331 			
332 			this.m_caretPosition.x = glyph.maxx;
333 		}
334 
335 		immutable w = this.m_size.width - this.m_padding;
336 
337 		// We check if the new displayed character is gonna be displayed outside of textbox
338 		if(this.m_caretPosition.x > w)
339 		{
340 			// Yes, we need to move text position to the left
341 			// TextBox width = 120
342 			// if glyph.x = 123, then we move text to -(123 - 120)
343 			this.m_textPosition.x = -(glyph.x - w);
344 
345 			this.m_caretPosition.x = w;
346 		}
347 	}
348 
349 	public override void initialize()
350 	{
351 		super.initialize();
352 
353 		float[4] bounds = this.m_theme.font.getTextBounds(this.m_text, 0, this.m_theme.fontSize);
354 
355 		immutable float h = bounds[3] - bounds[1];
356 
357 		if(h > this.m_size.height)
358 		{
359 			this.m_size.height = cast(int)h + 5;
360 		}
361 
362 		import std.math : round;
363 
364 		this.m_textPosition.y = round(this.m_size.halfHeight - (h / 2));
365 	}
366 
367 	/**
368 	 * Clear the textbox
369 	 */
370 	public void clear() nothrow
371 	{
372 		this.m_text = "";
373 
374 		this.m_textPosition.x = 0.0f;
375 		this.m_caretPosition.x = 0.0f;
376 		this.m_caretIndex = 0;
377 
378 		this.m_inSelectMode = false;
379 
380 		this.switchState!(State.Hovered);
381 	}
382 
383 	@property
384 	{
385 		public string text() const nothrow
386 		{
387 			import std.utf;
388 			return toUTF8(this.m_text);
389 		}
390 
391 		public wstring textw() const nothrow @nogc
392 		{
393 			return this.m_text;
394 		}
395 
396 		public void text(in wstring value) nothrow @nogc
397 		{
398 			this.m_text = value;
399 			this.m_visibleText = value;
400 		}
401 
402 		public void maxCharactersCount(in ushort value) nothrow @nogc
403 		{
404 			this.m_maxCharactersCount = value;
405 		}
406 	}
407 
408 }