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/BsGUIInputTool.h"
4#include "GUI/BsGUIElement.h"
5#include "Math/BsMath.h"
6#include "Math/BsVector2.h"
7#include "Text/BsFont.h"
8#include "String/BsUnicode.h"
9
10namespace bs
11{
12 void GUIInputTool::updateText(const GUIElement* element, const TEXT_SPRITE_DESC& textDesc)
13 {
14 mElement = element;
15 mTextDesc = textDesc;
16 mNumChars = UTF8::count(mTextDesc.text);
17
18 mLineDescs.clear();
19
20 bs_frame_mark();
21 {
22 const U32String utf32text = UTF8::toUTF32(mTextDesc.text);
23 TextData<FrameAlloc> textData(utf32text, mTextDesc.font, mTextDesc.fontSize,
24 mTextDesc.width, mTextDesc.height, mTextDesc.wordWrap, mTextDesc.wordBreak);
25
26 UINT32 numLines = textData.getNumLines();
27 UINT32 numPages = textData.getNumPages();
28
29 mNumQuads = 0;
30 for (UINT32 i = 0; i < numPages; i++)
31 mNumQuads += textData.getNumQuadsForPage(i);
32
33 if (mQuads != nullptr)
34 bs_delete(mQuads);
35
36 mQuads = bs_newN<Vector2>(mNumQuads * 4);
37
38 TextSprite::genTextQuads(textData, mTextDesc.width, mTextDesc.height, mTextDesc.horzAlign, mTextDesc.vertAlign, mTextDesc.anchor,
39 mQuads, nullptr, nullptr, mNumQuads);
40
41 // Store cached line data
42 UINT32 curCharIdx = 0;
43 UINT32 curLineIdx = 0;
44
45 Vector2I* alignmentOffsets = bs_frame_new<Vector2I>(numLines);
46 TextSprite::getAlignmentOffsets(textData, mTextDesc.width, mTextDesc.height, mTextDesc.horzAlign,
47 mTextDesc.vertAlign, alignmentOffsets);
48
49 for (UINT32 i = 0; i < numLines; i++)
50 {
51 const TextDataBase::TextLine& line = textData.getLine(i);
52
53 // Line has a newline char only if it wasn't created by word wrap and it isn't the last line
54 bool hasNewline = line.hasNewlineChar() && (curLineIdx != (numLines - 1));
55
56 UINT32 startChar = curCharIdx;
57 UINT32 endChar = curCharIdx + line.getNumChars() + (hasNewline ? 1 : 0);
58 UINT32 lineHeight = line.getYOffset();
59 INT32 lineYStart = alignmentOffsets[curLineIdx].y;
60
61 GUIInputLineDesc lineDesc(startChar, endChar, lineHeight, lineYStart, hasNewline);
62 mLineDescs.push_back(lineDesc);
63
64 curCharIdx = lineDesc.getEndChar();
65 curLineIdx++;
66 }
67
68 bs_frame_delete(alignmentOffsets);
69 }
70 bs_frame_clear();
71 }
72
73 Vector2I GUIInputTool::getTextOffset() const
74 {
75 Vector2I offset(mElement->_getLayoutData().area.x, mElement->_getLayoutData().area.y);
76
77 return offset + mElement->_getTextInputOffset() + Vector2I(mElement->_getTextInputRect().x, mElement->_getTextInputRect().y);
78 }
79
80 Rect2I GUIInputTool::getCharRect(UINT32 charIdx) const
81 {
82 Rect2I charRect = getLocalCharRect(charIdx);
83 Vector2I textOffset = getTextOffset();
84
85 charRect.x += textOffset.x;
86 charRect.y += textOffset.y;
87
88 return charRect;
89 }
90
91 Rect2I GUIInputTool::getLocalCharRect(UINT32 charIdx) const
92 {
93 UINT32 lineIdx = getLineForChar(charIdx);
94
95 // If char is newline we don't have any geometry to return
96 const GUIInputLineDesc& lineDesc = getLineDesc(lineIdx);
97 if(lineDesc.isNewline(charIdx))
98 return Rect2I();
99
100 UINT32 numNewlineChars = 0;
101 for(UINT32 i = 0; i < lineIdx; i++)
102 numNewlineChars += (getLineDesc(i).hasNewlineChar() ? 1 : 0);
103
104 INT32 quadIdx = (INT32)(charIdx - numNewlineChars);
105 if(quadIdx >= 0 && quadIdx < (INT32)mNumQuads)
106 {
107 UINT32 vertIdx = quadIdx * 4;
108
109 Rect2I charRect;
110 charRect.x = Math::roundToInt(mQuads[vertIdx + 0].x);
111 charRect.y = Math::roundToInt(mQuads[vertIdx + 0].y);
112 charRect.width = Math::roundToInt(mQuads[vertIdx + 3].x - charRect.x);
113 charRect.height = Math::roundToInt(mQuads[vertIdx + 3].y - charRect.y);
114
115 return charRect;
116 }
117
118 LOGERR("Invalid character index: " + toString(charIdx));
119 return Rect2I();
120 }
121
122 INT32 GUIInputTool::getCharIdxAtPos(const Vector2I& pos) const
123 {
124 Vector2 vecPos((float)pos.x, (float)pos.y);
125
126 UINT32 lineStartChar = 0;
127 UINT32 lineEndChar = 0;
128 UINT32 numNewlineChars = 0;
129 UINT32 lineIdx = 0;
130 for(auto& line : mLineDescs)
131 {
132 INT32 lineStart = line.getLineYStart() + getTextOffset().y;
133 if(pos.y >= lineStart && pos.y < (lineStart + (INT32)line.getLineHeight()))
134 {
135 lineStartChar = line.getStartChar();
136 lineEndChar = line.getEndChar(false);
137 break;
138 }
139
140 // Newline chars count in the startChar/endChar variables, but don't actually exist in the buffers
141 // so we need to filter them out
142 numNewlineChars += (line.hasNewlineChar() ? 1 : 0);
143
144 lineIdx++;
145 }
146
147 UINT32 lineStartQuad = lineStartChar - numNewlineChars;
148 UINT32 lineEndQuad = lineEndChar - numNewlineChars;
149
150 float nearestDist = std::numeric_limits<float>::max();
151 UINT32 nearestChar = 0;
152 bool foundChar = false;
153
154 Vector2I textOffset = getTextOffset();
155 for(UINT32 i = lineStartQuad; i < lineEndQuad; i++)
156 {
157 UINT32 curVert = i * 4;
158
159 float centerX = mQuads[curVert + 0].x + mQuads[curVert + 1].x;
160 centerX *= 0.5f;
161 centerX += textOffset.x;
162
163 float dist = Math::abs(centerX - vecPos.x);
164 if(dist < nearestDist)
165 {
166 nearestChar = i + numNewlineChars;
167 nearestDist = dist;
168 foundChar = true;
169 }
170 }
171
172 if(!foundChar)
173 return -1;
174
175 return nearestChar;
176 }
177
178 UINT32 GUIInputTool::getLineForChar(UINT32 charIdx, bool newlineCountsOnNextLine) const
179 {
180 UINT32 idx = 0;
181 for(auto& line : mLineDescs)
182 {
183 if((charIdx >= line.getStartChar() && charIdx < line.getEndChar()) ||
184 (charIdx == line.getStartChar() && line.getStartChar() == line.getEndChar()))
185 {
186 if(line.isNewline(charIdx) && newlineCountsOnNextLine)
187 return idx + 1; // Incrementing is safe because next line must exist, since we just found a newline char
188
189 return idx;
190 }
191
192 idx++;
193 }
194
195 LOGERR("Invalid character index: " + toString(charIdx));
196 return 0;
197 }
198
199 UINT32 GUIInputTool::getCharIdxAtInputIdx(UINT32 inputIdx) const
200 {
201 if(mNumChars == 0)
202 return 0;
203
204 UINT32 numLines = getNumLines();
205 UINT32 curPos = 0;
206 UINT32 curCharIdx = 0;
207 for(UINT32 i = 0; i < numLines; i++)
208 {
209 const GUIInputLineDesc& lineDesc = getLineDesc(i);
210
211 if(curPos == inputIdx)
212 return lineDesc.getStartChar();
213
214 curPos++; // Move past line start position
215
216 UINT32 numChars = lineDesc.getEndChar() - lineDesc.getStartChar();
217 UINT32 numCaretPositions = lineDesc.getEndChar(false) - lineDesc.getStartChar();
218 if(inputIdx >= (curPos + numCaretPositions))
219 {
220 curCharIdx += numChars;
221 curPos += numCaretPositions;
222 continue;
223 }
224
225 UINT32 diff = inputIdx - curPos;
226 curCharIdx += diff + 1; // Character after the caret
227
228 return curCharIdx;
229 }
230
231 return 0;
232 }
233
234 bool GUIInputTool::isNewline(UINT32 inputIdx) const
235 {
236 if(mNumChars == 0)
237 return true;
238
239 UINT32 numLines = getNumLines();
240 UINT32 curPos = 0;
241 for(UINT32 i = 0; i < numLines; i++)
242 {
243 const GUIInputLineDesc& lineDesc = getLineDesc(i);
244
245 if(curPos == inputIdx)
246 return true;
247
248 UINT32 numChars = lineDesc.getEndChar(false) - lineDesc.getStartChar();
249 curPos += numChars;
250 }
251
252 return false;
253 }
254
255 bool GUIInputTool::isNewlineChar(UINT32 charIdx) const
256 {
257 UINT32 byteIdx = UTF8::charToByteIndex(mTextDesc.text, charIdx);
258
259 return mTextDesc.text[byteIdx] == '\n';
260 }
261
262 bool GUIInputTool::isDescValid() const
263 {
264 // We we have some text but line descs are empty we may assume
265 // something went wrong when creating the line descs, therefore it is
266 // not valid and no text is displayed.
267 if(mNumChars > 0)
268 return !mLineDescs.empty();
269
270 return true;
271 }
272
273 GUIInputLineDesc::GUIInputLineDesc(UINT32 startChar, UINT32 endChar, UINT32 lineHeight, INT32 lineYStart, bool includesNewline)
274 :mStartChar(startChar), mEndChar(endChar), mLineHeight(lineHeight), mLineYStart(lineYStart), mIncludesNewline(includesNewline)
275 {
276
277 }
278
279 UINT32 GUIInputLineDesc::getEndChar(bool includeNewline) const
280 {
281 if(mIncludesNewline)
282 {
283 if(includeNewline)
284 return mEndChar;
285 else
286 {
287 if(mEndChar > 0)
288 return mEndChar - 1;
289 else
290 return mStartChar;
291 }
292 }
293 else
294 return mEndChar;
295 }
296
297 bool GUIInputLineDesc::isNewline(UINT32 charIdx) const
298 {
299 if(mIncludesNewline)
300 {
301 return (mEndChar - 1) == charIdx;
302 }
303 else
304 return false;
305 }
306}
307