1 /// 2 module nanogui.textbox; 3 4 /* 5 nanogui.textbox.d -- Fancy text box with builtin regular 6 expression-based validation 7 8 The text box widget was contributed by Christian Schueller. 9 10 NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>. 11 The widget drawing code is based on the NanoVG demo application 12 by Mikko Mononen. 13 14 All rights reserved. Use of this source code is governed by a 15 BSD-style license that can be found in the LICENSE.txt file. 16 */ 17 18 import std.array : replaceInPlace; 19 import std.algorithm : swap; 20 import std.math : abs; 21 22 import arsd.nanovega; 23 import nanogui.widget : Widget; 24 import nanogui.theme : Theme; 25 import nanogui.common : Vector2i, MouseAction, MouseButton, Cursor, 26 Color, boxGradient, fillColor, Vector2f, Key, KeyAction, KeyMod; 27 28 private auto squeezeGlyphs(T)(T[] glyphs_buffer, T[] glyphs) 29 { 30 import std.algorithm : uniq; 31 size_t i; 32 foreach(e; glyphs.uniq!"a.x==b.x") 33 glyphs_buffer[i++] = e; 34 35 return glyphs_buffer[0..i]; 36 } 37 38 /** 39 * Fancy text box with builtin regular expression-based validation. 40 * 41 * Remark: 42 * This class overrides `nanogui.Widget.mIconExtraScale` to be `0.8f`, 43 * which affects all subclasses of this Widget. Subclasses must explicitly 44 * set a different value if needed (e.g., in their constructor). 45 */ 46 class TextBox : Widget 47 { 48 public: 49 /// How to align the text in the text box. 50 enum Alignment { 51 Left, 52 Center, 53 Right 54 }; 55 56 this(Widget parent, string value = "Untitled") 57 { 58 super(parent); 59 mEditable = false; 60 mSpinnable = false; 61 mCommitted = true; 62 mValue = value; 63 mDefaultValue = ""; 64 mAlignment = Alignment.Center; 65 mUnits = ""; 66 mFormat = ""; 67 mUnitsImage = NVGImage(); 68 mValidFormat = true; 69 mValueTemp = value; 70 mCursorPos = -1; 71 mSelectionPos = -1; 72 mMousePos = Vector2i(-1,-1); 73 mMouseDownPos = Vector2i(-1,-1); 74 mMouseDragPos = Vector2i(-1,-1); 75 mMouseDownModifier = 0; 76 mTextOffset = 0; 77 mLastClick = 0; 78 if (mTheme) mFontSize = mTheme.mTextBoxFontSize; 79 mIconExtraScale = 0.8f;// widget override 80 } 81 82 bool editable() const { return mEditable; } 83 final void editable(bool editable) 84 { 85 mEditable = editable; 86 cursor = editable ? Cursor.IBeam : Cursor.Arrow; 87 } 88 89 final bool spinnable() const { return mSpinnable; } 90 final void spinnable(bool spinnable) { mSpinnable = spinnable; } 91 92 final string value() const { return mValue; } 93 final void value(string value) { mValue = value; } 94 95 final string defaultValue() const { return mDefaultValue; } 96 final void defaultValue(string defaultValue) { mDefaultValue = defaultValue; } 97 98 final Alignment alignment() const { return mAlignment; } 99 final void alignment(Alignment al) { mAlignment = al; } 100 101 final string units() const { return mUnits; } 102 final void units(string units) { mUnits = units; } 103 104 final auto unitsImage() const { return mUnitsImage; } 105 final void unitsImage(NVGImage image) { mUnitsImage = image; } 106 107 /// Return the underlying regular expression specifying valid formats 108 final string format() const { return mFormat; } 109 /// Specify a regular expression specifying valid formats 110 final void format(string format) { mFormat = format; } 111 112 /// Return the placeholder text to be displayed while the text box is empty. 113 final string placeholder() const { return mPlaceholder; } 114 /// Specify a placeholder text to be displayed while the text box is empty. 115 final void placeholder(string placeholder) { mPlaceholder = placeholder; } 116 117 /// Set the `Theme` used to draw this widget 118 override void theme(Theme theme) 119 { 120 Widget.theme(theme); 121 if (mTheme) 122 mFontSize = mTheme.mTextBoxFontSize; 123 } 124 125 /// The callback to execute when the value of this TextBox has changed. 126 final bool delegate(string str) callback() const { return mCallback; } 127 128 /// Sets the callback to execute when the value of this TextBox has changed. 129 final void callback(bool delegate(string str) callback) { mCallback = callback; } 130 131 override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers) 132 { 133 if (button == MouseButton.Left && down && !mFocused) 134 { 135 if (!mSpinnable || spinArea(p) == SpinArea.None) /* not on scrolling arrows */ 136 requestFocus(); 137 } 138 139 if (mEditable && focused) 140 { 141 if (down) 142 { 143 mMouseDownPos = p; 144 mMouseDownModifier = modifiers; 145 146 // double time = glfwGetTime(); 147 // if (time - mLastClick < 0.25) { 148 // /* Double-click: select all text */ 149 // mSelectionPos = 0; 150 // mCursorPos = (int) mValueTemp.size(); 151 // mMouseDownPos = Vector2i(-1, -1); 152 // } 153 // mLastClick = time; 154 } 155 else 156 { 157 mMouseDownPos = Vector2i(-1, -1); 158 mMouseDragPos = Vector2i(-1, -1); 159 } 160 return true; 161 } 162 else if (mSpinnable && !focused) 163 { 164 if (down) 165 { 166 if (spinArea(p) == SpinArea.None) 167 { 168 mMouseDownPos = p; 169 mMouseDownModifier = modifiers; 170 171 // double time = glfwGetTime(); 172 // if (time - mLastClick < 0.25) { 173 // /* Double-click: reset to default value */ 174 // mValue = mDefaultValue; 175 // if (mCallback) 176 // mCallback(mValue); 177 178 // mMouseDownPos = Vector2i(-1, -1); 179 // } 180 // mLastClick = time; 181 } 182 else 183 { 184 mMouseDownPos = Vector2i(-1, -1); 185 mMouseDragPos = Vector2i(-1, -1); 186 } 187 } 188 else 189 { 190 mMouseDownPos = Vector2i(-1, -1); 191 mMouseDragPos = Vector2i(-1, -1); 192 } 193 return true; 194 } 195 196 return false; 197 } 198 199 override bool mouseMotionEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers) 200 { 201 mMousePos = p; 202 203 if (!mEditable) 204 cursor = Cursor.Arrow; 205 else if (mSpinnable && !focused && spinArea(mMousePos) != SpinArea.None) /* scrolling arrows */ 206 cursor = Cursor.Hand; 207 else 208 cursor = Cursor.IBeam; 209 210 if (mEditable && focused) 211 { 212 return true; 213 } 214 return false; 215 } 216 217 override bool mouseDragEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers) 218 { 219 mMousePos = p; 220 mMouseDragPos = p; 221 222 if (mEditable && focused) 223 return true; 224 225 return false; 226 } 227 228 override bool focusEvent(bool focused) 229 { 230 super.focusEvent(focused); 231 232 string backup = mValue; 233 234 if (mEditable) 235 { 236 if (focused) 237 { 238 mValueTemp = mValue; 239 mCommitted = false; 240 mCursorPos = 0; 241 } 242 else 243 { 244 if (mValidFormat) 245 { 246 if (mValueTemp == "") 247 mValue = mDefaultValue; 248 else 249 mValue = mValueTemp; 250 } 251 252 if (mCallback && !mCallback(mValue)) 253 mValue = backup; 254 255 mValidFormat = true; 256 mCommitted = true; 257 mCursorPos = -1; 258 mSelectionPos = -1; 259 mTextOffset = 0; 260 } 261 262 mValidFormat = (mValueTemp == "") || checkFormat(mValueTemp, mFormat); 263 } 264 265 return true; 266 } 267 268 override bool keyboardEvent(int key, int scancode, KeyAction action, int modifiers) 269 { 270 if (mEditable && focused) 271 { 272 if (action == KeyAction.Press || action == KeyAction.Repeat) 273 { 274 import std.uni : byGrapheme; 275 import std.range : walkLength; 276 277 if (key == Key.Left) 278 { 279 if (modifiers == KeyMod.Shift) 280 { 281 if (mSelectionPos == -1) 282 mSelectionPos = mCursorPos; 283 } 284 else 285 mSelectionPos = -1; 286 287 if (mCursorPos > 0) 288 mCursorPos--; 289 } 290 else if (key == Key.Right) 291 { 292 if (modifiers == KeyMod.Shift) 293 { 294 if (mSelectionPos == -1) 295 mSelectionPos = mCursorPos; 296 } 297 else 298 mSelectionPos = -1; 299 300 if (mCursorPos < cast(int) mValueTemp.byGrapheme.walkLength) 301 mCursorPos++; 302 } 303 else if (key == Key.Home) 304 { 305 if (modifiers == KeyMod.Shift) 306 { 307 if (mSelectionPos == -1) 308 mSelectionPos = mCursorPos; 309 } 310 else 311 mSelectionPos = -1; 312 313 mCursorPos = 0; 314 } 315 else if (key == Key.End) 316 { 317 if (modifiers == KeyMod.Shift) 318 { 319 if (mSelectionPos == -1) 320 mSelectionPos = mCursorPos; 321 } 322 else 323 mSelectionPos = -1; 324 325 mCursorPos = cast(int) mValueTemp.byGrapheme.walkLength; 326 } 327 else if (key == Key.Backspace) 328 { 329 if (!deleteSelection()) 330 { 331 if (mCursorPos > 0) 332 { 333 auto begin = symbolLengthToBytes(mValueTemp, mCursorPos - 1); 334 auto end = symbolLengthToBytes(mValueTemp, mCursorPos); 335 mValueTemp.replaceInPlace(begin, end, (char[]).init); 336 mCursorPos--; 337 } 338 } 339 } 340 else if (key == Key.Delete) 341 { 342 if (!deleteSelection()) 343 { 344 if (mCursorPos < cast(int) mValueTemp.byGrapheme.walkLength) 345 { 346 auto begin = symbolLengthToBytes(mValueTemp, mCursorPos); 347 auto end = symbolLengthToBytes(mValueTemp, mCursorPos+1); 348 mValueTemp.replaceInPlace(begin, end, (char[]).init); 349 } 350 } 351 } 352 else if (key == Key.Enter) 353 { 354 if (!mCommitted) 355 focusEvent(false); 356 } 357 else if (key == Key.A && modifiers == KeyMod.Ctrl) 358 { 359 mCursorPos = cast(int) mValueTemp.byGrapheme.walkLength; 360 mSelectionPos = 0; 361 } 362 else if (key == Key.X && modifiers == KeyMod.Ctrl) 363 { 364 copySelection(); 365 deleteSelection(); 366 } 367 else if (key == Key.C && modifiers == KeyMod.Ctrl) 368 { 369 copySelection(); 370 } 371 else if (key == Key.V && modifiers == KeyMod.Ctrl) 372 { 373 deleteSelection(); 374 pasteFromClipboard(); 375 } 376 377 mValidFormat = 378 (mValueTemp == "") || checkFormat(mValueTemp, mFormat); 379 } 380 381 return true; 382 } 383 384 return false; 385 } 386 387 /// converts length in symbols to length in bytes 388 private auto symbolLengthToBytes(string txt, size_t count) 389 { 390 import std.uni : graphemeStride; 391 size_t pos; // current position in string 392 size_t total_len; // length of pos symbols in bytes 393 while(total_len != txt.length && pos < count) 394 { 395 assert(total_len <= txt.length); 396 // get length of the current graphem in bytes 397 auto len = graphemeStride(txt, total_len); 398 total_len += len; 399 pos++; 400 } 401 return total_len; 402 } 403 404 override bool keyboardCharacterEvent(dchar codepoint) 405 { 406 if (mEditable && focused) 407 { 408 deleteSelection(); 409 import std.conv : to; 410 auto replacement = codepoint.to!string; 411 auto pos = symbolLengthToBytes(mValueTemp, mCursorPos); 412 mValueTemp = mValueTemp[0..pos] ~ 413 replacement ~ 414 mValueTemp[pos..$]; 415 mCursorPos++; 416 417 mValidFormat = (mValueTemp == "") || checkFormat(mValueTemp, mFormat); 418 419 return true; 420 } 421 422 return false; 423 } 424 425 override Vector2i preferredSize(NVGContext nvg) const 426 { 427 Vector2i size = Vector2i(0, cast(int) (fontSize * 1.4f)); 428 429 float uw = 0; 430 if (mUnitsImage.valid) 431 { 432 int w, h; 433 nvg.imageSize(mUnitsImage, w, h); 434 float uh = size[1] * 0.4f; 435 uw = w * uh / h; 436 } else if (mUnits.length) 437 { 438 uw = nvg.textBounds(0, 0, mUnits, null); 439 } 440 float sw = 0; 441 if (mSpinnable) 442 sw = 14.0f; 443 444 float ts = nvg.textBounds(0, 0, mValue, null); 445 size[0] = size[1] + cast(int)(ts + uw + sw); 446 return size; 447 } 448 449 override void draw(NVGContext nvg) 450 { 451 super.draw(nvg); 452 453 NVGPaint bg = nvg.boxGradient( 454 mPos.x + 1, mPos.y + 1 + 1.0f, mSize.x - 2, mSize.y - 2, 455 3, 4, Color(255, 255, 255, 32), Color(32, 32, 32, 32)); 456 NVGPaint fg1 = nvg.boxGradient( 457 mPos.x + 1, mPos.y + 1 + 1.0f, mSize.x - 2, mSize.y - 2, 458 3, 4, Color(150, 150, 150, 32), Color(32, 32, 32, 32)); 459 NVGPaint fg2 = nvg.boxGradient( 460 mPos.x + 1, mPos.y + 1 + 1.0f, mSize.x - 2, mSize.y - 2, 461 3, 4, Color(255, 0, 0, 100), Color(255, 0, 0, 50)); 462 463 nvg.beginPath; 464 nvg.roundedRect(mPos.x + 1, mPos.y + 1 + 1.0f, mSize.x - 2, 465 mSize.y - 2, 3); 466 467 if (mEditable && focused) 468 mValidFormat ? nvg.fillPaint(fg1) : nvg.fillPaint(fg2); 469 else if (mSpinnable && mMouseDownPos.x != -1) 470 nvg.fillPaint(fg1); 471 else 472 nvg.fillPaint(bg); 473 474 nvg.fill; 475 476 nvg.beginPath; 477 nvg.roundedRect(mPos.x + 0.5f, mPos.y + 0.5f, mSize.x - 1, 478 mSize.y - 1, 2.5f); 479 nvg.strokeColor(NVGColor(0, 0, 0, 48)); 480 nvg.stroke; 481 482 nvg.fontSize(fontSize()); 483 if (mTheme !is null) 484 nvg.fontFaceId(mTheme.mFontNormal); 485 else 486 nvg.fontFace("sans"); 487 488 auto draw_pos = Vector2i(mPos.x, cast(int) (mPos.y + mSize.y * 0.5f + 1)); 489 490 float xSpacing = mSize.y * 0.3f; 491 492 float unitWidth = 0; 493 494 if (mUnitsImage.valid) 495 { 496 int w, h; 497 nvg.imageSize(mUnitsImage, w, h); 498 float unitHeight = mSize.y * 0.4f; 499 unitWidth = w * unitHeight / h; 500 NVGPaint imgPaint = nvg.imagePattern( 501 mPos.x + mSize.x - xSpacing - unitWidth, 502 draw_pos.y - unitHeight * 0.5f, unitWidth, unitHeight, 0, 503 mUnitsImage, mEnabled ? 0.7f : 0.35f); 504 nvg.beginPath; 505 nvg.rect(mPos.x + mSize.x - xSpacing - unitWidth, 506 draw_pos.y - unitHeight * 0.5f, unitWidth, unitHeight); 507 nvg.fillPaint(imgPaint); 508 nvg.fill; 509 unitWidth += 2; 510 } 511 else if (mUnits.length) 512 { 513 unitWidth = nvg.textBounds(0, 0, mUnits, null); 514 nvg.fillColor(Color(255, 255, 255, mEnabled ? 64 : 32)); 515 NVGTextAlign algn; 516 algn.right = true; 517 algn.middle = true; 518 nvg.textAlign(algn); 519 nvg.text(mPos.x + mSize.x - xSpacing, draw_pos.y, 520 mUnits); 521 unitWidth += 2; 522 } 523 524 float spinArrowsWidth = 0.0f; 525 526 if (mSpinnable && !focused()) { 527 spinArrowsWidth = 14.0f; 528 529 nvg.fontFace("icons"); 530 nvg.fontSize(((mFontSize < 0) ? mTheme.mButtonFontSize : mFontSize) * icon_scale()); 531 532 bool spinning = mMouseDownPos.x != -1; 533 534 /* up button */ { 535 bool hover = mMouseFocus && spinArea(mMousePos) == SpinArea.Top; 536 nvg.fillColor((mEnabled && (hover || spinning)) ? mTheme.mTextColor : mTheme.mDisabledTextColor); 537 auto icon = mTheme.mTextBoxUpIcon; 538 NVGTextAlign algn; 539 algn.left = true; 540 algn.middle = true; 541 nvg.textAlign(algn); 542 auto iconPos = Vector2f(mPos.x + 4.0f, 543 mPos.y + mSize.y/2.0f - xSpacing/2.0f); 544 nvg.text(iconPos.x, iconPos.y, [icon]); 545 } 546 547 /* down button */ { 548 bool hover = mMouseFocus && spinArea(mMousePos) == SpinArea.Bottom; 549 nvg.fillColor((mEnabled && (hover || spinning)) ? mTheme.mTextColor : mTheme.mDisabledTextColor); 550 auto icon = mTheme.mTextBoxDownIcon; 551 NVGTextAlign algn; 552 algn.left = true; 553 algn.middle = true; 554 nvg.textAlign(algn); 555 auto iconPos = Vector2f(mPos.x + 4.0f, 556 mPos.y + mSize.y/2.0f + xSpacing/2.0f + 1.5f); 557 nvg.text(iconPos.x, iconPos.y, [icon]); 558 } 559 560 nvg.fontSize(fontSize()); 561 nvg.fontFace("sans"); 562 } 563 564 final switch (mAlignment) { 565 case Alignment.Left: 566 NVGTextAlign algn; 567 algn.left = true; 568 algn.middle = true; 569 nvg.textAlign(algn); 570 draw_pos.x += cast(int)(xSpacing + spinArrowsWidth); 571 break; 572 case Alignment.Right: 573 NVGTextAlign algn; 574 algn.right = true; 575 algn.middle = true; 576 nvg.textAlign(algn); 577 draw_pos.x += cast(int)(mSize.x - unitWidth - xSpacing); 578 break; 579 case Alignment.Center: 580 NVGTextAlign algn; 581 algn.center = true; 582 algn.middle = true; 583 nvg.textAlign(algn); 584 draw_pos.x += cast(int)(mSize.x * 0.5f); 585 break; 586 } 587 588 nvg.fontSize(fontSize()); 589 nvg.fillColor(mEnabled && (!mCommitted || mValue.length) ? 590 mTheme.mTextColor : 591 mTheme.mDisabledTextColor); 592 593 // clip visible text area 594 float clipX = mPos.x + xSpacing + spinArrowsWidth - 1.0f; 595 float clipY = mPos.y + 1.0f; 596 float clipWidth = mSize.x - unitWidth - spinArrowsWidth - 2 * xSpacing + 2.0f; 597 float clipHeight = mSize.y - 3.0f; 598 599 nvg.save; 600 nvg.intersectScissor(clipX, clipY, clipWidth, clipHeight); 601 602 auto old_draw_pos = Vector2i(draw_pos); 603 draw_pos.x += cast(int) mTextOffset; 604 605 if (mCommitted) { 606 nvg.text(draw_pos.x, draw_pos.y, 607 !mValue.length ? mPlaceholder : mValue); 608 } else { 609 const int maxGlyphs = 1024; 610 NVGGlyphPosition[maxGlyphs] glyphs_buffer; 611 float[4] textBound; 612 nvg.textBounds(draw_pos.x, draw_pos.y, mValueTemp, 613 textBound); 614 float lineh = textBound[3] - textBound[1]; 615 616 // find cursor positions 617 auto glyphs = 618 nvg.textGlyphPositions(draw_pos.x, draw_pos.y, 619 mValueTemp, glyphs_buffer); 620 glyphs = squeezeGlyphs(glyphs_buffer[], glyphs); 621 updateCursor(nvg, textBound[2], glyphs); 622 623 // compute text offset 624 int prevCPos = mCursorPos > 0 ? mCursorPos - 1 : 0; 625 int len = cast(int) glyphs.length; 626 int nextCPos = mCursorPos < len ? mCursorPos + 1 : len; 627 float prevCX = cursorIndex2Position(prevCPos, textBound[2], glyphs); 628 float nextCX = cursorIndex2Position(nextCPos, textBound[2], glyphs); 629 630 if (nextCX > clipX + clipWidth) 631 mTextOffset -= nextCX - (clipX + clipWidth) + 1; 632 if (prevCX < clipX) 633 mTextOffset += clipX - prevCX + 1; 634 635 draw_pos.x = cast(int) (old_draw_pos.x + mTextOffset); 636 637 // draw text with offset 638 nvg.text(draw_pos.x, draw_pos.y, mValueTemp); 639 nvg.textBounds(draw_pos.x, draw_pos.y, mValueTemp, textBound); 640 641 // recompute cursor positions 642 glyphs = nvg.textGlyphPositions(draw_pos.x, draw_pos.y, 643 mValueTemp, glyphs_buffer); 644 glyphs = squeezeGlyphs(glyphs_buffer[], glyphs); 645 646 if (mCursorPos > -1) { 647 if (mSelectionPos > -1) { 648 float caretx = cursorIndex2Position(mCursorPos, textBound[2], 649 glyphs); 650 float selx = cursorIndex2Position(mSelectionPos, textBound[2], 651 glyphs); 652 653 if (caretx > selx) 654 { 655 swap(caretx, selx); 656 } 657 658 // draw selection 659 nvg.beginPath; 660 nvg.fillColor(Color(255, 255, 255, 80)); 661 nvg.rect(caretx, draw_pos.y - lineh * 0.5f, selx - caretx, 662 lineh); 663 nvg.fill; 664 } 665 666 float caretx = cursorIndex2Position(mCursorPos, textBound[2], glyphs); 667 668 // draw cursor 669 nvg.beginPath; 670 nvg.moveTo(caretx, draw_pos.y - lineh * 0.5f); 671 nvg.lineTo(caretx, draw_pos.y + lineh * 0.5f); 672 nvg.strokeColor(nvgRGBA(255, 192, 0, 255)); 673 nvg.strokeWidth(1.0f); 674 nvg.stroke; 675 } 676 } 677 nvg.restore; 678 } 679 680 // override void save(Serializer &s) const; 681 // override bool load(Serializer &s); 682 protected: 683 684 // hide method 685 override void cursor(Cursor value) 686 { 687 super.cursor(value); 688 } 689 690 bool checkFormat(string input, string format) 691 { 692 if (!format.length) 693 return true; 694 // try 695 // { 696 import std.regex : regex, matchAll; 697 import std.range : walkLength; 698 auto r = regex(format); 699 auto ma = input.matchAll(r); 700 return ma.walkLength == 1; 701 // } 702 // catch (RegexException) 703 // { 704 // throw; 705 // } 706 } 707 708 bool copySelection() 709 { 710 import nanogui.screen : Screen; 711 if (mSelectionPos > -1) { 712 Screen sc = cast(Screen) (window.parent); 713 if (!sc) 714 return false; 715 716 int begin = mCursorPos; 717 int end = mSelectionPos; 718 719 if (begin > end) 720 swap(begin, end); 721 722 // glfwSetClipboardString(sc->glfwWindow(), 723 // mValueTemp.substr(begin, end).c_str()); 724 return true; 725 } 726 727 return false; 728 } 729 730 void pasteFromClipboard() 731 { 732 import nanogui.screen : Screen; 733 Screen sc = cast(Screen) (window.parent); 734 if (!sc) 735 return; 736 // const char* cbstr = glfwGetClipboardString(sc->glfwWindow()); 737 // if (cbstr) 738 // mValueTemp.insert(mCursorPos, std::string(cbstr)); 739 } 740 bool deleteSelection() 741 { 742 if (mSelectionPos > -1) { 743 size_t begin = symbolLengthToBytes(mValueTemp, mCursorPos); 744 size_t end = symbolLengthToBytes(mValueTemp, mSelectionPos); 745 746 if (begin > end) 747 swap(begin, end); 748 749 if (begin == end - 1) 750 mValueTemp.replaceInPlace(begin, begin+1, (char[]).init); 751 else 752 mValueTemp.replaceInPlace(begin, end, (char[]).init); 753 754 import std.utf : count; 755 mCursorPos = cast(int) mValueTemp[0..begin].count; 756 mSelectionPos = -1; 757 return true; 758 } 759 760 return false; 761 } 762 763 void updateCursor(NVGContext nvg, float lastx, 764 const(NVGGlyphPosition)[] glyphs) 765 { 766 // handle mouse cursor events 767 if (mMouseDownPos.x != -1) { 768 if (mMouseDownModifier == KeyMod.Shift) 769 { 770 if (mSelectionPos == -1) 771 mSelectionPos = mCursorPos; 772 } else 773 mSelectionPos = -1; 774 775 mCursorPos = 776 position2CursorIndex(mMouseDownPos.x, lastx, glyphs); 777 778 mMouseDownPos = Vector2i(-1, -1); 779 } else if (mMouseDragPos.x != -1) { 780 if (mSelectionPos == -1) 781 mSelectionPos = mCursorPos; 782 783 mCursorPos = 784 position2CursorIndex(mMouseDragPos.x, lastx, glyphs); 785 } else { 786 // set cursor to last character 787 if (mCursorPos == -2) 788 mCursorPos = cast(int) glyphs.length; 789 } 790 791 if (mCursorPos == mSelectionPos) 792 mSelectionPos = -1; 793 } 794 float cursorIndex2Position(int index, float lastx, 795 const(NVGGlyphPosition)[] glyphs) 796 { 797 float pos = 0; 798 if (index == glyphs.length) 799 pos = lastx; // last character 800 else 801 pos = glyphs[index].x; 802 803 return pos; 804 } 805 806 int position2CursorIndex(float posx, float lastx, 807 const(NVGGlyphPosition)[] glyphs) 808 { 809 int mCursorId = 0; 810 float caretx = glyphs[mCursorId].x; 811 for (int j = 1; j < glyphs.length; j++) { 812 if (abs(caretx - posx) > abs(glyphs[j].x - posx)) { 813 mCursorId = j; 814 caretx = glyphs[mCursorId].x; 815 } 816 } 817 if (abs(caretx - posx) > abs(lastx - posx)) 818 mCursorId = cast(int) glyphs.length; 819 820 return mCursorId; 821 } 822 823 /// The location (if any) for the spin area. 824 enum SpinArea { None, Top, Bottom } 825 SpinArea spinArea(Vector2i pos) 826 { 827 if (0 <= pos.x - mPos.x && pos.x - mPos.x < 14.0f) { /* on scrolling arrows */ 828 if (mSize.y >= pos.y - mPos.y && pos.y - mPos.y <= mSize.y / 2.0f) { /* top part */ 829 return SpinArea.Top; 830 } else if (0.0f <= pos.y - mPos.y && pos.y - mPos.y > mSize.y / 2.0f) { /* bottom part */ 831 return SpinArea.Bottom; 832 } 833 } 834 return SpinArea.None; 835 } 836 837 bool mEditable; 838 bool mSpinnable; 839 bool mCommitted; 840 string mValue; 841 string mDefaultValue; 842 Alignment mAlignment; 843 string mUnits; 844 string mFormat; 845 NVGImage mUnitsImage; 846 bool delegate(string str) mCallback; 847 bool mValidFormat; 848 string mValueTemp; 849 string mPlaceholder; 850 int mCursorPos; 851 int mSelectionPos; 852 Vector2i mMousePos; 853 Vector2i mMouseDownPos; 854 Vector2i mMouseDragPos; 855 int mMouseDownModifier; 856 float mTextOffset; 857 double mLastClick; 858 } 859 860 // /** 861 // * \class IntBox textbox.h nanogui/textbox.h 862 // * 863 // * \brief A specialization of TextBox for representing integral values. 864 // * 865 // * Template parameters should be integral types, e.g. `int`, `long`, 866 // * `uint32_t`, etc. 867 // */ 868 // template <typename Scalar> 869 // class IntBox : public TextBox { 870 // public: 871 // IntBox(Widget *parent, Scalar value = (Scalar) 0) : TextBox(parent) { 872 // setDefaultValue("0"); 873 // setFormat(std::is_signed<Scalar>::value ? "[-]?[0-9]*" : "[0-9]*"); 874 // setValueIncrement(1); 875 // setMinMaxValues(std::numeric_limits<Scalar>::lowest(), std::numeric_limits<Scalar>::max()); 876 // setValue(value); 877 // setSpinnable(false); 878 // } 879 880 // Scalar value() const { 881 // std::istringstream iss(TextBox::value()); 882 // Scalar value = 0; 883 // iss >> value; 884 // return value; 885 // } 886 887 // void setValue(Scalar value) { 888 // Scalar clampedValue = std::min(std::max(value, mMinValue),mMaxValue); 889 // TextBox::setValue(std::to_string(clampedValue)); 890 // } 891 892 // void setCallback(const std::function<void(Scalar)> &cb) { 893 // TextBox::setCallback( 894 // [cb, this](const string &str) { 895 // std::istringstream iss(str); 896 // Scalar value = 0; 897 // iss >> value; 898 // setValue(value); 899 // cb(value); 900 // return true; 901 // } 902 // ); 903 // } 904 905 // void setValueIncrement(Scalar incr) { 906 // mValueIncrement = incr; 907 // } 908 // void setMinValue(Scalar minValue) { 909 // mMinValue = minValue; 910 // } 911 // void setMaxValue(Scalar maxValue) { 912 // mMaxValue = maxValue; 913 // } 914 // void setMinMaxValues(Scalar minValue, Scalar maxValue) { 915 // setMinValue(minValue); 916 // setMaxValue(maxValue); 917 // } 918 919 // virtual bool mouseButtonEvent(Vector2i p, int button, bool down, int modifiers) override { 920 // if ((mEditable || mSpinnable) && down) 921 // mMouseDownValue = value(); 922 923 // SpinArea area = spinArea(p); 924 // if (mSpinnable && area != SpinArea.None && down && !focused()) { 925 // if (area == SpinArea.Top) { 926 // setValue(value() + mValueIncrement); 927 // if (mCallback) 928 // mCallback(mValue); 929 // } else if (area == SpinArea.Bottom) { 930 // setValue(value() - mValueIncrement); 931 // if (mCallback) 932 // mCallback(mValue); 933 // } 934 // return true; 935 // } 936 937 // return TextBox::mouseButtonEvent(p, button, down, modifiers); 938 // } 939 // virtual bool mouseDragEvent(Vector2i p, Vector2i rel, int button, int modifiers) override { 940 // if (TextBox::mouseDragEvent(p, rel, button, modifiers)) { 941 // return true; 942 // } 943 // if (mSpinnable && !focused() && button == 2 /* 1 << GLFW_MOUSE_BUTTON_2 */ && mMouseDownPos.x != -1) { 944 // int valueDelta = static_cast<int>((p.x - mMouseDownPos.x) / float(10)); 945 // setValue(mMouseDownValue + valueDelta * mValueIncrement); 946 // if (mCallback) 947 // mCallback(mValue); 948 // return true; 949 // } 950 // return false; 951 // } 952 // virtual bool scrollEvent(Vector2i p, const Vector2f &rel) override { 953 // if (Widget::scrollEvent(p, rel)) { 954 // return true; 955 // } 956 // if (mSpinnable && !focused()) { 957 // int valueDelta = (rel.y > 0) ? 1 : -1; 958 // setValue(value() + valueDelta*mValueIncrement); 959 // if (mCallback) 960 // mCallback(mValue); 961 // return true; 962 // } 963 // return false; 964 // } 965 // private: 966 // Scalar mMouseDownValue; 967 // Scalar mValueIncrement; 968 // Scalar mMinValue, mMaxValue; 969 // public: 970 // EIGEN_MAKE_ALIGNED_OPERATOR_NEW 971 // }; 972 973 /** 974 * \class FloatBox textbox.d nanogui/textbox.d 975 * 976 * \brief A specialization of TextBox representing floating point values. 977 978 * Template parameters should be float types, e.g. `float`, `double`, 979 * `float64_t`, etc. 980 */ 981 class FloatBox(Scalar) : TextBox { 982 public: 983 this(Widget parent, Scalar v = cast(Scalar) 0.0f) 984 { 985 super(parent); 986 mNumberFormat = Scalar.sizeof == float.sizeof ? "%.4g" : "%.7g"; 987 defaultValue("0"); 988 format("[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?");valueIncrement(cast(Scalar) 0.1); 989 minMaxValues(Scalar.min_exp, Scalar.max); 990 value(v); 991 spinnable(false); 992 } 993 994 final string numberFormat() const { return mNumberFormat; } 995 final void numberFormat(string format) { mNumberFormat = format; } 996 997 final Scalar value() const { 998 import std.conv : to; 999 return TextBox.value.to!Scalar; 1000 } 1001 1002 final void value(Scalar v) { 1003 import std.algorithm : min, max; 1004 import std..string : fromStringz; 1005 import core.stdc.stdio : snprintf; 1006 1007 Scalar clampedValue = min(max(v, mMinValue), mMaxValue); 1008 char[50] buffer; 1009 snprintf(buffer.ptr, 50, mNumberFormat.ptr, clampedValue); 1010 TextBox.value(buffer.ptr.fromStringz.dup); 1011 } 1012 1013 final void callback(void delegate(Scalar) cb) { 1014 TextBox.callback((string str) { 1015 import std.conv : to; 1016 Scalar scalar = str.to!Scalar; 1017 value(scalar); 1018 cb(scalar); 1019 return true; 1020 }); 1021 } 1022 1023 final void valueIncrement(Scalar value) { 1024 mValueIncrement = value; 1025 } 1026 1027 final void minValue(Scalar value) { 1028 mMinValue = value; 1029 } 1030 1031 final void maxValue(Scalar value) { 1032 mMaxValue = value; 1033 } 1034 1035 final void minMaxValues(Scalar min_value, Scalar max_value) { 1036 minValue(min_value); 1037 maxValue(max_value); 1038 } 1039 1040 override bool mouseButtonEvent(Vector2i p, MouseButton button, bool down, int modifiers) 1041 { 1042 if ((mEditable || mSpinnable) && down) 1043 mMouseDownValue = value(); 1044 1045 SpinArea area = spinArea(p); 1046 if (mSpinnable && area != SpinArea.None && down && !focused()) { 1047 if (area == SpinArea.Top) { 1048 value(value() + mValueIncrement); 1049 if (mCallback) 1050 mCallback(mValue); 1051 } else if (area == SpinArea.Bottom) { 1052 value(value() - mValueIncrement); 1053 if (mCallback) 1054 mCallback(mValue); 1055 } 1056 return true; 1057 } 1058 1059 return TextBox.mouseButtonEvent(p, button, down, modifiers); 1060 } 1061 1062 override bool mouseDragEvent(Vector2i p, Vector2i rel, MouseButton button, int modifiers) 1063 { 1064 if (TextBox.mouseDragEvent(p, rel, button, modifiers)) { 1065 return true; 1066 } 1067 if (mSpinnable && !focused() && button == 2 /* 1 << GLFW_MOUSE_BUTTON_2 */ && mMouseDownPos.x != -1) { 1068 int valueDelta = cast(int)((p.x - mMouseDownPos.x) / float(10)); 1069 value(mMouseDownValue + valueDelta * mValueIncrement); 1070 if (mCallback) 1071 mCallback(mValue); 1072 return true; 1073 } 1074 return false; 1075 } 1076 1077 override bool scrollEvent(Vector2i p, Vector2f rel) 1078 { 1079 if (Widget.scrollEvent(p, rel)) { 1080 return true; 1081 } 1082 if (mSpinnable && !focused()) { 1083 const valueDelta = (rel.y > 0) ? 1 : -1; 1084 value(value() + valueDelta*mValueIncrement); 1085 if (mCallback) 1086 mCallback(mValue); 1087 return true; 1088 } 1089 return false; 1090 } 1091 1092 private: 1093 string mNumberFormat; 1094 Scalar mMouseDownValue; 1095 Scalar mValueIncrement; 1096 Scalar mMinValue, mMaxValue; 1097 }