1 ///
2 module nanogui.textbox;
3 
4 /*
5 	nanogui.textbox.d -- Fancy text box with builtin regular
6 	expression-based validation
7 
8 	The text box widget was contributed by Christian Schueller.
9 
10 	NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>.
11 	The widget drawing code is based on the NanoVG demo application
12 	by Mikko Mononen.
13 
14 	All rights reserved. Use of this source code is governed by a
15 	BSD-style license that can be found in the LICENSE.txt file.
16 */
17 
18 import std.array : replaceInPlace;
19 import std.algorithm : swap;
20 import std.math : abs;
21 import std.traits : isIntegral, isFloatingPoint, isSigned;
22 
23 import arsd.nanovega;
24 import nanogui.widget : Widget;
25 import nanogui.theme : Theme;
26 import nanogui.common : Vector2i, MouseAction, MouseButton, Cursor, 
27     Color, boxGradient, fillColor, Vector2f, Key, KeyAction, KeyMod,
28 	NanoContext;
29 
30 private auto squeezeGlyphs(T)(T[] glyphs_buffer, T[] glyphs)
31 {
32     import std.algorithm : uniq;
33     size_t i;
34     foreach(e; glyphs.uniq!"a.x==b.x")
35         glyphs_buffer[i++] = e;
36 
37     return glyphs_buffer[0..i];
38 }
39 
40 /**
41  * Fancy text box with builtin regular expression-based validation.
42  *
43  * Remark:
44  *     This class overrides `nanogui.Widget.mIconExtraScale` to be `0.8f`,
45  *     which affects all subclasses of this Widget.  Subclasses must explicitly
46  *     set a different value if needed (e.g., in their constructor).
47  */
48 class TextBox : Widget
49 {
50 public:
51 	/// How to align the text in the text box.
52 	enum Alignment {
53 		Left,
54 		Center,
55 		Right
56 	};
57 
58 	this(Widget parent, string value = "Untitled")
59 	{
60 		super(parent);
61 		mEditable  = false;
62 		mSpinnable = false;
63 		mCommitted = true;
64 		mValue     = value;
65 		mDefaultValue = "";
66 		mAlignment = Alignment.Center;
67 		mUnits = "";
68 		mFormat = "";
69 		mUnitsImage = NVGImage();
70 		mValidFormat = true;
71 		mValueTemp = value;
72 		mCursorPos = -1;
73 		mSelectionPos = -1;
74 		mMousePos = Vector2i(-1,-1);
75 		mMouseDownPos = Vector2i(-1,-1);
76 		mMouseDragPos = Vector2i(-1,-1);
77 		mMouseDownModifier = 0;
78 		mTextOffset = 0;
79 		mLastClick = 0;
80 		if (mTheme) mFontSize = mTheme.mTextBoxFontSize;
81 		mIconExtraScale = 0.8f;// widget override
82 	}
83 
84 	bool editable() const { return mEditable; }
85 	final void editable(bool editable)
86 	{
87 		mEditable = editable;
88 		cursor = editable ? Cursor.IBeam : Cursor.Arrow;
89 	}
90 
91 	final bool spinnable() const { return mSpinnable; }
92 	final void spinnable(bool spinnable) { mSpinnable = spinnable; }
93 
94 	final string value() const { return mValue; }
95 	final void value(string value) { mValue = value; }
96 
97 	final string defaultValue() const { return mDefaultValue; }
98 	final void defaultValue(string defaultValue) { mDefaultValue = defaultValue; }
99 
100 	final Alignment alignment() const { return mAlignment; }
101 	final void alignment(Alignment al) { mAlignment = al; }
102 
103 	final string units() const { return mUnits; }
104 	final void units(string units) { mUnits = units; }
105 
106 	final auto unitsImage() const { return mUnitsImage; }
107 	final void unitsImage(NVGImage image) { mUnitsImage = image; }
108 
109 	/// Return the underlying regular expression specifying valid formats
110 	final string format() const { return mFormat; }
111 	/// Specify a regular expression specifying valid formats
112 	final void format(string format) { mFormat = format; }
113 
114 	/// Return the placeholder text to be displayed while the text box is empty.
115 	final string placeholder() const { return mPlaceholder; }
116 	/// Specify a placeholder text to be displayed while the text box is empty.
117 	final void placeholder(string placeholder) { mPlaceholder = placeholder; }
118 
119 	/// Set the `Theme` used to draw this widget
120 	override void theme(Theme theme)
121 	{
122 		Widget.theme(theme);
123 		if (mTheme)
124 			mFontSize = mTheme.mTextBoxFontSize;
125 	}
126 
127 	/// The callback to execute when the value of this TextBox has changed.
128 	final bool delegate(string str) callback() const { return mCallback; }
129 
130 	/// Sets the callback to execute when the value of this TextBox has changed.
131 	final void callback(bool delegate(string str) callback) { mCallback = callback; }
132 
133 	override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers)
134     {
135         if (button == MouseButton.Left && down && !mFocused)
136         {
137             if (!mSpinnable || spinArea(p) == SpinArea.None) /* not on scrolling arrows */
138             {
139                 requestFocus();
140                 screen.resetBlinkingCursor;
141             }
142         }
143 
144         if (mEditable && focused)
145         {
146             if (down)
147             {
148                 mMouseDownPos = p;
149                 mMouseDownModifier = modifiers;
150 
151                 version(__unported__)
152                 {
153                     // double time = glfwGetTime();
154                     // if (time - mLastClick < 0.25) {
155                     //     /* Double-click: select all text */
156                     //     mSelectionPos = 0;
157                     //     mCursorPos = (int) mValueTemp.size();
158                     //     mMouseDownPos = Vector2i(-1, -1);
159                     // }
160                     // mLastClick = time;
161                 }
162             }
163             else
164             {
165                 mMouseDownPos = Vector2i(-1, -1);
166                 mMouseDragPos = Vector2i(-1, -1);
167             }
168             return true;
169         }
170         else if (mSpinnable && !focused)
171         {
172             if (down)
173             {
174                 if (spinArea(p) == SpinArea.None)
175                 {
176                     mMouseDownPos = p;
177                     mMouseDownModifier = modifiers;
178 
179                     version(__unported__)
180                     {
181                         // double time = glfwGetTime();
182                         // if (time - mLastClick < 0.25) {
183                         //     /* Double-click: reset to default value */
184                         //     mValue = mDefaultValue;
185                         //     if (mCallback)
186                         //         mCallback(mValue);
187 
188                         //     mMouseDownPos = Vector2i(-1, -1);
189                         // }
190                         // mLastClick = time;
191                     }
192                 }
193                 else
194                 {
195                     mMouseDownPos = Vector2i(-1, -1);
196                     mMouseDragPos = Vector2i(-1, -1);
197                 }
198             }
199             else
200             {
201                 mMouseDownPos = Vector2i(-1, -1);
202                 mMouseDragPos = Vector2i(-1, -1);
203             }
204             return true;
205         }
206 
207         return false;
208     }
209 
210 	override bool mouseMotionEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers)
211     {
212         mMousePos = p;
213 
214         if (!mEditable)
215             cursor = Cursor.Arrow;
216         else if (mSpinnable && !focused && spinArea(mMousePos) != SpinArea.None) /* scrolling arrows */
217             cursor = Cursor.Hand;
218         else
219             cursor = Cursor.IBeam;
220 
221         if (mEditable && focused)
222         {
223             return true;
224         }
225         return false;
226     }
227 
228 	override bool mouseDragEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers)
229     {
230         mMousePos = p;
231         mMouseDragPos = p;
232 
233         if (mEditable && focused)
234             return true;
235 
236         return false;
237     }
238 
239 	override bool focusEvent(bool focused)
240     {
241         super.focusEvent(focused);
242 
243         string backup = mValue;
244 
245         if (mEditable)
246         {
247             if (focused)
248             {
249                 mValueTemp = mValue;
250                 mCommitted = false;
251                 mCursorPos = 0;
252             }
253             else
254             {
255                 if (mValidFormat)
256                 {
257                     if (mValueTemp == "")
258                         mValue = mDefaultValue;
259                     else
260                         mValue = mValueTemp;
261                 }
262 
263                 if (mCallback && !mCallback(mValue))
264                     mValue = backup;
265 
266                 mValidFormat = true;
267                 mCommitted = true;
268                 mCursorPos = -1;
269                 mSelectionPos = -1;
270                 mTextOffset = 0;
271             }
272 
273             mValidFormat = (mValueTemp == "") || checkFormat(mValueTemp, mFormat);
274         }
275 
276         return true;
277     }
278 
279 	override bool keyboardEvent(int key, int scancode, KeyAction action, int modifiers)
280     {
281         if (mEditable && focused)
282         {
283             if (action == KeyAction.Press || action == KeyAction.Repeat)
284             {
285                 import std.uni : byGrapheme;
286                 import std.range : walkLength;
287 
288                 if (key == Key.Left)
289                 {
290                     if (modifiers == KeyMod.Shift)
291                     {
292                         if (mSelectionPos == -1)
293                             mSelectionPos = mCursorPos;
294                     }
295                     else
296                         mSelectionPos = -1;
297 
298                     if (mCursorPos > 0)
299                     {
300                         mCursorPos--;
301                         screen.resetBlinkingCursor;
302                     }
303                 }
304                 else if (key == Key.Right)
305                 {
306                     if (modifiers == KeyMod.Shift)
307                     {
308                         if (mSelectionPos == -1)
309                             mSelectionPos = mCursorPos;
310                     }
311                     else
312                         mSelectionPos = -1;
313 
314                     if (mCursorPos < cast(int) mValueTemp.byGrapheme.walkLength)
315                     {
316                         mCursorPos++;
317                         screen.resetBlinkingCursor;
318                     }
319                 }
320                 else if (key == Key.Home)
321                 {
322                     if (modifiers == KeyMod.Shift)
323                     {
324                         if (mSelectionPos == -1)
325                             mSelectionPos = mCursorPos;
326                     }
327                     else
328                         mSelectionPos = -1;
329 
330                     if (mCursorPos)
331                     {
332                         mCursorPos = 0;
333                         screen.resetBlinkingCursor;
334                     }
335                 }
336                 else if (key == Key.End)
337                 {
338                     if (modifiers == KeyMod.Shift)
339                     {
340                         if (mSelectionPos == -1)
341                             mSelectionPos = mCursorPos;
342                     }
343                     else
344                         mSelectionPos = -1;
345 
346                     const newCursorPos = cast(int) mValueTemp.byGrapheme.walkLength;
347                     if (mCursorPos != newCursorPos)
348                     {
349                         mCursorPos = newCursorPos;
350                         screen.resetBlinkingCursor;
351                     }
352                 }
353                 else if (key == Key.Backspace)
354                 {
355                     if (!deleteSelection())
356                     {
357                         if (mCursorPos > 0)
358                         {
359                             auto begin = symbolLengthToBytes(mValueTemp, mCursorPos - 1);
360                             auto end   = symbolLengthToBytes(mValueTemp, mCursorPos);
361                             mValueTemp.replaceInPlace(begin, end, (char[]).init);
362                             mCursorPos--;
363                             screen.resetBlinkingCursor;
364                         }
365                     }
366                 }
367                 else if (key == Key.Delete)
368                 {
369                     if (!deleteSelection())
370                     {
371                         if (mCursorPos < cast(int) mValueTemp.byGrapheme.walkLength)
372                         {
373                             auto begin = symbolLengthToBytes(mValueTemp, mCursorPos);
374                             auto end   = symbolLengthToBytes(mValueTemp, mCursorPos+1);
375                             mValueTemp.replaceInPlace(begin, end, (char[]).init);
376                             screen.resetBlinkingCursor;
377                         }
378                     }
379                 }
380                 else if (key == Key.Enter)
381                 {
382                     if (!mCommitted)
383                         focusEvent(false);
384                 }
385                 else if (key == Key.Esc)
386                 {
387                     super.focusEvent(focused);
388                     mCommitted = true;
389                     mFocused = false;
390                 }
391                 else if (key == Key.A && modifiers == KeyMod.Ctrl)
392                 {
393                     mCursorPos = cast(int) mValueTemp.byGrapheme.walkLength;
394                     mSelectionPos = 0;
395                     screen.resetBlinkingCursor;
396                 }
397                 else if (key == Key.X && modifiers == KeyMod.Ctrl)
398                 {
399                     copySelection();
400                     deleteSelection();
401                     screen.resetBlinkingCursor;
402                 }
403                 else if (key == Key.C && modifiers == KeyMod.Ctrl)
404                 {
405                     copySelection();
406                     screen.resetBlinkingCursor;
407                 }
408                 else if (key == Key.V && modifiers == KeyMod.Ctrl)
409                 {
410                     deleteSelection();
411                     pasteFromClipboard();
412                     screen.resetBlinkingCursor;
413                 }
414 
415                 mValidFormat =
416                     (mValueTemp == "") || checkFormat(mValueTemp, mFormat);
417             }
418 
419             return true;
420         }
421 
422         return false;
423     }
424 
425 	/// converts length in symbols to length in bytes
426     private auto symbolLengthToBytes(string txt, size_t count)
427     {
428         import std.uni : graphemeStride;
429         size_t pos; // current position in string
430         size_t total_len; // length of pos symbols in bytes
431         while(total_len != txt.length && pos < count)
432         {
433             assert(total_len <= txt.length);
434             // get length of the current graphem in bytes
435             auto len = graphemeStride(txt, total_len);
436             total_len += len;
437             pos++;
438         }
439         return total_len;
440     }
441 
442 	override bool keyboardCharacterEvent(dchar codepoint)
443     {
444         if (mEditable && focused)
445         {
446             deleteSelection();
447             import std.conv : to;
448             auto replacement = codepoint.to!string;
449             auto pos = symbolLengthToBytes(mValueTemp, mCursorPos);
450             mValueTemp = mValueTemp[0..pos] ~ 
451                          replacement ~ 
452                          mValueTemp[pos..$];
453             mCursorPos++;
454 
455             mValidFormat = (mValueTemp == "") || checkFormat(mValueTemp, mFormat);
456 
457             return true;
458         }
459 
460         return false;
461     }
462 
463 	override Vector2i preferredSize(NanoContext ctx) const
464 	{
465 		Vector2i size = Vector2i(0, cast(int) (fontSize * 1.4f));
466 
467 		float uw = 0;
468 		if (mUnitsImage.valid) 
469 		{
470 			int w, h;
471 			ctx.imageSize(mUnitsImage, w, h);
472 			float uh = size[1] * 0.4f;
473 			uw = w * uh / h;
474 		} else if (mUnits.length)
475 		{
476 			uw = ctx.textBounds(0, 0, mUnits, null);
477 		}
478 		float sw = 0;
479 		if (mSpinnable)
480 			sw = 14.0f;
481 
482 		float ts = ctx.textBounds(0, 0, mValue, null);
483 		size[0] = size[1] + cast(int)(ts + uw + sw);
484 		return size;
485 	}
486 
487 	override void draw(ref NanoContext ctx)
488     {
489         super.draw(ctx);
490 
491         NVGPaint bg = ctx.boxGradient(
492             mPos.x + 1, mPos.y + 1 + 1.0f, mSize.x - 2, mSize.y - 2,
493             3, 4, Color(255, 255, 255, 32), Color(32, 32, 32, 32));
494         NVGPaint fg1 = ctx.boxGradient(
495             mPos.x + 1, mPos.y + 1 + 1.0f, mSize.x - 2, mSize.y - 2,
496             3, 4, Color(150, 150, 150, 32), Color(32, 32, 32, 32));
497         NVGPaint fg2 = ctx.boxGradient(
498             mPos.x + 1, mPos.y + 1 + 1.0f, mSize.x - 2, mSize.y - 2,
499             3, 4, Color(255, 0, 0, 100), Color(255, 0, 0, 50));
500 
501         ctx.beginPath;
502         ctx.roundedRect(mPos.x + 1, mPos.y + 1 + 1.0f, mSize.x - 2,
503                     mSize.y - 2, 3);
504 
505         if (mEditable && focused)
506             mValidFormat ? ctx.fillPaint(fg1) : ctx.fillPaint(fg2);
507         else if (mSpinnable && mMouseDownPos.x != -1)
508             ctx.fillPaint(fg1);
509         else
510             ctx.fillPaint(bg);
511 
512         ctx.fill;
513 
514         ctx.beginPath;
515         ctx.roundedRect(mPos.x + 0.5f, mPos.y + 0.5f, mSize.x - 1,
516                     mSize.y - 1, 2.5f);
517         ctx.strokeColor(NVGColor(0, 0, 0, 48));
518         ctx.stroke;
519 
520         ctx.fontSize(fontSize());
521         if (mTheme !is null)
522             ctx.fontFaceId(mTheme.mFontNormal);
523         else
524             ctx.fontFace("sans");
525 
526         auto draw_pos = Vector2i(mPos.x, cast(int) (mPos.y + mSize.y * 0.5f + 1));
527 
528         float xSpacing = mSize.y * 0.3f;
529 
530         float unitWidth = 0;
531 
532         if (mUnitsImage.valid)
533         {
534             int w, h;
535             ctx.imageSize(mUnitsImage, w, h);
536             float unitHeight = mSize.y * 0.4f;
537             unitWidth = w * unitHeight / h;
538             NVGPaint imgPaint = ctx.imagePattern(
539                 mPos.x + mSize.x - xSpacing - unitWidth,
540                 draw_pos.y - unitHeight * 0.5f, unitWidth, unitHeight, 0,
541                 mUnitsImage, mEnabled ? 0.7f : 0.35f);
542             ctx.beginPath;
543             ctx.rect(mPos.x + mSize.x - xSpacing - unitWidth,
544                     draw_pos.y - unitHeight * 0.5f, unitWidth, unitHeight);
545             ctx.fillPaint(imgPaint);
546             ctx.fill;
547             unitWidth += 2;
548         }
549         else if (mUnits.length)
550         {
551             unitWidth = ctx.textBounds(0, 0, mUnits, null);
552             ctx.fillColor(Color(255, 255, 255, mEnabled ? 64 : 32));
553             NVGTextAlign algn;
554 			algn.right = true;
555 			algn.middle = true;
556 			ctx.textAlign(algn);
557             ctx.text(mPos.x + mSize.x - xSpacing, draw_pos.y,
558                     mUnits);
559             unitWidth += 2;
560         }
561 
562         float spinArrowsWidth = 0.0f;
563 
564         if (mSpinnable && !focused()) {
565             spinArrowsWidth = 14.0f;
566 
567             ctx.fontFace("icons");
568             ctx.fontSize(((mFontSize < 0) ? mTheme.mButtonFontSize : mFontSize) * icon_scale());
569 
570             bool spinning = mMouseDownPos.x != -1;
571 
572             /* up button */ {
573                 bool hover = mMouseFocus && spinArea(mMousePos) == SpinArea.Top;
574                 ctx.fillColor((mEnabled && (hover || spinning)) ? mTheme.mTextColor : mTheme.mDisabledTextColor);
575                 auto icon = mTheme.mTextBoxUpIcon;
576                 NVGTextAlign algn;
577                 algn.left = true;
578                 algn.middle = true;
579                 ctx.textAlign(algn);
580                 auto iconPos = Vector2f(mPos.x + 4.0f,
581                                 mPos.y + mSize.y/2.0f - xSpacing/2.0f);
582                 ctx.text(iconPos.x, iconPos.y, [icon]);
583             }
584 
585             /* down button */ {
586                 bool hover = mMouseFocus && spinArea(mMousePos) == SpinArea.Bottom;
587                 ctx.fillColor((mEnabled && (hover || spinning)) ? mTheme.mTextColor : mTheme.mDisabledTextColor);
588                 auto icon = mTheme.mTextBoxDownIcon;
589                 NVGTextAlign algn;
590                 algn.left = true;
591                 algn.middle = true;
592                 ctx.textAlign(algn);
593                 auto iconPos = Vector2f(mPos.x + 4.0f,
594                                 mPos.y + mSize.y/2.0f + xSpacing/2.0f + 1.5f);
595                 ctx.text(iconPos.x, iconPos.y, [icon]);
596             }
597 
598             ctx.fontSize(fontSize());
599             ctx.fontFace("sans");
600         }
601 
602         final switch (mAlignment) {
603             case Alignment.Left:
604                 NVGTextAlign algn;
605                 algn.left = true;
606                 algn.middle = true;
607                 ctx.textAlign(algn);
608                 draw_pos.x += cast(int)(xSpacing + spinArrowsWidth);
609                 break;
610             case Alignment.Right:
611                 NVGTextAlign algn;
612                 algn.right = true;
613                 algn.middle = true;
614                 ctx.textAlign(algn);
615                 draw_pos.x += cast(int)(mSize.x - unitWidth - xSpacing);
616                 break;
617             case Alignment.Center:
618                 NVGTextAlign algn;
619                 algn.center = true;
620                 algn.middle = true;
621                 ctx.textAlign(algn);
622                 draw_pos.x += cast(int)(mSize.x * 0.5f);
623                 break;
624         }
625 
626         ctx.fontSize(fontSize());
627         ctx.fillColor(mEnabled && (!mCommitted || mValue.length) ?
628             mTheme.mTextColor :
629             mTheme.mDisabledTextColor);
630 
631         // clip visible text area
632         float clipX = mPos.x + xSpacing + spinArrowsWidth - 1.0f;
633         float clipY = mPos.y + 1.0f;
634         float clipWidth = mSize.x - unitWidth - spinArrowsWidth - 2 * xSpacing + 2.0f;
635         float clipHeight = mSize.y - 3.0f;
636 
637         ctx.save;
638         ctx.intersectScissor(clipX, clipY, clipWidth, clipHeight);
639 
640         auto old_draw_pos = Vector2i(draw_pos);
641         draw_pos.x += cast(int) mTextOffset;
642 
643         if (mCommitted) {
644             ctx.text(draw_pos.x, draw_pos.y,
645                 !mValue.length ? mPlaceholder : mValue);
646         } else {
647             const int maxGlyphs = 1024;
648             NVGGlyphPosition[maxGlyphs] glyphs_buffer;
649             float[4] textBound;
650             ctx.textBounds(draw_pos.x, draw_pos.y, mValueTemp,
651                         textBound);
652             float lineh = textBound[3] - textBound[1];
653 
654             // find cursor positions
655             auto glyphs =
656                 ctx.textGlyphPositions(draw_pos.x, draw_pos.y,
657                                     mValueTemp, glyphs_buffer);
658             glyphs = squeezeGlyphs(glyphs_buffer[], glyphs);
659             updateCursor(ctx, textBound[2], glyphs);
660 
661             // compute text offset
662             int prevCPos = mCursorPos > 0 ? mCursorPos - 1 : 0;
663             int len = cast(int) glyphs.length;
664             int nextCPos = mCursorPos < len ? mCursorPos + 1 : len;
665             float prevCX = cursorIndex2Position(prevCPos, textBound[2], glyphs);
666             float nextCX = cursorIndex2Position(nextCPos, textBound[2], glyphs);
667 
668             if (nextCX > clipX + clipWidth)
669                 mTextOffset -= nextCX - (clipX + clipWidth) + 1;
670             if (prevCX < clipX)
671                 mTextOffset += clipX - prevCX + 1;
672 
673             draw_pos.x = cast(int) (old_draw_pos.x + mTextOffset);
674 
675             // draw text with offset
676             ctx.text(draw_pos.x, draw_pos.y, mValueTemp);
677             ctx.textBounds(draw_pos.x, draw_pos.y, mValueTemp, textBound);
678 
679             // recompute cursor positions
680             glyphs = ctx.textGlyphPositions(draw_pos.x, draw_pos.y,
681                     mValueTemp, glyphs_buffer);
682             glyphs = squeezeGlyphs(glyphs_buffer[], glyphs);
683 
684             if (mCursorPos > -1) {
685                 if (mSelectionPos > -1) {
686                     float caretx = cursorIndex2Position(mCursorPos, textBound[2],
687                                                         glyphs);
688                     float selx = cursorIndex2Position(mSelectionPos, textBound[2],
689                                                     glyphs);
690 
691                     if (caretx > selx)
692                     {
693                         swap(caretx, selx);
694                     }
695 
696                     // draw selection
697                     ctx.beginPath;
698                     ctx.fillColor(Color(255, 255, 255, 80));
699                     ctx.rect(caretx, draw_pos.y - lineh * 0.5f, selx - caretx,
700                             lineh);
701                     ctx.fill;
702                 }
703 
704                 float caretx = cursorIndex2Position(mCursorPos, textBound[2], glyphs);
705 
706                 // draw cursor
707                 if (screen.blinkingCursorIsVisibleNow)
708                 {
709                     ctx.beginPath;
710                     ctx.moveTo(caretx, draw_pos.y - lineh * 0.5f);
711                     ctx.lineTo(caretx, draw_pos.y + lineh * 0.5f);
712                     ctx.strokeColor(nvgRGBA(255, 192, 0, 255));
713                     ctx.strokeWidth(1.0f);
714                     ctx.stroke;
715                 }
716             }
717         }
718         ctx.restore;
719     }
720 
721 // override void save(Serializer &s) const;
722 // override bool load(Serializer &s);
723 protected:
724 
725     // hide method
726     override void cursor(Cursor value)
727     {
728         super.cursor(value);
729     }
730 
731 	bool checkFormat(string input, string format)
732     {
733         if (!format.length)
734             return true;
735         // try
736         // {
737             import std.regex : regex, matchAll;
738             import std.range : walkLength;
739             auto r = regex(format);
740             auto ma = input.matchAll(r);
741             return ma.walkLength == 1;
742         // }
743         // catch (RegexException)
744         // {
745         //     throw;
746         // }
747     }
748 
749     bool copySelection()
750     {
751         import std.string : toStringz;
752         import gfm.sdl2 : SDL_SetClipboardText;
753         import nanogui.screen : Screen;
754 
755         if (mSelectionPos > -1)
756         {
757             Screen sc = cast(Screen) (window.parent);
758             if (!sc)
759                 return false;
760 
761             int begin = mCursorPos;
762             int end = mSelectionPos;
763 
764             if (begin > end)
765                 swap(begin, end);
766 
767             SDL_SetClipboardText(mValueTemp[begin..end].toStringz);
768 
769             return true;
770         }
771 
772         return false;
773     }
774 
775 	void pasteFromClipboard()
776     {
777         import std.conv : text;
778         import std.string : fromStringz;
779         import gfm.sdl2 : SDL_HasClipboardText, SDL_GetClipboardText;
780         import nanogui.screen : Screen;
781 
782         Screen sc = cast(Screen) (window.parent);
783         if (!sc)
784             return;
785         if (SDL_HasClipboardText())
786         {
787             const ptr = SDL_GetClipboardText();
788             if (ptr)
789             {
790                 auto str = ptr.fromStringz;
791                 mValueTemp = text(mValueTemp[0..mCursorPos], str, mValueTemp[mCursorPos..$]);
792                 mCursorPos += str.length;
793             }
794         }
795     }
796 
797 	bool deleteSelection()
798     {
799         if (mSelectionPos > -1) {
800             size_t begin = symbolLengthToBytes(mValueTemp, mCursorPos);
801             size_t end = symbolLengthToBytes(mValueTemp, mSelectionPos);
802 
803             if (begin > end)
804                 swap(begin, end);
805 
806             if (begin == end - 1)
807                 mValueTemp.replaceInPlace(begin, begin+1, (char[]).init);
808             else
809                 mValueTemp.replaceInPlace(begin, end, (char[]).init);
810 
811             import std.utf : count;
812             mCursorPos = cast(int) mValueTemp[0..begin].count;
813             mSelectionPos = -1;
814             return true;
815         }
816 
817         return false;
818     }
819 
820 	void updateCursor(NanoContext ctx, float lastx,
821 					  const(NVGGlyphPosition)[] glyphs)
822     {
823         // handle mouse cursor events
824         if (mMouseDownPos.x != -1) {
825             if (mMouseDownModifier == KeyMod.Shift)
826             {
827                 if (mSelectionPos == -1)
828                     mSelectionPos = mCursorPos;
829             } else
830                 mSelectionPos = -1;
831 
832             mCursorPos =
833                 position2CursorIndex(mMouseDownPos.x, lastx, glyphs);
834 
835             mMouseDownPos = Vector2i(-1, -1);
836         } else if (mMouseDragPos.x != -1) {
837             if (mSelectionPos == -1)
838                 mSelectionPos = mCursorPos;
839 
840             mCursorPos =
841                 position2CursorIndex(mMouseDragPos.x, lastx, glyphs);
842         } else {
843             // set cursor to last character
844             if (mCursorPos == -2)
845                 mCursorPos = cast(int) glyphs.length;
846         }
847 
848         if (mCursorPos == mSelectionPos)
849             mSelectionPos = -1;
850     }
851 	float cursorIndex2Position(int index, float lastx,
852 							   const(NVGGlyphPosition)[] glyphs)
853     {
854         float pos = 0;
855         if (index == glyphs.length)
856             pos = lastx; // last character
857         else
858             pos = glyphs[index].x;
859 
860         return pos;
861     }
862 
863 	int position2CursorIndex(float posx, float lastx,
864 							 const(NVGGlyphPosition)[] glyphs)
865     {
866         int mCursorId = 0;
867         float caretx = glyphs[mCursorId].x;
868         for (int j = 1; j < glyphs.length; j++) {
869             if (abs(caretx - posx) > abs(glyphs[j].x - posx)) {
870                 mCursorId = j;
871                 caretx = glyphs[mCursorId].x;
872             }
873         }
874         if (abs(caretx - posx) > abs(lastx - posx))
875             mCursorId = cast(int) glyphs.length;
876 
877         return mCursorId;
878     }
879 
880 	/// The location (if any) for the spin area.
881 	enum SpinArea { None, Top, Bottom }
882 	SpinArea spinArea(Vector2i pos)
883     {
884         if (0 <= pos.x - mPos.x && pos.x - mPos.x < 14.0f) { /* on scrolling arrows */
885             if (mSize.y >= pos.y - mPos.y && pos.y - mPos.y <= mSize.y / 2.0f) { /* top part */
886                 return SpinArea.Top;
887             } else if (0.0f <= pos.y - mPos.y && pos.y - mPos.y > mSize.y / 2.0f) { /* bottom part */
888                 return SpinArea.Bottom;
889             }
890         }
891         return SpinArea.None;
892     }
893 
894 	bool mEditable;
895 	bool mSpinnable;
896 	bool mCommitted;
897 	string mValue;
898 	string mDefaultValue;
899 	Alignment mAlignment;
900 	string mUnits;
901 	string mFormat;
902 	NVGImage mUnitsImage;
903 	bool delegate(string str) mCallback;
904 	bool mValidFormat;
905 	string mValueTemp;
906 	string mPlaceholder;
907 	int mCursorPos;
908 	int mSelectionPos;
909 	Vector2i mMousePos;
910 	Vector2i mMouseDownPos;
911 	Vector2i mMouseDragPos;
912 	int mMouseDownModifier;
913 	float mTextOffset;
914 	double mLastClick;
915 }
916 
917 /**
918  * \class IntBox textbox.h nanogui/textbox.h
919  *
920  * \brief A specialization of TextBox for representing integral values.
921  *
922  * Template parameters should be integral types, e.g. `int`, `long`,
923  * `uint32_t`, etc.
924  */
925 class IntBox(Scalar) : TextBox if (isIntegral!Scalar) {
926 public:
927 	this(Widget parent, Scalar v = cast(Scalar) 0)
928 	{
929 		super(parent);
930 		defaultValue("0");
931 		format(isSigned!Scalar ? "[-]?[0-9]*" : "[0-9]*");
932 		valueIncrement(cast(Scalar) 1);
933 		minMaxValues(Scalar.min, Scalar.max);
934 		value(v);
935 		spinnable(false);
936 	}
937 
938 	final Scalar value() const
939 	{
940 		import std.conv : to;
941 		return TextBox.value.to!Scalar;
942 	}
943 
944 	final void value(Scalar v)
945 	{
946 		import std.algorithm : min, max;
947 		import std.conv : to;
948 		Scalar clampedValue = min(max(v, mMinValue), mMaxValue);
949 		TextBox.value = to!string(clampedValue);
950 	}
951 
952 	final void callback(void delegate(Scalar) cb)
953 	{
954 		TextBox.callback((string str)
955 		{
956 			import std.conv : to, ConvOverflowException;
957 			Scalar scalar;
958 			try {
959 				scalar = str.to!Scalar;
960 			} catch (ConvOverflowException ex) {
961 				// TODO
962 			}
963 			value(scalar);
964 			cb(scalar);
965 			return true;
966 		});
967 	}
968 
969 	final void valueIncrement(Scalar value)
970 	{
971 		mValueIncrement = value;
972 	}
973 
974 	final void minValue(Scalar value)
975 	{
976 		mMinValue = value;
977 	}
978 
979 	final void maxValue(Scalar value)
980 	{
981 		mMaxValue = value;
982 	}
983 
984 	final void minMaxValues(Scalar min_value, Scalar max_value)
985 	{
986 		minValue(min_value);
987 		maxValue(max_value);
988 	}
989 
990 	override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers)
991 	{
992 		if ((mEditable || mSpinnable) && down)
993 			mMouseDownValue = value();
994 
995 		SpinArea area = spinArea(p);
996 		if (mSpinnable && area != SpinArea.None && down && !focused()) {
997 			if (area == SpinArea.Top) {
998 				value(cast(Scalar) (value() + mValueIncrement));
999 				if (mCallback)
1000 					mCallback(mValue);
1001 			} else if (area == SpinArea.Bottom) {
1002 				value(cast(Scalar) (value() - mValueIncrement));
1003 				if (mCallback)
1004 					mCallback(mValue);
1005 			}
1006 			return true;
1007 		}
1008 
1009 		return TextBox.mouseButtonEvent(p, button, down, modifiers);
1010 	}
1011 
1012 	override bool mouseDragEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers)
1013 	{
1014 		if (TextBox.mouseDragEvent(p, rel, button, modifiers)) {
1015 			return true;
1016 		}
1017 		if (mSpinnable && !focused() && button == 2 /* 1 << GLFW_MOUSE_BUTTON_2 */ && mMouseDownPos.x != -1) {
1018 			int valueDelta = cast(int) ((p.x - mMouseDownPos.x) / float(10));
1019 			value(cast (Scalar)(mMouseDownValue + valueDelta * mValueIncrement));
1020 			if (mCallback)
1021 				mCallback(mValue);
1022 			return true;
1023 		}
1024 		return false;
1025 	}
1026 
1027 	override bool scrollEvent(Vector2i p, Vector2f rel)
1028 	{
1029 		if (Widget.scrollEvent(p, rel)) {
1030 			return true;
1031 		}
1032 		if (mSpinnable && !focused()) {
1033 			const valueDelta = (rel.y > 0) ? 1 : -1;
1034 			value(cast(Scalar) (value() + valueDelta*mValueIncrement));
1035 			if (mCallback)
1036 				mCallback(mValue);
1037 			return true;
1038 		}
1039 		return false;
1040 	}
1041 
1042 private:
1043 	Scalar mMouseDownValue;
1044 	Scalar mValueIncrement;
1045 	Scalar mMinValue, mMaxValue;
1046 }
1047 
1048 /**
1049  * \class FloatBox textbox.d nanogui/textbox.d
1050  *
1051  * \brief A specialization of TextBox representing floating point values.
1052 
1053  * Template parameters should be float types, e.g. `float`, `double`,
1054  * `float64_t`, etc.
1055  */
1056 class FloatBox(Scalar) : TextBox if (isFloatingPoint!Scalar) {
1057 public:
1058 	this(Widget parent, Scalar v = cast(Scalar) 0.0f)
1059 	{
1060 		super(parent);
1061 		mNumberFormat = Scalar.sizeof == float.sizeof ? "%.4g" : "%.7g";
1062 		defaultValue("0");
1063 		format("[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?");valueIncrement(cast(Scalar) 0.1);
1064 		minMaxValues(Scalar.min_exp, Scalar.max);
1065 		value(v);
1066 		spinnable(false);
1067 	}
1068 
1069 	final string numberFormat() const { return mNumberFormat; }
1070 	final void numberFormat(string format) { mNumberFormat = format; }
1071 
1072 	final Scalar value() const {
1073 		import std.conv : to;
1074 		return TextBox.value.to!Scalar;
1075 	}
1076 
1077 	final void value(Scalar v) {
1078 		import std.algorithm : min, max;
1079 		import std.string : fromStringz;
1080 		import core.stdc.stdio : snprintf;
1081 
1082 		Scalar clampedValue = min(max(v, mMinValue), mMaxValue);
1083 		char[50] buffer;
1084 		snprintf(buffer.ptr, 50, mNumberFormat.ptr, clampedValue);
1085 		TextBox.value(buffer.ptr.fromStringz.dup);
1086 	}
1087 
1088 	final void callback(void delegate(Scalar) cb) {
1089 		TextBox.callback((string str) {
1090 			import std.conv : to;
1091 			Scalar scalar = str.to!Scalar;
1092 			value(scalar);
1093 			cb(scalar);
1094 			return true;
1095 		});
1096 	}
1097 
1098 	final void valueIncrement(Scalar value) {
1099 		mValueIncrement = value;
1100 	}
1101 
1102 	final void minValue(Scalar value) {
1103 		mMinValue = value;
1104 	}
1105 
1106 	final void maxValue(Scalar value) {
1107 		mMaxValue = value;
1108 	}
1109 
1110 	final void minMaxValues(Scalar min_value, Scalar max_value) {
1111 		minValue(min_value);
1112 		maxValue(max_value);
1113 	}
1114 
1115 	override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers)
1116 	{
1117 		if ((mEditable || mSpinnable) && down)
1118 			mMouseDownValue = value();
1119 
1120 		SpinArea area = spinArea(p);
1121 		if (mSpinnable && area != SpinArea.None && down && !focused()) {
1122 			if (area == SpinArea.Top) {
1123 				value(value() + mValueIncrement);
1124 				if (mCallback)
1125 					mCallback(mValue);
1126 			} else if (area == SpinArea.Bottom) {
1127 				value(value() - mValueIncrement);
1128 				if (mCallback)
1129 					mCallback(mValue);
1130 			}
1131 			return true;
1132 		}
1133 
1134 		return TextBox.mouseButtonEvent(p, button, down, modifiers);
1135 	}
1136 
1137 	override bool mouseDragEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers)
1138 	{
1139 		if (TextBox.mouseDragEvent(p, rel, button, modifiers)) {
1140 			return true;
1141 		}
1142 		if (mSpinnable && !focused() && button == 2 /* 1 << GLFW_MOUSE_BUTTON_2 */ && mMouseDownPos.x != -1) {
1143 			int valueDelta = cast(int)((p.x - mMouseDownPos.x) / float(10));
1144 			value(mMouseDownValue + valueDelta * mValueIncrement);
1145 			if (mCallback)
1146 				mCallback(mValue);
1147 			return true;
1148 		}
1149 		return false;
1150 	}
1151 
1152 	override bool scrollEvent(Vector2i p, Vector2f rel)
1153 	{
1154 		if (Widget.scrollEvent(p, rel)) {
1155 			return true;
1156 		}
1157 		if (mSpinnable && !focused()) {
1158 			const valueDelta = (rel.y > 0) ? 1 : -1;
1159 			value(value() + valueDelta*mValueIncrement);
1160 			if (mCallback)
1161 				mCallback(mValue);
1162 			return true;
1163 		}
1164 		return false;
1165 	}
1166 
1167 private:
1168 	string mNumberFormat;
1169 	Scalar mMouseDownValue;
1170 	Scalar mValueIncrement;
1171 	Scalar mMinValue, mMaxValue;
1172 }