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 | |
10 | namespace 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 | |