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