1 | /* |
2 | src/textbox.cpp -- Fancy text box with builtin regular |
3 | expression-based validation |
4 | |
5 | The text box widget was contributed by Christian Schueller. |
6 | |
7 | NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>. |
8 | The widget drawing code is based on the NanoVG demo application |
9 | by Mikko Mononen. |
10 | |
11 | All rights reserved. Use of this source code is governed by a |
12 | BSD-style license that can be found in the LICENSE.txt file. |
13 | */ |
14 | |
15 | #include <nanogui/window.h> |
16 | #include <nanogui/screen.h> |
17 | #include <nanogui/textbox.h> |
18 | #include <nanogui/opengl.h> |
19 | #include <nanogui/theme.h> |
20 | #include <nanogui/serializer/core.h> |
21 | #include <regex> |
22 | #include <iostream> |
23 | |
24 | NAMESPACE_BEGIN(nanogui) |
25 | |
26 | TextBox::TextBox(Widget *parent,const std::string &value) |
27 | : Widget(parent), |
28 | mEditable(false), |
29 | mSpinnable(false), |
30 | mCommitted(true), |
31 | mValue(value), |
32 | mDefaultValue("" ), |
33 | mAlignment(Alignment::Center), |
34 | mUnits("" ), |
35 | mFormat("" ), |
36 | mUnitsImage(-1), |
37 | mValidFormat(true), |
38 | mValueTemp(value), |
39 | mCursorPos(-1), |
40 | mSelectionPos(-1), |
41 | mMousePos(Vector2i(-1,-1)), |
42 | mMouseDownPos(Vector2i(-1,-1)), |
43 | mMouseDragPos(Vector2i(-1,-1)), |
44 | mMouseDownModifier(0), |
45 | mTextOffset(0), |
46 | mLastClick(0) { |
47 | if (mTheme) mFontSize = mTheme->mTextBoxFontSize; |
48 | mIconExtraScale = 0.8f;// widget override |
49 | } |
50 | |
51 | void TextBox::setEditable(bool editable) { |
52 | mEditable = editable; |
53 | setCursor(editable ? Cursor::IBeam : Cursor::Arrow); |
54 | } |
55 | |
56 | void TextBox::setTheme(Theme *theme) { |
57 | Widget::setTheme(theme); |
58 | if (mTheme) |
59 | mFontSize = mTheme->mTextBoxFontSize; |
60 | } |
61 | |
62 | Vector2i TextBox::preferredSize(NVGcontext *ctx) const { |
63 | Vector2i size(0, fontSize() * 1.4f); |
64 | |
65 | float uw = 0; |
66 | if (mUnitsImage > 0) { |
67 | int w, h; |
68 | nvgImageSize(ctx, mUnitsImage, &w, &h); |
69 | float uh = size(1) * 0.4f; |
70 | uw = w * uh / h; |
71 | } else if (!mUnits.empty()) { |
72 | uw = nvgTextBounds(ctx, 0, 0, mUnits.c_str(), nullptr, nullptr); |
73 | } |
74 | float sw = 0; |
75 | if (mSpinnable) { |
76 | sw = 14.f; |
77 | } |
78 | |
79 | float ts = nvgTextBounds(ctx, 0, 0, mValue.c_str(), nullptr, nullptr); |
80 | size(0) = size(1) + ts + uw + sw; |
81 | return size; |
82 | } |
83 | |
84 | void TextBox::draw(NVGcontext* ctx) { |
85 | Widget::draw(ctx); |
86 | |
87 | NVGpaint bg = nvgBoxGradient(ctx, |
88 | mPos.x() + 1, mPos.y() + 1 + 1.0f, mSize.x() - 2, mSize.y() - 2, |
89 | 3, 4, Color(255, 32), Color(32, 32)); |
90 | NVGpaint fg1 = nvgBoxGradient(ctx, |
91 | mPos.x() + 1, mPos.y() + 1 + 1.0f, mSize.x() - 2, mSize.y() - 2, |
92 | 3, 4, Color(150, 32), Color(32, 32)); |
93 | NVGpaint fg2 = nvgBoxGradient(ctx, |
94 | mPos.x() + 1, mPos.y() + 1 + 1.0f, mSize.x() - 2, mSize.y() - 2, |
95 | 3, 4, nvgRGBA(255, 0, 0, 100), nvgRGBA(255, 0, 0, 50)); |
96 | |
97 | nvgBeginPath(ctx); |
98 | nvgRoundedRect(ctx, mPos.x() + 1, mPos.y() + 1 + 1.0f, mSize.x() - 2, |
99 | mSize.y() - 2, 3); |
100 | |
101 | if (mEditable && focused()) |
102 | mValidFormat ? nvgFillPaint(ctx, fg1) : nvgFillPaint(ctx, fg2); |
103 | else if (mSpinnable && mMouseDownPos.x() != -1) |
104 | nvgFillPaint(ctx, fg1); |
105 | else |
106 | nvgFillPaint(ctx, bg); |
107 | |
108 | nvgFill(ctx); |
109 | |
110 | nvgBeginPath(ctx); |
111 | nvgRoundedRect(ctx, mPos.x() + 0.5f, mPos.y() + 0.5f, mSize.x() - 1, |
112 | mSize.y() - 1, 2.5f); |
113 | nvgStrokeColor(ctx, Color(0, 48)); |
114 | nvgStroke(ctx); |
115 | |
116 | nvgFontSize(ctx, fontSize()); |
117 | nvgFontFace(ctx, "sans" ); |
118 | Vector2i drawPos(mPos.x(), mPos.y() + mSize.y() * 0.5f + 1); |
119 | |
120 | float xSpacing = mSize.y() * 0.3f; |
121 | |
122 | float unitWidth = 0; |
123 | |
124 | if (mUnitsImage > 0) { |
125 | int w, h; |
126 | nvgImageSize(ctx, mUnitsImage, &w, &h); |
127 | float unitHeight = mSize.y() * 0.4f; |
128 | unitWidth = w * unitHeight / h; |
129 | NVGpaint imgPaint = nvgImagePattern( |
130 | ctx, mPos.x() + mSize.x() - xSpacing - unitWidth, |
131 | drawPos.y() - unitHeight * 0.5f, unitWidth, unitHeight, 0, |
132 | mUnitsImage, mEnabled ? 0.7f : 0.35f); |
133 | nvgBeginPath(ctx); |
134 | nvgRect(ctx, mPos.x() + mSize.x() - xSpacing - unitWidth, |
135 | drawPos.y() - unitHeight * 0.5f, unitWidth, unitHeight); |
136 | nvgFillPaint(ctx, imgPaint); |
137 | nvgFill(ctx); |
138 | unitWidth += 2; |
139 | } else if (!mUnits.empty()) { |
140 | unitWidth = nvgTextBounds(ctx, 0, 0, mUnits.c_str(), nullptr, nullptr); |
141 | nvgFillColor(ctx, Color(255, mEnabled ? 64 : 32)); |
142 | nvgTextAlign(ctx, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE); |
143 | nvgText(ctx, mPos.x() + mSize.x() - xSpacing, drawPos.y(), |
144 | mUnits.c_str(), nullptr); |
145 | unitWidth += 2; |
146 | } |
147 | |
148 | float spinArrowsWidth = 0.f; |
149 | |
150 | if (mSpinnable && !focused()) { |
151 | spinArrowsWidth = 14.f; |
152 | |
153 | nvgFontFace(ctx, "icons" ); |
154 | nvgFontSize(ctx, ((mFontSize < 0) ? mTheme->mButtonFontSize : mFontSize) * icon_scale()); |
155 | |
156 | bool spinning = mMouseDownPos.x() != -1; |
157 | |
158 | /* up button */ { |
159 | bool hover = mMouseFocus && spinArea(mMousePos) == SpinArea::Top; |
160 | nvgFillColor(ctx, (mEnabled && (hover || spinning)) ? mTheme->mTextColor : mTheme->mDisabledTextColor); |
161 | auto icon = utf8(mTheme->mTextBoxUpIcon); |
162 | nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); |
163 | Vector2f iconPos(mPos.x() + 4.f, |
164 | mPos.y() + mSize.y()/2.f - xSpacing/2.f); |
165 | nvgText(ctx, iconPos.x(), iconPos.y(), icon.data(), nullptr); |
166 | } |
167 | |
168 | /* down button */ { |
169 | bool hover = mMouseFocus && spinArea(mMousePos) == SpinArea::Bottom; |
170 | nvgFillColor(ctx, (mEnabled && (hover || spinning)) ? mTheme->mTextColor : mTheme->mDisabledTextColor); |
171 | auto icon = utf8(mTheme->mTextBoxDownIcon); |
172 | nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); |
173 | Vector2f iconPos(mPos.x() + 4.f, |
174 | mPos.y() + mSize.y()/2.f + xSpacing/2.f + 1.5f); |
175 | nvgText(ctx, iconPos.x(), iconPos.y(), icon.data(), nullptr); |
176 | } |
177 | |
178 | nvgFontSize(ctx, fontSize()); |
179 | nvgFontFace(ctx, "sans" ); |
180 | } |
181 | |
182 | switch (mAlignment) { |
183 | case Alignment::Left: |
184 | nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); |
185 | drawPos.x() += xSpacing + spinArrowsWidth; |
186 | break; |
187 | case Alignment::Right: |
188 | nvgTextAlign(ctx, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE); |
189 | drawPos.x() += mSize.x() - unitWidth - xSpacing; |
190 | break; |
191 | case Alignment::Center: |
192 | nvgTextAlign(ctx, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); |
193 | drawPos.x() += mSize.x() * 0.5f; |
194 | break; |
195 | } |
196 | |
197 | nvgFontSize(ctx, fontSize()); |
198 | nvgFillColor(ctx, mEnabled && (!mCommitted || !mValue.empty()) ? |
199 | mTheme->mTextColor : |
200 | mTheme->mDisabledTextColor); |
201 | |
202 | // clip visible text area |
203 | float clipX = mPos.x() + xSpacing + spinArrowsWidth - 1.0f; |
204 | float clipY = mPos.y() + 1.0f; |
205 | float clipWidth = mSize.x() - unitWidth - spinArrowsWidth - 2 * xSpacing + 2.0f; |
206 | float clipHeight = mSize.y() - 3.0f; |
207 | |
208 | nvgSave(ctx); |
209 | nvgIntersectScissor(ctx, clipX, clipY, clipWidth, clipHeight); |
210 | |
211 | Vector2i oldDrawPos(drawPos); |
212 | drawPos.x() += mTextOffset; |
213 | |
214 | if (mCommitted) { |
215 | nvgText(ctx, drawPos.x(), drawPos.y(), |
216 | mValue.empty() ? mPlaceholder.c_str() : mValue.c_str(), nullptr); |
217 | } else { |
218 | const int maxGlyphs = 1024; |
219 | NVGglyphPosition glyphs[maxGlyphs]; |
220 | float textBound[4]; |
221 | nvgTextBounds(ctx, drawPos.x(), drawPos.y(), mValueTemp.c_str(), |
222 | nullptr, textBound); |
223 | float lineh = textBound[3] - textBound[1]; |
224 | |
225 | // find cursor positions |
226 | int nglyphs = |
227 | nvgTextGlyphPositions(ctx, drawPos.x(), drawPos.y(), |
228 | mValueTemp.c_str(), nullptr, glyphs, maxGlyphs); |
229 | updateCursor(ctx, textBound[2], glyphs, nglyphs); |
230 | |
231 | // compute text offset |
232 | int prevCPos = mCursorPos > 0 ? mCursorPos - 1 : 0; |
233 | int nextCPos = mCursorPos < nglyphs ? mCursorPos + 1 : nglyphs; |
234 | float prevCX = cursorIndex2Position(prevCPos, textBound[2], glyphs, nglyphs); |
235 | float nextCX = cursorIndex2Position(nextCPos, textBound[2], glyphs, nglyphs); |
236 | |
237 | if (nextCX > clipX + clipWidth) |
238 | mTextOffset -= nextCX - (clipX + clipWidth) + 1; |
239 | if (prevCX < clipX) |
240 | mTextOffset += clipX - prevCX + 1; |
241 | |
242 | drawPos.x() = oldDrawPos.x() + mTextOffset; |
243 | |
244 | // draw text with offset |
245 | nvgText(ctx, drawPos.x(), drawPos.y(), mValueTemp.c_str(), nullptr); |
246 | nvgTextBounds(ctx, drawPos.x(), drawPos.y(), mValueTemp.c_str(), |
247 | nullptr, textBound); |
248 | |
249 | // recompute cursor positions |
250 | nglyphs = nvgTextGlyphPositions(ctx, drawPos.x(), drawPos.y(), |
251 | mValueTemp.c_str(), nullptr, glyphs, maxGlyphs); |
252 | |
253 | if (mCursorPos > -1) { |
254 | if (mSelectionPos > -1) { |
255 | float caretx = cursorIndex2Position(mCursorPos, textBound[2], |
256 | glyphs, nglyphs); |
257 | float selx = cursorIndex2Position(mSelectionPos, textBound[2], |
258 | glyphs, nglyphs); |
259 | |
260 | if (caretx > selx) |
261 | std::swap(caretx, selx); |
262 | |
263 | // draw selection |
264 | nvgBeginPath(ctx); |
265 | nvgFillColor(ctx, nvgRGBA(255, 255, 255, 80)); |
266 | nvgRect(ctx, caretx, drawPos.y() - lineh * 0.5f, selx - caretx, |
267 | lineh); |
268 | nvgFill(ctx); |
269 | } |
270 | |
271 | float caretx = cursorIndex2Position(mCursorPos, textBound[2], glyphs, nglyphs); |
272 | |
273 | // draw cursor |
274 | nvgBeginPath(ctx); |
275 | nvgMoveTo(ctx, caretx, drawPos.y() - lineh * 0.5f); |
276 | nvgLineTo(ctx, caretx, drawPos.y() + lineh * 0.5f); |
277 | nvgStrokeColor(ctx, nvgRGBA(255, 192, 0, 255)); |
278 | nvgStrokeWidth(ctx, 1.0f); |
279 | nvgStroke(ctx); |
280 | } |
281 | } |
282 | nvgRestore(ctx); |
283 | } |
284 | |
285 | bool TextBox::mouseButtonEvent(const Vector2i &p, int button, bool down, |
286 | int modifiers) { |
287 | |
288 | if (button == GLFW_MOUSE_BUTTON_1 && down && !mFocused) { |
289 | if (!mSpinnable || spinArea(p) == SpinArea::None) /* not on scrolling arrows */ |
290 | requestFocus(); |
291 | } |
292 | |
293 | if (mEditable && focused()) { |
294 | if (down) { |
295 | mMouseDownPos = p; |
296 | mMouseDownModifier = modifiers; |
297 | |
298 | double time = glfwGetTime(); |
299 | if (time - mLastClick < 0.25) { |
300 | /* Double-click: select all text */ |
301 | mSelectionPos = 0; |
302 | mCursorPos = (int) mValueTemp.size(); |
303 | mMouseDownPos = Vector2i(-1, -1); |
304 | } |
305 | mLastClick = time; |
306 | } else { |
307 | mMouseDownPos = Vector2i(-1, -1); |
308 | mMouseDragPos = Vector2i(-1, -1); |
309 | } |
310 | return true; |
311 | } else if (mSpinnable && !focused()) { |
312 | if (down) { |
313 | if (spinArea(p) == SpinArea::None) { |
314 | mMouseDownPos = p; |
315 | mMouseDownModifier = modifiers; |
316 | |
317 | double time = glfwGetTime(); |
318 | if (time - mLastClick < 0.25) { |
319 | /* Double-click: reset to default value */ |
320 | mValue = mDefaultValue; |
321 | if (mCallback) |
322 | mCallback(mValue); |
323 | |
324 | mMouseDownPos = Vector2i(-1, -1); |
325 | } |
326 | mLastClick = time; |
327 | } else { |
328 | mMouseDownPos = Vector2i(-1, -1); |
329 | mMouseDragPos = Vector2i(-1, -1); |
330 | } |
331 | } else { |
332 | mMouseDownPos = Vector2i(-1, -1); |
333 | mMouseDragPos = Vector2i(-1, -1); |
334 | } |
335 | return true; |
336 | } |
337 | |
338 | return false; |
339 | } |
340 | |
341 | bool TextBox::mouseMotionEvent(const Vector2i &p, const Vector2i & /* rel */, |
342 | int /* button */, int /* modifiers */) { |
343 | mMousePos = p; |
344 | |
345 | if (!mEditable) |
346 | setCursor(Cursor::Arrow); |
347 | else if (mSpinnable && !focused() && spinArea(mMousePos) != SpinArea::None) /* scrolling arrows */ |
348 | setCursor(Cursor::Hand); |
349 | else |
350 | setCursor(Cursor::IBeam); |
351 | |
352 | if (mEditable && focused()) { |
353 | return true; |
354 | } |
355 | return false; |
356 | } |
357 | |
358 | bool TextBox::mouseDragEvent(const Vector2i &p, const Vector2i &/* rel */, |
359 | int /* button */, int /* modifiers */) { |
360 | mMousePos = p; |
361 | mMouseDragPos = p; |
362 | |
363 | if (mEditable && focused()) { |
364 | return true; |
365 | } |
366 | return false; |
367 | } |
368 | |
369 | bool TextBox::focusEvent(bool focused) { |
370 | Widget::focusEvent(focused); |
371 | |
372 | std::string backup = mValue; |
373 | |
374 | if (mEditable) { |
375 | if (focused) { |
376 | mValueTemp = mValue; |
377 | mCommitted = false; |
378 | mCursorPos = 0; |
379 | } else { |
380 | if (mValidFormat) { |
381 | if (mValueTemp == "" ) |
382 | mValue = mDefaultValue; |
383 | else |
384 | mValue = mValueTemp; |
385 | } |
386 | |
387 | if (mCallback && !mCallback(mValue)) |
388 | mValue = backup; |
389 | |
390 | mValidFormat = true; |
391 | mCommitted = true; |
392 | mCursorPos = -1; |
393 | mSelectionPos = -1; |
394 | mTextOffset = 0; |
395 | } |
396 | |
397 | mValidFormat = (mValueTemp == "" ) || checkFormat(mValueTemp, mFormat); |
398 | } |
399 | |
400 | return true; |
401 | } |
402 | |
403 | bool TextBox::keyboardEvent(int key, int /* scancode */, int action, int modifiers) { |
404 | if (mEditable && focused()) { |
405 | if (action == GLFW_PRESS || action == GLFW_REPEAT) { |
406 | if (key == GLFW_KEY_LEFT) { |
407 | if (modifiers == GLFW_MOD_SHIFT) { |
408 | if (mSelectionPos == -1) |
409 | mSelectionPos = mCursorPos; |
410 | } else { |
411 | mSelectionPos = -1; |
412 | } |
413 | |
414 | if (mCursorPos > 0) |
415 | mCursorPos--; |
416 | } else if (key == GLFW_KEY_RIGHT) { |
417 | if (modifiers == GLFW_MOD_SHIFT) { |
418 | if (mSelectionPos == -1) |
419 | mSelectionPos = mCursorPos; |
420 | } else { |
421 | mSelectionPos = -1; |
422 | } |
423 | |
424 | if (mCursorPos < (int) mValueTemp.length()) |
425 | mCursorPos++; |
426 | } else if (key == GLFW_KEY_HOME) { |
427 | if (modifiers == GLFW_MOD_SHIFT) { |
428 | if (mSelectionPos == -1) |
429 | mSelectionPos = mCursorPos; |
430 | } else { |
431 | mSelectionPos = -1; |
432 | } |
433 | |
434 | mCursorPos = 0; |
435 | } else if (key == GLFW_KEY_END) { |
436 | if (modifiers == GLFW_MOD_SHIFT) { |
437 | if (mSelectionPos == -1) |
438 | mSelectionPos = mCursorPos; |
439 | } else { |
440 | mSelectionPos = -1; |
441 | } |
442 | |
443 | mCursorPos = (int) mValueTemp.size(); |
444 | } else if (key == GLFW_KEY_BACKSPACE) { |
445 | if (!deleteSelection()) { |
446 | if (mCursorPos > 0) { |
447 | mValueTemp.erase(mValueTemp.begin() + mCursorPos - 1); |
448 | mCursorPos--; |
449 | } |
450 | } |
451 | } else if (key == GLFW_KEY_DELETE) { |
452 | if (!deleteSelection()) { |
453 | if (mCursorPos < (int) mValueTemp.length()) |
454 | mValueTemp.erase(mValueTemp.begin() + mCursorPos); |
455 | } |
456 | } else if (key == GLFW_KEY_ENTER) { |
457 | if (!mCommitted) |
458 | focusEvent(false); |
459 | } else if (key == GLFW_KEY_A && modifiers == SYSTEM_COMMAND_MOD) { |
460 | mCursorPos = (int) mValueTemp.length(); |
461 | mSelectionPos = 0; |
462 | } else if (key == GLFW_KEY_X && modifiers == SYSTEM_COMMAND_MOD) { |
463 | copySelection(); |
464 | deleteSelection(); |
465 | } else if (key == GLFW_KEY_C && modifiers == SYSTEM_COMMAND_MOD) { |
466 | copySelection(); |
467 | } else if (key == GLFW_KEY_V && modifiers == SYSTEM_COMMAND_MOD) { |
468 | deleteSelection(); |
469 | pasteFromClipboard(); |
470 | } |
471 | |
472 | mValidFormat = |
473 | (mValueTemp == "" ) || checkFormat(mValueTemp, mFormat); |
474 | } |
475 | |
476 | return true; |
477 | } |
478 | |
479 | return false; |
480 | } |
481 | |
482 | bool TextBox::keyboardCharacterEvent(unsigned int codepoint) { |
483 | if (mEditable && focused()) { |
484 | std::ostringstream convert; |
485 | convert << (char) codepoint; |
486 | |
487 | deleteSelection(); |
488 | mValueTemp.insert(mCursorPos, convert.str()); |
489 | mCursorPos++; |
490 | |
491 | mValidFormat = (mValueTemp == "" ) || checkFormat(mValueTemp, mFormat); |
492 | |
493 | return true; |
494 | } |
495 | |
496 | return false; |
497 | } |
498 | |
499 | bool TextBox::checkFormat(const std::string &input, const std::string &format) { |
500 | if (format.empty()) |
501 | return true; |
502 | try { |
503 | std::regex regex(format); |
504 | return regex_match(input, regex); |
505 | } catch (const std::regex_error &) { |
506 | #if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 9) |
507 | std::cerr << "Warning: cannot validate text field due to lacking regular expression support. please compile with GCC >= 4.9" << std::endl; |
508 | return true; |
509 | #else |
510 | throw; |
511 | #endif |
512 | } |
513 | } |
514 | |
515 | bool TextBox::copySelection() { |
516 | if (mSelectionPos > -1) { |
517 | Screen *sc = dynamic_cast<Screen *>(this->window()->parent()); |
518 | if (!sc) |
519 | return false; |
520 | |
521 | int begin = mCursorPos; |
522 | int end = mSelectionPos; |
523 | |
524 | if (begin > end) |
525 | std::swap(begin, end); |
526 | |
527 | glfwSetClipboardString(sc->glfwWindow(), |
528 | mValueTemp.substr(begin, end).c_str()); |
529 | return true; |
530 | } |
531 | |
532 | return false; |
533 | } |
534 | |
535 | void TextBox::pasteFromClipboard() { |
536 | Screen *sc = dynamic_cast<Screen *>(this->window()->parent()); |
537 | if (!sc) |
538 | return; |
539 | const char* cbstr = glfwGetClipboardString(sc->glfwWindow()); |
540 | if (cbstr) |
541 | mValueTemp.insert(mCursorPos, std::string(cbstr)); |
542 | } |
543 | |
544 | bool TextBox::deleteSelection() { |
545 | if (mSelectionPos > -1) { |
546 | int begin = mCursorPos; |
547 | int end = mSelectionPos; |
548 | |
549 | if (begin > end) |
550 | std::swap(begin, end); |
551 | |
552 | if (begin == end - 1) |
553 | mValueTemp.erase(mValueTemp.begin() + begin); |
554 | else |
555 | mValueTemp.erase(mValueTemp.begin() + begin, |
556 | mValueTemp.begin() + end); |
557 | |
558 | mCursorPos = begin; |
559 | mSelectionPos = -1; |
560 | return true; |
561 | } |
562 | |
563 | return false; |
564 | } |
565 | |
566 | void TextBox::updateCursor(NVGcontext *, float lastx, |
567 | const NVGglyphPosition *glyphs, int size) { |
568 | // handle mouse cursor events |
569 | if (mMouseDownPos.x() != -1) { |
570 | if (mMouseDownModifier == GLFW_MOD_SHIFT) { |
571 | if (mSelectionPos == -1) |
572 | mSelectionPos = mCursorPos; |
573 | } else |
574 | mSelectionPos = -1; |
575 | |
576 | mCursorPos = |
577 | position2CursorIndex(mMouseDownPos.x(), lastx, glyphs, size); |
578 | |
579 | mMouseDownPos = Vector2i(-1, -1); |
580 | } else if (mMouseDragPos.x() != -1) { |
581 | if (mSelectionPos == -1) |
582 | mSelectionPos = mCursorPos; |
583 | |
584 | mCursorPos = |
585 | position2CursorIndex(mMouseDragPos.x(), lastx, glyphs, size); |
586 | } else { |
587 | // set cursor to last character |
588 | if (mCursorPos == -2) |
589 | mCursorPos = size; |
590 | } |
591 | |
592 | if (mCursorPos == mSelectionPos) |
593 | mSelectionPos = -1; |
594 | } |
595 | |
596 | float TextBox::cursorIndex2Position(int index, float lastx, |
597 | const NVGglyphPosition *glyphs, int size) { |
598 | float pos = 0; |
599 | if (index == size) |
600 | pos = lastx; // last character |
601 | else |
602 | pos = glyphs[index].x; |
603 | |
604 | return pos; |
605 | } |
606 | |
607 | int TextBox::position2CursorIndex(float posx, float lastx, |
608 | const NVGglyphPosition *glyphs, int size) { |
609 | int mCursorId = 0; |
610 | float caretx = glyphs[mCursorId].x; |
611 | for (int j = 1; j < size; j++) { |
612 | if (std::abs(caretx - posx) > std::abs(glyphs[j].x - posx)) { |
613 | mCursorId = j; |
614 | caretx = glyphs[mCursorId].x; |
615 | } |
616 | } |
617 | if (std::abs(caretx - posx) > std::abs(lastx - posx)) |
618 | mCursorId = size; |
619 | |
620 | return mCursorId; |
621 | } |
622 | |
623 | TextBox::SpinArea TextBox::spinArea(const Vector2i & pos) { |
624 | if (0 <= pos.x() - mPos.x() && pos.x() - mPos.x() < 14.f) { /* on scrolling arrows */ |
625 | if (mSize.y() >= pos.y() - mPos.y() && pos.y() - mPos.y() <= mSize.y() / 2.f) { /* top part */ |
626 | return SpinArea::Top; |
627 | } else if (0.f <= pos.y() - mPos.y() && pos.y() - mPos.y() > mSize.y() / 2.f) { /* bottom part */ |
628 | return SpinArea::Bottom; |
629 | } |
630 | } |
631 | return SpinArea::None; |
632 | } |
633 | |
634 | void TextBox::save(Serializer &s) const { |
635 | Widget::save(s); |
636 | s.set("editable" , mEditable); |
637 | s.set("spinnable" , mSpinnable); |
638 | s.set("committed" , mCommitted); |
639 | s.set("value" , mValue); |
640 | s.set("defaultValue" , mDefaultValue); |
641 | s.set("alignment" , (int) mAlignment); |
642 | s.set("units" , mUnits); |
643 | s.set("format" , mFormat); |
644 | s.set("unitsImage" , mUnitsImage); |
645 | s.set("validFormat" , mValidFormat); |
646 | s.set("valueTemp" , mValueTemp); |
647 | s.set("cursorPos" , mCursorPos); |
648 | s.set("selectionPos" , mSelectionPos); |
649 | } |
650 | |
651 | bool TextBox::load(Serializer &s) { |
652 | if (!Widget::load(s)) return false; |
653 | if (!s.get("editable" , mEditable)) return false; |
654 | if (!s.get("spinnable" , mSpinnable)) return false; |
655 | if (!s.get("committed" , mCommitted)) return false; |
656 | if (!s.get("value" , mValue)) return false; |
657 | if (!s.get("defaultValue" , mDefaultValue)) return false; |
658 | if (!s.get("alignment" , mAlignment)) return false; |
659 | if (!s.get("units" , mUnits)) return false; |
660 | if (!s.get("format" , mFormat)) return false; |
661 | if (!s.get("unitsImage" , mUnitsImage)) return false; |
662 | if (!s.get("validFormat" , mValidFormat)) return false; |
663 | if (!s.get("valueTemp" , mValueTemp)) return false; |
664 | if (!s.get("cursorPos" , mCursorPos)) return false; |
665 | if (!s.get("selectionPos" , mSelectionPos)) return false; |
666 | mMousePos = mMouseDownPos = mMouseDragPos = Vector2i::Constant(-1); |
667 | mMouseDownModifier = mTextOffset = 0; |
668 | return true; |
669 | } |
670 | |
671 | NAMESPACE_END(nanogui) |
672 | |