1//************************************ bs::framework - Copyright 2018 Marko Pintera **************************************//
2//*********** Licensed under the MIT license. See LICENSE.md for full terms. This notice is not to be removed. ***********//
3#include "GUI/BsGUIInputBox.h"
4#include "GUI/BsGUIManager.h"
5#include "2D/BsImageSprite.h"
6#include "GUI/BsGUISkin.h"
7#include "Image/BsSpriteTexture.h"
8#include "2D/BsTextSprite.h"
9#include "GUI/BsGUIDimensions.h"
10#include "GUI/BsGUITextInputEvent.h"
11#include "GUI/BsGUIMouseEvent.h"
12#include "GUI/BsGUICommandEvent.h"
13#include "GUI/BsGUIInputCaret.h"
14#include "GUI/BsGUIInputSelection.h"
15#include "GUI/BsGUIContextMenu.h"
16#include "GUI/BsGUIHelper.h"
17#include "Utility/BsTime.h"
18#include "Platform/BsPlatform.h"
19#include "String/BsUnicode.h"
20
21namespace bs
22{
23 VirtualButton GUIInputBox::mCopyVB = VirtualButton("Copy");
24 VirtualButton GUIInputBox::mPasteVB = VirtualButton("Paste");
25 VirtualButton GUIInputBox::mCutVB = VirtualButton("Cut");
26 VirtualButton GUIInputBox::mSelectAllVB = VirtualButton("SelectAll");
27
28 const String& GUIInputBox::getGUITypeName()
29 {
30 static String name = "InputBox";
31 return name;
32 }
33
34 GUIInputBox::GUIInputBox(const String& styleName, const GUIDimensions& dimensions, bool multiline)
35 : GUIElement(styleName, dimensions, GUIElementOption::AcceptsKeyFocus), mIsMultiline(multiline)
36 {
37 mImageSprite = bs_new<ImageSprite>();
38 mTextSprite = bs_new<TextSprite>();
39 }
40
41 GUIInputBox::~GUIInputBox()
42 {
43 bs_delete(mTextSprite);
44 bs_delete(mImageSprite);
45 }
46
47 GUIInputBox* GUIInputBox::create(bool multiline, const String& styleName)
48 {
49 return new (bs_alloc<GUIInputBox>()) GUIInputBox(getStyleName<GUIInputBox>(styleName), GUIDimensions::create(), multiline);
50 }
51
52 GUIInputBox* GUIInputBox::create(bool multiline, const GUIOptions& options, const String& styleName)
53 {
54 return new (bs_alloc<GUIInputBox>()) GUIInputBox(getStyleName<GUIInputBox>(styleName), GUIDimensions::create(options), multiline);
55 }
56
57 GUIInputBox* GUIInputBox::create(const GUIOptions& options, const String& styleName)
58 {
59 return new (bs_alloc<GUIInputBox>()) GUIInputBox(getStyleName<GUIInputBox>(styleName), GUIDimensions::create(options), false);
60 }
61
62 void GUIInputBox::setText(const String& text)
63 {
64 if (mText == text)
65 return;
66
67 bool filterOkay = true;
68 if(mFilter != nullptr)
69 {
70 filterOkay = mFilter(text);
71 }
72
73 if(filterOkay)
74 {
75 Vector2I origSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
76
77 mText = text;
78 mNumChars = UTF8::count(mText);
79
80 if (mHasFocus)
81 {
82 TEXT_SPRITE_DESC textDesc = getTextDesc();
83
84 gGUIManager().getInputCaretTool()->updateText(this, textDesc);
85 gGUIManager().getInputSelectionTool()->updateText(this, textDesc);
86
87 if (mNumChars > 0)
88 gGUIManager().getInputCaretTool()->moveCaretToChar(mNumChars - 1, CARET_AFTER);
89 else
90 gGUIManager().getInputCaretTool()->moveCaretToChar(0, CARET_BEFORE);
91
92 if (mSelectionShown)
93 gGUIManager().getInputSelectionTool()->selectAll();
94
95 scrollTextToCaret();
96 }
97
98 Vector2I newSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
99 if (origSize != newSize)
100 _markLayoutAsDirty();
101 else
102 _markContentAsDirty();
103 }
104 }
105
106 UINT32 GUIInputBox::_getNumRenderElements() const
107 {
108 UINT32 numElements = mImageSprite->getNumRenderElements();
109 numElements += mTextSprite->getNumRenderElements();
110
111 if(mCaretShown && gGUIManager().getCaretBlinkState())
112 numElements += gGUIManager().getInputCaretTool()->getSprite()->getNumRenderElements();
113
114 if(mSelectionShown)
115 {
116 const Vector<ImageSprite*>& sprites = gGUIManager().getInputSelectionTool()->getSprites();
117 for(auto& selectionSprite : sprites)
118 {
119 numElements += selectionSprite->getNumRenderElements();
120 }
121 }
122
123 return numElements;
124 }
125
126 const SpriteMaterialInfo& GUIInputBox::_getMaterial(UINT32 renderElementIdx, SpriteMaterial** material) const
127 {
128 UINT32 localRenderElementIdx;
129 Sprite* sprite = renderElemToSprite(renderElementIdx, localRenderElementIdx);
130
131 *material = sprite->getMaterial(localRenderElementIdx);
132 return sprite->getMaterialInfo(localRenderElementIdx);
133 }
134
135 void GUIInputBox::_getMeshInfo(UINT32 renderElementIdx, UINT32& numVertices, UINT32& numIndices, GUIMeshType& type) const
136 {
137 UINT32 localRenderElementIdx;
138 Sprite* sprite = renderElemToSprite(renderElementIdx, localRenderElementIdx);
139
140 UINT32 numQuads = sprite->getNumQuads(localRenderElementIdx);
141 numVertices = numQuads * 4;
142 numIndices = numQuads * 6;
143 type = GUIMeshType::Triangle;
144 }
145
146 void GUIInputBox::updateRenderElementsInternal()
147 {
148 mImageDesc.width = mLayoutData.area.width;
149 mImageDesc.height = mLayoutData.area.height;
150 mImageDesc.borderLeft = _getStyle()->border.left;
151 mImageDesc.borderRight = _getStyle()->border.right;
152 mImageDesc.borderTop = _getStyle()->border.top;
153 mImageDesc.borderBottom = _getStyle()->border.bottom;
154 mImageDesc.color = getTint();
155
156 const HSpriteTexture& activeTex = getActiveTexture();
157 if(SpriteTexture::checkIsLoaded(activeTex))
158 mImageDesc.texture = activeTex;
159
160 mImageSprite->update(mImageDesc, (UINT64)_getParentWidget());
161
162 TEXT_SPRITE_DESC textDesc = getTextDesc();
163 mTextSprite->update(textDesc, (UINT64)_getParentWidget());
164
165 if(mCaretShown && gGUIManager().getCaretBlinkState())
166 {
167 gGUIManager().getInputCaretTool()->updateText(this, textDesc); // TODO - These shouldn't be here. Only call this when one of these parameters changes.
168 gGUIManager().getInputCaretTool()->updateSprite();
169 }
170
171 if(mSelectionShown)
172 {
173 gGUIManager().getInputSelectionTool()->updateText(this, textDesc); // TODO - These shouldn't be here. Only call this when one of these parameters changes.
174 gGUIManager().getInputSelectionTool()->updateSprite();
175 }
176
177 // When text bounds are reduced the scroll needs to be adjusted so that
178 // input box isn't filled with mostly empty space.
179 Vector2I offset(mLayoutData.area.x, mLayoutData.area.y);
180 clampScrollToBounds(mTextSprite->getBounds(offset, Rect2I()));
181
182 GUIElement::updateRenderElementsInternal();
183 }
184
185 void GUIInputBox::updateClippedBounds()
186 {
187 Vector2I offset(mLayoutData.area.x, mLayoutData.area.y);
188 mClippedBounds = mImageSprite->getBounds(offset, mLayoutData.getLocalClipRect());
189 }
190
191 Sprite* GUIInputBox::renderElemToSprite(UINT32 renderElemIdx, UINT32& localRenderElemIdx) const
192 {
193 UINT32 oldNumElements = 0;
194 UINT32 newNumElements = oldNumElements + mTextSprite->getNumRenderElements();
195 if(renderElemIdx < newNumElements)
196 {
197 localRenderElemIdx = renderElemIdx - oldNumElements;
198 return mTextSprite;
199 }
200
201 oldNumElements = newNumElements;
202 newNumElements += mImageSprite->getNumRenderElements();
203
204 if(renderElemIdx < newNumElements)
205 {
206 localRenderElemIdx = renderElemIdx - oldNumElements;
207 return mImageSprite;
208 }
209
210 if(mCaretShown && gGUIManager().getCaretBlinkState())
211 {
212 oldNumElements = newNumElements;
213 newNumElements += gGUIManager().getInputCaretTool()->getSprite()->getNumRenderElements();
214
215 if(renderElemIdx < newNumElements)
216 {
217 localRenderElemIdx = renderElemIdx - oldNumElements;
218 return gGUIManager().getInputCaretTool()->getSprite();
219 }
220 }
221
222 if(mSelectionShown)
223 {
224 const Vector<ImageSprite*>& sprites = gGUIManager().getInputSelectionTool()->getSprites();
225 for(auto& selectionSprite : sprites)
226 {
227 oldNumElements = newNumElements;
228 newNumElements += selectionSprite->getNumRenderElements();
229
230 if(renderElemIdx < newNumElements)
231 {
232 localRenderElemIdx = renderElemIdx - oldNumElements;
233 return selectionSprite;
234 }
235 }
236 }
237
238 localRenderElemIdx = renderElemIdx;
239 return nullptr;
240 }
241
242 Vector2I GUIInputBox::renderElemToOffset(UINT32 renderElemIdx) const
243 {
244 UINT32 oldNumElements = 0;
245 UINT32 newNumElements = oldNumElements + mTextSprite->getNumRenderElements();
246 if(renderElemIdx < newNumElements)
247 return getTextOffset();
248
249 oldNumElements = newNumElements;
250 newNumElements += mImageSprite->getNumRenderElements();
251
252 if(renderElemIdx < newNumElements)
253 return Vector2I(mLayoutData.area.x, mLayoutData.area.y);;
254
255 if(mCaretShown && gGUIManager().getCaretBlinkState())
256 {
257 oldNumElements = newNumElements;
258 newNumElements += gGUIManager().getInputCaretTool()->getSprite()->getNumRenderElements();
259
260 if(renderElemIdx < newNumElements)
261 return gGUIManager().getInputCaretTool()->getSpriteOffset();
262 }
263
264 if(mSelectionShown)
265 {
266 UINT32 spriteIdx = 0;
267 const Vector<ImageSprite*>& sprites = gGUIManager().getInputSelectionTool()->getSprites();
268 for(auto& selectionSprite : sprites)
269 {
270 oldNumElements = newNumElements;
271 newNumElements += selectionSprite->getNumRenderElements();
272
273 if(renderElemIdx < newNumElements)
274 return gGUIManager().getInputSelectionTool()->getSelectionSpriteOffset(spriteIdx);
275
276 spriteIdx++;
277 }
278 }
279
280 return Vector2I();
281 }
282
283 Rect2I GUIInputBox::renderElemToClipRect(UINT32 renderElemIdx) const
284 {
285 UINT32 oldNumElements = 0;
286 UINT32 newNumElements = oldNumElements + mTextSprite->getNumRenderElements();
287 if(renderElemIdx < newNumElements)
288 return getTextClipRect();
289
290 oldNumElements = newNumElements;
291 newNumElements += mImageSprite->getNumRenderElements();
292
293 if(renderElemIdx < newNumElements)
294 return mLayoutData.getLocalClipRect();
295
296 if(mCaretShown && gGUIManager().getCaretBlinkState())
297 {
298 oldNumElements = newNumElements;
299 newNumElements += gGUIManager().getInputCaretTool()->getSprite()->getNumRenderElements();
300
301 if(renderElemIdx < newNumElements)
302 {
303 return gGUIManager().getInputCaretTool()->getSpriteClipRect(getTextClipRect());
304 }
305 }
306
307 if(mSelectionShown)
308 {
309 UINT32 spriteIdx = 0;
310 const Vector<ImageSprite*>& sprites = gGUIManager().getInputSelectionTool()->getSprites();
311 for(auto& selectionSprite : sprites)
312 {
313 oldNumElements = newNumElements;
314 newNumElements += selectionSprite->getNumRenderElements();
315
316 if(renderElemIdx < newNumElements)
317 return gGUIManager().getInputSelectionTool()->getSelectionSpriteClipRect(spriteIdx, getTextClipRect());
318
319 spriteIdx++;
320 }
321 }
322
323 return Rect2I();
324 }
325
326 Vector2I GUIInputBox::_getOptimalSize() const
327 {
328 UINT32 imageWidth = 0;
329 UINT32 imageHeight = 0;
330
331 const HSpriteTexture& activeTex = getActiveTexture();
332 if(SpriteTexture::checkIsLoaded(activeTex))
333 {
334 imageWidth = activeTex->getWidth();
335 imageHeight = activeTex->getHeight();
336 }
337
338 Vector2I contentSize = GUIHelper::calcOptimalContentsSize(mText, *_getStyle(), _getDimensions());
339 UINT32 contentWidth = std::max(imageWidth, (UINT32)contentSize.x);
340 UINT32 contentHeight = std::max(imageHeight, (UINT32)contentSize.y);
341
342 return Vector2I(contentWidth, contentHeight);
343 }
344
345 Vector2I GUIInputBox::_getTextInputOffset() const
346 {
347 return mTextOffset;
348 }
349
350 Rect2I GUIInputBox::_getTextInputRect() const
351 {
352 Rect2I textBounds = getCachedContentBounds();
353 textBounds.x -= mLayoutData.area.x;
354 textBounds.y -= mLayoutData.area.y;
355
356 return textBounds;
357 }
358
359 UINT32 GUIInputBox::_getRenderElementDepth(UINT32 renderElementIdx) const
360 {
361 UINT32 localRenderElementIdx;
362 Sprite* sprite = renderElemToSprite(renderElementIdx, localRenderElementIdx);
363
364 if(sprite == mImageSprite)
365 return _getDepth() + 3;
366 else if(sprite == mTextSprite)
367 return _getDepth() + 1;
368 else if(sprite == gGUIManager().getInputCaretTool()->getSprite())
369 return _getDepth();
370 else // Selection sprites
371 return _getDepth() + 2;
372 }
373
374 UINT32 GUIInputBox::_getRenderElementDepthRange() const
375 {
376 return 4;
377 }
378
379 bool GUIInputBox::_hasCustomCursor(const Vector2I position, CursorType& type) const
380 {
381 if(_isInBounds(position) && !_isDisabled())
382 {
383 type = CursorType::IBeam;
384 return true;
385 }
386
387 return false;
388 }
389
390 void GUIInputBox::_fillBuffer(UINT8* vertices, UINT32* indices, UINT32 vertexOffset, UINT32 indexOffset,
391 UINT32 maxNumVerts, UINT32 maxNumIndices, UINT32 renderElementIdx) const
392 {
393 UINT8* uvs = vertices + sizeof(Vector2);
394 UINT32 vertexStride = sizeof(Vector2) * 2;
395 UINT32 indexStride = sizeof(UINT32);
396
397 UINT32 localRenderElementIdx;
398 Sprite* sprite = renderElemToSprite(renderElementIdx, localRenderElementIdx);
399 Vector2I offset = renderElemToOffset(renderElementIdx);
400 Rect2I clipRect = renderElemToClipRect(renderElementIdx);
401
402 sprite->fillBuffer(vertices, uvs, indices, vertexOffset, indexOffset, maxNumVerts, maxNumIndices, vertexStride,
403 indexStride, localRenderElementIdx, offset, clipRect);
404 }
405
406 bool GUIInputBox::_mouseEvent(const GUIMouseEvent& ev)
407 {
408 if(ev.getType() == GUIMouseEventType::MouseOver)
409 {
410 if (!_isDisabled())
411 {
412 if (!mHasFocus)
413 {
414 Vector2I origSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
415 mState = State::Hover;
416 Vector2I newSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
417
418 if (origSize != newSize)
419 _markLayoutAsDirty();
420 else
421 _markContentAsDirty();
422 }
423
424 mIsMouseOver = true;
425 }
426
427 return true;
428 }
429 else if(ev.getType() == GUIMouseEventType::MouseOut)
430 {
431 if (!_isDisabled())
432 {
433 if (!mHasFocus)
434 {
435 Vector2I origSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
436 mState = State::Normal;
437 Vector2I newSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
438
439 if (origSize != newSize)
440 _markLayoutAsDirty();
441 else
442 _markContentAsDirty();
443 }
444
445 mIsMouseOver = false;
446 }
447
448 return true;
449 }
450 else if(ev.getType() == GUIMouseEventType::MouseDoubleClick && ev.getButton() == GUIMouseButton::Left)
451 {
452 if (!_isDisabled())
453 {
454 showSelection(0);
455 gGUIManager().getInputSelectionTool()->selectAll();
456
457 _markContentAsDirty();
458 }
459
460 return true;
461 }
462 else if(ev.getType() == GUIMouseEventType::MouseDown && ev.getButton() == GUIMouseButton::Left)
463 {
464 if (!_isDisabled())
465 {
466 if (ev.isShiftDown())
467 {
468 if (!mSelectionShown)
469 showSelection(gGUIManager().getInputCaretTool()->getCaretPos());
470 }
471 else
472 {
473 bool focusGainedThisFrame = mHasFocus && mFocusGainedFrame == gTime().getFrameIdx();
474
475 // We want to select all on focus gain, so don't override that
476 if(!focusGainedThisFrame)
477 clearSelection();
478
479 showCaret();
480 }
481
482 if (mNumChars > 0)
483 gGUIManager().getInputCaretTool()->moveCaretToPos(ev.getPosition());
484 else
485 gGUIManager().getInputCaretTool()->moveCaretToStart();
486
487 if (ev.isShiftDown())
488 gGUIManager().getInputSelectionTool()->moveSelectionToCaret(gGUIManager().getInputCaretTool()->getCaretPos());
489
490 scrollTextToCaret();
491 _markContentAsDirty();
492 }
493
494 return true;
495 }
496 else if(ev.getType() == GUIMouseEventType::MouseDragStart)
497 {
498 if (!_isDisabled())
499 {
500 if (!ev.isShiftDown())
501 {
502 mDragInProgress = true;
503
504 UINT32 caretPos = gGUIManager().getInputCaretTool()->getCaretPos();
505 showSelection(caretPos);
506 gGUIManager().getInputSelectionTool()->selectionDragStart(caretPos);
507 _markContentAsDirty();
508
509 return true;
510 }
511 }
512 }
513 else if(ev.getType() == GUIMouseEventType::MouseDragEnd)
514 {
515 if (!_isDisabled())
516 {
517 if (!ev.isShiftDown())
518 {
519 mDragInProgress = false;
520
521 gGUIManager().getInputSelectionTool()->selectionDragEnd();
522 _markContentAsDirty();
523 return true;
524 }
525 }
526 }
527 else if(ev.getType() == GUIMouseEventType::MouseDrag)
528 {
529 if (!_isDisabled())
530 {
531 if (!ev.isShiftDown())
532 {
533 if (mNumChars > 0)
534 gGUIManager().getInputCaretTool()->moveCaretToPos(ev.getPosition());
535 else
536 gGUIManager().getInputCaretTool()->moveCaretToStart();
537
538 gGUIManager().getInputSelectionTool()->selectionDragUpdate(gGUIManager().getInputCaretTool()->getCaretPos());
539
540 scrollTextToCaret();
541 _markContentAsDirty();
542 return true;
543 }
544 }
545 }
546
547 return false;
548 }
549
550 bool GUIInputBox::_textInputEvent(const GUITextInputEvent& ev)
551 {
552 if (_isDisabled())
553 return false;
554
555 Vector2I origSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
556
557 if(mSelectionShown)
558 deleteSelectedText(true);
559
560 UINT32 charIdx = gGUIManager().getInputCaretTool()->getCharIdxAtCaretPos();
561
562 bool filterOkay = true;
563 if(mFilter != nullptr)
564 {
565 String newText = mText;
566 UINT32 byteIdx = UTF8::charToByteIndex(mText, charIdx);
567 String utf8chars = UTF8::fromUTF32(U32String(1, ev.getInputChar()));
568 newText.insert(newText.begin() + byteIdx, utf8chars.begin(), utf8chars.end());
569
570 filterOkay = mFilter(newText);
571 }
572
573 if(filterOkay)
574 {
575 insertChar(charIdx, ev.getInputChar());
576
577 gGUIManager().getInputCaretTool()->moveCaretToChar(charIdx, CARET_AFTER);
578 scrollTextToCaret();
579
580 if(!onValueChanged.empty())
581 onValueChanged(mText);
582 }
583
584 Vector2I newSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
585 if (origSize != newSize)
586 _markLayoutAsDirty();
587 else
588 _markContentAsDirty();
589
590 return true;
591 }
592
593 bool GUIInputBox::_commandEvent(const GUICommandEvent& ev)
594 {
595 if (_isDisabled())
596 return false;
597
598 bool baseReturn = GUIElement::_commandEvent(ev);
599
600 if(ev.getType() == GUICommandEventType::Redraw)
601 {
602 _markContentAsDirty();
603 return true;
604 }
605
606 if(ev.getType() == GUICommandEventType::FocusGained)
607 {
608 Vector2I origSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
609 mState = State::Focused;
610
611 showSelection(0);
612 gGUIManager().getInputSelectionTool()->selectAll();
613
614 mHasFocus = true;
615 mFocusGainedFrame = gTime().getFrameIdx();
616
617 Vector2I newSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
618 if (origSize != newSize)
619 _markLayoutAsDirty();
620 else
621 _markContentAsDirty();
622
623 return true;
624 }
625
626 if(ev.getType() == GUICommandEventType::FocusLost)
627 {
628 Vector2I origSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
629 mState = State::Normal;
630
631 hideCaret();
632 clearSelection();
633
634 mHasFocus = false;
635
636 Vector2I newSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
637 if (origSize != newSize)
638 _markLayoutAsDirty();
639 else
640 _markContentAsDirty();
641
642 return true;
643 }
644
645 if(ev.getType() == GUICommandEventType::Backspace)
646 {
647 if(mNumChars > 0)
648 {
649 Vector2I origSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
650 if(mSelectionShown)
651 {
652 deleteSelectedText();
653 }
654 else
655 {
656 UINT32 charIdx = gGUIManager().getInputCaretTool()->getCharIdxAtCaretPos() - 1;
657
658 if(charIdx < mNumChars)
659 {
660 bool filterOkay = true;
661 if(mFilter != nullptr)
662 {
663 String newText = mText;
664 UINT32 byteIdx = UTF8::charToByteIndex(mText, charIdx);
665 UINT32 byteCount = UTF8::charByteCount(mText, charIdx);
666 newText.erase(byteIdx, byteCount);
667
668 filterOkay = mFilter(newText);
669 }
670
671 if(filterOkay)
672 {
673 eraseChar(charIdx);
674
675 if (charIdx > 0)
676 {
677 charIdx--;
678
679 gGUIManager().getInputCaretTool()->moveCaretToChar(charIdx, CARET_AFTER);
680 }
681 else
682 gGUIManager().getInputCaretTool()->moveCaretToChar(charIdx, CARET_BEFORE);
683
684 scrollTextToCaret();
685
686 if(!onValueChanged.empty())
687 onValueChanged(mText);
688 }
689 }
690 }
691
692 Vector2I newSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
693 if (origSize != newSize)
694 _markLayoutAsDirty();
695 else
696 _markContentAsDirty();
697 }
698
699 return true;
700 }
701
702 if(ev.getType() == GUICommandEventType::Delete)
703 {
704 if(mNumChars > 0)
705 {
706 Vector2I origSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
707 if(mSelectionShown)
708 {
709 deleteSelectedText();
710 }
711 else
712 {
713 UINT32 charIdx = gGUIManager().getInputCaretTool()->getCharIdxAtCaretPos();
714 if(charIdx < mNumChars)
715 {
716 bool filterOkay = true;
717 if(mFilter != nullptr)
718 {
719 String newText = mText;
720 UINT32 byteIdx = UTF8::charToByteIndex(mText, charIdx);
721 UINT32 byteCount = UTF8::charByteCount(mText, charIdx);
722 newText.erase(byteIdx, byteCount);
723
724 filterOkay = mFilter(newText);
725 }
726
727 if(filterOkay)
728 {
729 eraseChar(charIdx);
730
731 if(charIdx > 0)
732 charIdx--;
733
734 gGUIManager().getInputCaretTool()->moveCaretToChar(charIdx, CARET_AFTER);
735
736 scrollTextToCaret();
737
738 if(!onValueChanged.empty())
739 onValueChanged(mText);
740 }
741 }
742 }
743
744 Vector2I newSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
745 if (origSize != newSize)
746 _markLayoutAsDirty();
747 else
748 _markContentAsDirty();
749 }
750
751 return true;
752 }
753
754 if(ev.getType() == GUICommandEventType::MoveLeft)
755 {
756 if(mSelectionShown)
757 {
758 UINT32 selStart = gGUIManager().getInputSelectionTool()->getSelectionStart();
759 clearSelection();
760
761 if (!mCaretShown)
762 showCaret();
763
764 if(selStart > 0)
765 gGUIManager().getInputCaretTool()->moveCaretToChar(selStart - 1, CARET_AFTER);
766 else
767 gGUIManager().getInputCaretTool()->moveCaretToChar(0, CARET_BEFORE);
768 }
769 else
770 gGUIManager().getInputCaretTool()->moveCaretLeft();
771
772 scrollTextToCaret();
773 _markContentAsDirty();
774 return true;
775 }
776
777 if(ev.getType() == GUICommandEventType::SelectLeft)
778 {
779 if(!mSelectionShown)
780 showSelection(gGUIManager().getInputCaretTool()->getCaretPos());
781
782 gGUIManager().getInputCaretTool()->moveCaretLeft();
783 gGUIManager().getInputSelectionTool()->moveSelectionToCaret(gGUIManager().getInputCaretTool()->getCaretPos());
784
785 scrollTextToCaret();
786 _markContentAsDirty();
787 return true;
788 }
789
790 if(ev.getType() == GUICommandEventType::MoveRight)
791 {
792 if(mSelectionShown)
793 {
794 UINT32 selEnd = gGUIManager().getInputSelectionTool()->getSelectionEnd();
795 clearSelection();
796
797 if (!mCaretShown)
798 showCaret();
799
800 if(selEnd > 0)
801 gGUIManager().getInputCaretTool()->moveCaretToChar(selEnd - 1, CARET_AFTER);
802 else
803 gGUIManager().getInputCaretTool()->moveCaretToChar(0, CARET_BEFORE);
804 }
805 else
806 gGUIManager().getInputCaretTool()->moveCaretRight();
807
808 scrollTextToCaret();
809 _markContentAsDirty();
810 return true;
811 }
812
813 if(ev.getType() == GUICommandEventType::SelectRight)
814 {
815 if(!mSelectionShown)
816 showSelection(gGUIManager().getInputCaretTool()->getCaretPos());
817
818 gGUIManager().getInputCaretTool()->moveCaretRight();
819 gGUIManager().getInputSelectionTool()->moveSelectionToCaret(gGUIManager().getInputCaretTool()->getCaretPos());
820
821 scrollTextToCaret();
822 _markContentAsDirty();
823 return true;
824 }
825
826 if(ev.getType() == GUICommandEventType::MoveUp)
827 {
828 if (mSelectionShown)
829 clearSelection();
830
831 if (!mCaretShown)
832 showCaret();
833
834 gGUIManager().getInputCaretTool()->moveCaretUp();
835
836 scrollTextToCaret();
837 _markContentAsDirty();
838 return true;
839 }
840
841 if(ev.getType() == GUICommandEventType::SelectUp)
842 {
843 if(!mSelectionShown)
844 showSelection(gGUIManager().getInputCaretTool()->getCaretPos());;
845
846 gGUIManager().getInputCaretTool()->moveCaretUp();
847 gGUIManager().getInputSelectionTool()->moveSelectionToCaret(gGUIManager().getInputCaretTool()->getCaretPos());
848
849 scrollTextToCaret();
850 _markContentAsDirty();
851 return true;
852 }
853
854 if(ev.getType() == GUICommandEventType::MoveDown)
855 {
856 if (mSelectionShown)
857 clearSelection();
858
859 if (!mCaretShown)
860 showCaret();
861
862 gGUIManager().getInputCaretTool()->moveCaretDown();
863
864 scrollTextToCaret();
865 _markContentAsDirty();
866 return true;
867 }
868
869 if(ev.getType() == GUICommandEventType::SelectDown)
870 {
871 if(!mSelectionShown)
872 showSelection(gGUIManager().getInputCaretTool()->getCaretPos());
873
874 gGUIManager().getInputCaretTool()->moveCaretDown();
875 gGUIManager().getInputSelectionTool()->moveSelectionToCaret(gGUIManager().getInputCaretTool()->getCaretPos());
876
877 scrollTextToCaret();
878 _markContentAsDirty();
879 return true;
880 }
881
882 if(ev.getType() == GUICommandEventType::Return)
883 {
884 if (mIsMultiline)
885 {
886 Vector2I origSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
887
888 if (mSelectionShown)
889 deleteSelectedText();
890
891 UINT32 charIdx = gGUIManager().getInputCaretTool()->getCharIdxAtCaretPos();
892
893 bool filterOkay = true;
894 if (mFilter != nullptr)
895 {
896 String newText = mText;
897 UINT32 byteIdx = UTF8::charToByteIndex(mText, charIdx);
898 newText.insert(newText.begin() + byteIdx, '\n');
899
900 filterOkay = mFilter(newText);
901 }
902
903 if (filterOkay)
904 {
905 insertChar(charIdx, '\n');
906
907 gGUIManager().getInputCaretTool()->moveCaretRight();
908 scrollTextToCaret();
909
910 if (!onValueChanged.empty())
911 onValueChanged(mText);
912 }
913
914 Vector2I newSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
915 if (origSize != newSize)
916 _markLayoutAsDirty();
917 else
918 _markContentAsDirty();
919
920 return true;
921 }
922 }
923
924 if (ev.getType() == GUICommandEventType::Confirm)
925 {
926 onConfirm();
927 return true;
928 }
929
930 return baseReturn;
931 }
932
933 bool GUIInputBox::_virtualButtonEvent(const GUIVirtualButtonEvent& ev)
934 {
935 if (_isDisabled())
936 return false;
937
938 if(ev.getButton() == mCutVB)
939 {
940 cutText();
941
942 return true;
943 }
944 else if(ev.getButton() == mCopyVB)
945 {
946 copyText();
947
948 return true;
949 }
950 else if(ev.getButton() == mPasteVB)
951 {
952 pasteText();
953 return true;
954 }
955 else if(ev.getButton() == mSelectAllVB)
956 {
957 showSelection(0);
958 gGUIManager().getInputSelectionTool()->selectAll();
959 _markContentAsDirty();
960
961 return true;
962 }
963
964 return false;
965 }
966
967 void GUIInputBox::showCaret()
968 {
969 mCaretShown = true;
970
971 TEXT_SPRITE_DESC textDesc = getTextDesc();
972 gGUIManager().getInputCaretTool()->updateText(this, textDesc);
973 }
974
975 void GUIInputBox::hideCaret()
976 {
977 mCaretShown = false;
978 }
979
980 void GUIInputBox::showSelection(UINT32 anchorCaretPos)
981 {
982 TEXT_SPRITE_DESC textDesc = getTextDesc();
983 gGUIManager().getInputSelectionTool()->updateText(this, textDesc);
984
985 gGUIManager().getInputSelectionTool()->showSelection(anchorCaretPos);
986 mSelectionShown = true;
987 }
988
989 void GUIInputBox::clearSelection()
990 {
991 gGUIManager().getInputSelectionTool()->clearSelectionVisuals();
992 mSelectionShown = false;
993 }
994
995 void GUIInputBox::scrollTextToCaret()
996 {
997 TEXT_SPRITE_DESC textDesc = getTextDesc();
998
999 Vector2I textOffset = getTextOffset();
1000 Vector2I caretPos = gGUIManager().getInputCaretTool()->getCaretPosition(textOffset);
1001 UINT32 caretHeight = gGUIManager().getInputCaretTool()->getCaretHeight();
1002 UINT32 caretWidth = 1;
1003
1004 INT32 left = textOffset.x - mTextOffset.x;
1005 // Include caret width here because we don't want to scroll if just the caret is outside the bounds
1006 // (Possible if the text width is exactly the maximum width)
1007 INT32 right = left + (INT32)textDesc.width + caretWidth;
1008 INT32 top = textOffset.y - mTextOffset.y;
1009 INT32 bottom = top + (INT32)textDesc.height;
1010
1011 // If caret is too high to display we don't want the offset to keep adjusting itself
1012 caretHeight = std::min(caretHeight, (UINT32)(bottom - top));
1013 INT32 caretRight = caretPos.x + (INT32)caretWidth;
1014 INT32 caretBottom = caretPos.y + (INT32)caretHeight;
1015
1016 Vector2I offset;
1017 if(caretPos.x < left)
1018 {
1019 offset.x = left - caretPos.x;
1020 }
1021 else if(caretRight > right)
1022 {
1023 offset.x = -(caretRight - right);
1024 }
1025
1026 if(caretPos.y < top)
1027 {
1028 offset.y = top - caretPos.y;
1029 }
1030 else if(caretBottom > bottom)
1031 {
1032 offset.y = -(caretBottom - bottom);
1033 }
1034
1035 mTextOffset += offset;
1036
1037 gGUIManager().getInputCaretTool()->updateText(this, textDesc);
1038 gGUIManager().getInputSelectionTool()->updateText(this, textDesc);
1039 }
1040
1041 void GUIInputBox::clampScrollToBounds(Rect2I unclippedTextBounds)
1042 {
1043 TEXT_SPRITE_DESC textDesc = getTextDesc();
1044
1045 Vector2I newTextOffset;
1046 INT32 maxScrollableWidth = std::max(0, (INT32)unclippedTextBounds.width - (INT32)textDesc.width);
1047 INT32 maxScrollableHeight = std::max(0, (INT32)unclippedTextBounds.height - (INT32)textDesc.height);
1048 newTextOffset.x = Math::clamp(mTextOffset.x, -maxScrollableWidth, 0);
1049 newTextOffset.y = Math::clamp(mTextOffset.y, -maxScrollableHeight, 0);
1050
1051 if(newTextOffset != mTextOffset)
1052 {
1053 mTextOffset = newTextOffset;
1054
1055 gGUIManager().getInputCaretTool()->updateText(this, textDesc);
1056 gGUIManager().getInputSelectionTool()->updateText(this, textDesc);
1057 }
1058 }
1059
1060 void GUIInputBox::insertString(UINT32 charIdx, const String& string)
1061 {
1062 UINT32 byteIdx = UTF8::charToByteIndex(mText, charIdx);
1063
1064 mText.insert(mText.begin() + byteIdx, string.begin(), string.end());
1065 mNumChars = UTF8::count(mText);
1066
1067 TEXT_SPRITE_DESC textDesc = getTextDesc();
1068
1069 gGUIManager().getInputCaretTool()->updateText(this, textDesc);
1070 gGUIManager().getInputSelectionTool()->updateText(this, textDesc);
1071 }
1072
1073 void GUIInputBox::insertChar(UINT32 charIdx, UINT32 charCode)
1074 {
1075 UINT32 byteIdx = UTF8::charToByteIndex(mText, charIdx);
1076 String utf8chars = UTF8::fromUTF32(U32String(1, (char32_t)charCode));
1077
1078 mText.insert(mText.begin() + byteIdx, utf8chars.begin(), utf8chars.end());
1079 mNumChars = UTF8::count(mText);
1080
1081 TEXT_SPRITE_DESC textDesc = getTextDesc();
1082
1083 gGUIManager().getInputCaretTool()->updateText(this, textDesc);
1084 gGUIManager().getInputSelectionTool()->updateText(this, textDesc);
1085 }
1086
1087 void GUIInputBox::eraseChar(UINT32 charIdx)
1088 {
1089 UINT32 byteIdx = UTF8::charToByteIndex(mText, charIdx);
1090 UINT32 byteCount = UTF8::charByteCount(mText, charIdx);
1091
1092 mText.erase(byteIdx, byteCount);
1093 mNumChars = UTF8::count(mText);
1094
1095 TEXT_SPRITE_DESC textDesc = getTextDesc();
1096
1097 gGUIManager().getInputCaretTool()->updateText(this, textDesc);
1098 gGUIManager().getInputSelectionTool()->updateText(this, textDesc);
1099 }
1100
1101 void GUIInputBox::deleteSelectedText(bool internal)
1102 {
1103 UINT32 selStart = gGUIManager().getInputSelectionTool()->getSelectionStart();
1104 UINT32 selEnd = gGUIManager().getInputSelectionTool()->getSelectionEnd();
1105
1106 UINT32 byteStart = UTF8::charToByteIndex(mText, selStart);
1107 UINT32 byteEnd = UTF8::charToByteIndex(mText, selEnd);
1108
1109 bool filterOkay = true;
1110 if (!internal && mFilter != nullptr)
1111 {
1112 String newText = mText;
1113 newText.erase(newText.begin() + byteStart, newText.begin() + byteEnd);
1114
1115 filterOkay = mFilter(newText);
1116 }
1117
1118 if (!mCaretShown)
1119 showCaret();
1120
1121 if(filterOkay)
1122 {
1123 mText.erase(mText.begin() + byteStart, mText.begin() + byteEnd);
1124 mNumChars = UTF8::count(mText);
1125
1126 TEXT_SPRITE_DESC textDesc = getTextDesc();
1127 gGUIManager().getInputCaretTool()->updateText(this, textDesc);
1128 gGUIManager().getInputSelectionTool()->updateText(this, textDesc);
1129
1130 if(selStart > 0)
1131 {
1132 UINT32 newCaretPos = selStart - 1;
1133 gGUIManager().getInputCaretTool()->moveCaretToChar(newCaretPos, CARET_AFTER);
1134 }
1135 else
1136 {
1137 gGUIManager().getInputCaretTool()->moveCaretToChar(0, CARET_BEFORE);
1138 }
1139
1140 scrollTextToCaret();
1141
1142 if (!internal)
1143 onValueChanged(mText);
1144 }
1145
1146 clearSelection();
1147 }
1148
1149 String GUIInputBox::getSelectedText()
1150 {
1151 UINT32 selStart = gGUIManager().getInputSelectionTool()->getSelectionStart();
1152 UINT32 selEnd = gGUIManager().getInputSelectionTool()->getSelectionEnd();
1153
1154 UINT32 byteStart = UTF8::charToByteIndex(mText, selStart);
1155 UINT32 byteEnd = UTF8::charToByteIndex(mText, selEnd);
1156
1157 return mText.substr(byteStart, byteEnd - byteStart);
1158 }
1159
1160 Vector2I GUIInputBox::getTextOffset() const
1161 {
1162 Rect2I textBounds = getCachedContentBounds();
1163 return Vector2I(textBounds.x, textBounds.y) + mTextOffset;
1164 }
1165
1166 Rect2I GUIInputBox::getTextClipRect() const
1167 {
1168 Rect2I contentClipRect = getCachedContentClipRect();
1169 return Rect2I(contentClipRect.x - mTextOffset.x, contentClipRect.y - mTextOffset.y, contentClipRect.width, contentClipRect.height);
1170 }
1171
1172 TEXT_SPRITE_DESC GUIInputBox::getTextDesc() const
1173 {
1174 TEXT_SPRITE_DESC textDesc;
1175 textDesc.text = mText;
1176 textDesc.font = _getStyle()->font;
1177 textDesc.fontSize = _getStyle()->fontSize;
1178 textDesc.color = getTint() * getActiveTextColor();
1179
1180 Rect2I textBounds = getCachedContentBounds();
1181 textDesc.width = textBounds.width;
1182 textDesc.height = textBounds.height;
1183 textDesc.horzAlign = _getStyle()->textHorzAlign;
1184 textDesc.vertAlign = _getStyle()->textVertAlign;
1185 textDesc.wordWrap = mIsMultiline;
1186
1187 return textDesc;
1188 }
1189
1190 const HSpriteTexture& GUIInputBox::getActiveTexture() const
1191 {
1192 switch(mState)
1193 {
1194 case State::Focused:
1195 return _getStyle()->focused.texture;
1196 case State::Hover:
1197 return _getStyle()->hover.texture;
1198 case State::Normal:
1199 return _getStyle()->normal.texture;
1200 }
1201
1202 return _getStyle()->normal.texture;
1203 }
1204
1205 Color GUIInputBox::getActiveTextColor() const
1206 {
1207 switch (mState)
1208 {
1209 case State::Focused:
1210 return _getStyle()->focused.textColor;
1211 case State::Hover:
1212 return _getStyle()->hover.textColor;
1213 case State::Normal:
1214 return _getStyle()->normal.textColor;
1215 }
1216
1217 return _getStyle()->normal.textColor;
1218 }
1219
1220 SPtr<GUIContextMenu> GUIInputBox::_getContextMenu() const
1221 {
1222 static SPtr<GUIContextMenu> contextMenu;
1223
1224 if (contextMenu == nullptr)
1225 {
1226 contextMenu = bs_shared_ptr_new<GUIContextMenu>();
1227
1228 contextMenu->addMenuItem("Cut", std::bind(&GUIInputBox::cutText, const_cast<GUIInputBox*>(this)), 0);
1229 contextMenu->addMenuItem("Copy", std::bind(&GUIInputBox::copyText, const_cast<GUIInputBox*>(this)), 0);
1230 contextMenu->addMenuItem("Paste", std::bind(&GUIInputBox::pasteText, const_cast<GUIInputBox*>(this)), 0);
1231
1232 contextMenu->setLocalizedName("Cut", HString("Cut"));
1233 contextMenu->setLocalizedName("Copy", HString("Copy"));
1234 contextMenu->setLocalizedName("Paste", HString("Paste"));
1235 }
1236
1237 if (!_isDisabled())
1238 return contextMenu;
1239
1240 return nullptr;
1241 }
1242
1243 void GUIInputBox::cutText()
1244 {
1245 Vector2I origSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
1246
1247 copyText();
1248 deleteSelectedText();
1249
1250 Vector2I newSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
1251 if (origSize != newSize)
1252 _markLayoutAsDirty();
1253 else
1254 _markContentAsDirty();
1255 }
1256
1257 void GUIInputBox::copyText()
1258 {
1259 Platform::copyToClipboard(getSelectedText());
1260 }
1261
1262 void GUIInputBox::pasteText()
1263 {
1264 deleteSelectedText(true);
1265
1266 String textInClipboard = Platform::copyFromClipboard();
1267 UINT32 charIdx = gGUIManager().getInputCaretTool()->getCharIdxAtCaretPos();
1268
1269 bool filterOkay = true;
1270 if(mFilter != nullptr)
1271 {
1272 String newText = mText;
1273
1274 UINT32 byteIdx = UTF8::charToByteIndex(newText, charIdx);
1275 newText.insert(newText.begin() + byteIdx, textInClipboard.begin(), textInClipboard.end());
1276
1277 filterOkay = mFilter(newText);
1278 }
1279
1280 if(filterOkay)
1281 {
1282 Vector2I origSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
1283 insertString(charIdx, textInClipboard);
1284
1285 UINT32 numChars = UTF8::count(textInClipboard);
1286 if(numChars > 0)
1287 gGUIManager().getInputCaretTool()->moveCaretToChar(charIdx + (numChars - 1), CARET_AFTER);
1288
1289 scrollTextToCaret();
1290
1291 Vector2I newSize = mDimensions.calculateSizeRange(_getOptimalSize()).optimal;
1292 if (origSize != newSize)
1293 _markLayoutAsDirty();
1294 else
1295 _markContentAsDirty();
1296
1297 if(!onValueChanged.empty())
1298 onValueChanged(mText);
1299 }
1300 }
1301}
1302