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/BsGUIInputCaret.h"
4#include "Image/BsSpriteTexture.h"
5#include "GUI/BsGUIManager.h"
6#include "2D/BsImageSprite.h"
7#include "GUI/BsGUIElement.h"
8#include "Text/BsFont.h"
9
10namespace bs
11{
12 GUIInputCaret::GUIInputCaret()
13 {
14 mCaretSprite = bs_new<ImageSprite>();
15 }
16
17 GUIInputCaret::~GUIInputCaret()
18 {
19 bs_delete(mCaretSprite);
20 }
21
22 Vector2I GUIInputCaret::getSpriteOffset() const
23 {
24 return getCaretPosition(getTextOffset());
25 }
26
27 Rect2I GUIInputCaret::getSpriteClipRect(const Rect2I& parentClipRect) const
28 {
29 Vector2I offset(mElement->_getLayoutData().area.x, mElement->_getLayoutData().area.y);
30
31 Vector2I clipOffset = getSpriteOffset() - offset -
32 Vector2I(mElement->_getTextInputRect().x, mElement->_getTextInputRect().y);
33
34 Rect2I clipRect(-clipOffset.x, -clipOffset.y, mTextDesc.width, mTextDesc.height);
35
36 Rect2I localParentCliprect = parentClipRect;
37
38 // Move parent rect to our space
39 localParentCliprect.x += mElement->_getTextInputOffset().x + clipRect.x;
40 localParentCliprect.y += mElement->_getTextInputOffset().y + clipRect.y;
41
42 // Clip our rectangle so its not larger then the parent
43 clipRect.clip(localParentCliprect);
44
45 // Increase clip size by 1, so we can fit the caret in case it is fully at the end of the text
46 clipRect.width += 1;
47
48 return clipRect;
49 }
50
51 void GUIInputCaret::updateSprite()
52 {
53 IMAGE_SPRITE_DESC mCaretDesc;
54 mCaretDesc.width = 1;
55 mCaretDesc.height = getCaretHeight();
56 mCaretDesc.texture = GUIManager::instance().getCaretTexture();
57
58 GUIWidget* widget = nullptr;
59 if (mElement != nullptr)
60 widget = mElement->_getParentWidget();
61
62 mCaretSprite->update(mCaretDesc, (UINT64)widget);
63 }
64
65 void GUIInputCaret::moveCaretToStart()
66 {
67 mCaretPos = 0;
68 }
69
70 void GUIInputCaret::moveCaretToEnd()
71 {
72 mCaretPos = getMaxCaretPos();
73 }
74
75 void GUIInputCaret::moveCaretLeft()
76 {
77 mCaretPos = (UINT32)std::max(0, (INT32)mCaretPos - 1);
78 }
79
80 void GUIInputCaret::moveCaretRight()
81 {
82 UINT32 maxCaretPos = getMaxCaretPos();
83
84 mCaretPos = std::min(maxCaretPos, mCaretPos + 1);
85 }
86
87 void GUIInputCaret::moveCaretUp()
88 {
89 UINT32 charIdx = getCharIdxAtCaretPos();
90 if(charIdx > 0)
91 charIdx -= 1;
92
93 UINT32 lineIdx = getLineForChar(charIdx);
94 const GUIInputLineDesc& desc = getLineDesc(lineIdx);
95 // If char is a newline, I want that to count as being on the next line because that's
96 // how user sees it
97 if(desc.isNewline(charIdx))
98 lineIdx++;
99
100 if(lineIdx == 0)
101 {
102 moveCaretToStart();
103 return;
104 }
105
106 Vector2I caretCoords = getCaretPosition(mElement->_getTextInputOffset());
107 caretCoords.y -= getCaretHeight();
108
109 moveCaretToPos(caretCoords);
110 }
111
112 void GUIInputCaret::moveCaretDown()
113 {
114 UINT32 charIdx = getCharIdxAtCaretPos();
115 if(charIdx > 0)
116 charIdx -= 1;
117
118 UINT32 lineIdx = getLineForChar(charIdx);
119 const GUIInputLineDesc& desc = getLineDesc(lineIdx);
120 // If char is a newline, I want that to count as being on the next line because that's
121 // how user sees it
122 if(desc.isNewline(charIdx))
123 lineIdx++;
124
125 if(lineIdx == (getNumLines() - 1))
126 {
127 moveCaretToEnd();
128 return;
129 }
130
131 Vector2I caretCoords = getCaretPosition(mElement->_getTextInputOffset());
132 caretCoords.y += getCaretHeight();
133
134 moveCaretToPos(caretCoords);
135 }
136
137 void GUIInputCaret::moveCaretToPos(const Vector2I& pos)
138 {
139 INT32 charIdx = getCharIdxAtPos(pos);
140
141 if(charIdx != -1)
142 {
143 Rect2I charRect = getCharRect(charIdx);
144
145 float xCenter = charRect.x + charRect.width * 0.5f;
146 if(pos.x <= xCenter)
147 moveCaretToChar(charIdx, CARET_BEFORE);
148 else
149 moveCaretToChar(charIdx, CARET_AFTER);
150 }
151 else
152 {
153 UINT32 numLines = getNumLines();
154
155 if(numLines == 0)
156 {
157 mCaretPos = 0;
158 return;
159 }
160
161 UINT32 curPos = 0;
162 for(UINT32 i = 0; i < numLines; i++)
163 {
164 const GUIInputLineDesc& line = getLineDesc(i);
165
166 INT32 lineStart = line.getLineYStart() + getTextOffset().y;
167 if(pos.y >= lineStart && pos.y < (lineStart + (INT32)line.getLineHeight()))
168 {
169 mCaretPos = curPos;
170 return;
171 }
172
173 UINT32 numChars = line.getEndChar(false) - line.getStartChar() + 1; // +1 For extra line start position
174 curPos += numChars;
175 }
176
177 {
178 const GUIInputLineDesc& firstLine = getLineDesc(0);
179 INT32 lineStart = firstLine.getLineYStart() + getTextOffset().y;
180
181 if(pos.y < lineStart) // Before first line
182 mCaretPos = 0;
183 else // After last line
184 mCaretPos = curPos - 1;
185 }
186 }
187 }
188
189 void GUIInputCaret::moveCaretToChar(UINT32 charIdx, CaretPos caretPos)
190 {
191 if(charIdx >= mNumChars)
192 {
193 mCaretPos = 0;
194 return;
195 }
196
197 UINT32 numLines = getNumLines();
198 UINT32 curPos = 0;
199 UINT32 curCharIdx = 0;
200 for(UINT32 i = 0; i < numLines; i++)
201 {
202 const GUIInputLineDesc& lineDesc = getLineDesc(i);
203
204 curPos++; // Move past line start position
205
206 UINT32 numChars = lineDesc.getEndChar() - lineDesc.getStartChar();
207 UINT32 numCaretPositions = lineDesc.getEndChar(false) - lineDesc.getStartChar();
208 if(charIdx >= (curCharIdx + numChars))
209 {
210 curCharIdx += numChars;
211 curPos += numCaretPositions;
212 continue;
213 }
214
215 UINT32 diff = charIdx - curCharIdx;
216
217 if(caretPos == CARET_BEFORE)
218 curPos += diff - 1;
219 else
220 curPos += diff;
221
222 break;
223 }
224
225 mCaretPos = curPos;
226 }
227
228 UINT32 GUIInputCaret::getCharIdxAtCaretPos() const
229 {
230 return getCharIdxAtInputIdx(mCaretPos);
231 }
232
233 Vector2I GUIInputCaret::getCaretPosition(const Vector2I& offset) const
234 {
235 if(mNumChars > 0 && isDescValid())
236 {
237 UINT32 curPos = 0;
238 UINT32 numLines = getNumLines();
239
240 for(UINT32 i = 0; i < numLines; i++)
241 {
242 const GUIInputLineDesc& lineDesc = getLineDesc(i);
243
244 if(mCaretPos == curPos)
245 {
246 // Caret is on line start
247 return Vector2I(offset.x, lineDesc.getLineYStart() + getTextOffset().y);
248 }
249
250 curPos += lineDesc.getEndChar(false) - lineDesc.getStartChar() + 1; // + 1 for special line start position
251 }
252
253 UINT32 charIdx = getCharIdxAtCaretPos();
254 if(charIdx > 0)
255 charIdx -= 1;
256
257 charIdx = std::min((UINT32)(mNumChars - 1), charIdx);
258
259 Rect2I charRect = getCharRect(charIdx);
260 UINT32 lineIdx = getLineForChar(charIdx);
261 UINT32 yOffset = getLineDesc(lineIdx).getLineYStart() + getTextOffset().y;
262
263 return Vector2I(charRect.x + charRect.width, yOffset);
264 }
265
266 return offset;
267 }
268
269 UINT32 GUIInputCaret::getCaretHeight() const
270 {
271 UINT32 charIdx = getCharIdxAtCaretPos();
272 if(charIdx > 0)
273 charIdx -= 1;
274
275 if(charIdx < mNumChars && isDescValid())
276 {
277 UINT32 lineIdx = getLineForChar(charIdx);
278 return getLineDesc(lineIdx).getLineHeight();
279 }
280 else
281 {
282 if(mTextDesc.font != nullptr)
283 {
284 UINT32 nearestSize = mTextDesc.font->getClosestSize(mTextDesc.fontSize);
285 SPtr<const FontBitmap> fontData = mTextDesc.font->getBitmap(nearestSize);
286
287 if(fontData != nullptr)
288 return fontData->lineHeight;
289 }
290 }
291
292 return 0;
293 }
294
295 bool GUIInputCaret::isCaretAtNewline() const
296 {
297 return isNewline(mCaretPos);
298 }
299
300 UINT32 GUIInputCaret::getMaxCaretPos() const
301 {
302 if(mNumChars == 0)
303 return 0;
304
305 UINT32 numLines = getNumLines();
306 UINT32 maxPos = 0;
307 for(UINT32 i = 0; i < numLines; i++)
308 {
309 const GUIInputLineDesc& lineDesc = getLineDesc(i);
310
311 UINT32 numChars = lineDesc.getEndChar(false) - lineDesc.getStartChar() + 1; // + 1 for special line start position
312 maxPos += numChars;
313 }
314
315 return maxPos - 1;
316 }
317}