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