1 /++ 2 Small technologies assembly module for building a debug floating interface. 3 4 A widget is that fundamental unit of the interface. Other widgets are inherited 5 from it for interaction with each other. Also, all widgets must inherit from the 6 instance to control the behavior of the widgets (position, activity, etc.). 7 8 In total, the following widgets are available by default: 9 * Button. 10 * Scroll widget. 11 * Text field. 12 * Mini window (aka context for widgets). 13 * Widget optional selection. (checkbox). 14 * Text widget. 15 16 Macros: 17 LREF = <a href="#$1">$1</a> 18 HREF = <a href="$1">$2</a> 19 20 Authors: $(HREF https://github.com/TodNaz,TodNaz) 21 Copyright: Copyright (c) 2020 - 2021, TodNaz. 22 License: $(HREF https://github.com/TodNaz/Tida/blob/master/LICENSE,MIT) 23 +/ 24 module tida.ui; 25 26 import tida; 27 28 alias Action = void delegate() @safe; 29 alias ActioncClick = void delegate(MouseButton) @safe; 30 31 /++ 32 Information about the color palette of widgets. 33 +/ 34 struct ThemeInfo 35 { 36 public: 37 /++ 38 Widget background. 39 +/ 40 Color!ubyte background = Color!ubyte("ADADAD"); 41 42 /++ 43 The color of the lines of the widget. 44 +/ 45 Color!ubyte line = Color!ubyte("575757"); 46 47 /++ 48 The color of the text of the widget. 49 +/ 50 Color!ubyte text = Color!ubyte("000000"); 51 52 /++ 53 The background color of the widget when the cursor is hovered over it. 54 +/ 55 Color!ubyte backgroundHolder = Color!ubyte("BDBDBD"); 56 57 /++ 58 Widget title color. 59 +/ 60 Color!ubyte title = Color!ubyte("9D9D9D"); 61 62 /++ 63 The color of the progressive line of the widget. 64 +/ 65 Color!ubyte progress = Color!ubyte("EDEDED"); 66 67 /++ 68 A drawing option that indicates whether corners should be rounded. 69 +/ 70 bool isRoundedCorners = false; 71 72 /++ 73 corner radius. 74 +/ 75 float radiusRounded = 8.0f; 76 } 77 78 /// Default theme widgets. 79 enum DefaultTheme = ThemeInfo(); 80 81 /++ 82 Interface for interacting with the widget. 83 +/ 84 interface UIWidget 85 { 86 @safe: 87 /++ 88 Available width of the widget. 89 +/ 90 @property uint width(); 91 92 /++ 93 Available height of the widget. 94 +/ 95 @property uint height(); 96 97 /++ 98 Resize the widget. 99 100 Params: 101 w = Available width of the widget. 102 h = Available height of the widget. 103 +/ 104 void resize(uint w, uint h); 105 106 /++ 107 The relative position of the widget. Serves for relative positioning of 108 the widget relative to another or its own object. 109 +/ 110 @property ref Vecf widgetPosition(); 111 } 112 113 /++ 114 Button widget. Serves for processing some actions when clicking 115 on the widget itself. 116 +/ 117 class UIButton : Instance, UIWidget 118 { 119 private: 120 Font font; 121 uint _width = 128, _height = 32; 122 bool _isHold = false; 123 Vecf releative = vecf(0.0f, 0.0f); 124 125 public: 126 /++ 127 The text of the widget that will be displayed directly on the 128 button for intuitiveness. 129 +/ 130 string label; 131 132 /++ 133 The icon that is displayed on the widget. serves for the intuitiveness of the button. 134 +/ 135 Image icon; 136 137 /++ 138 Action when the mouse cursor is over the button. 139 +/ 140 Action onHolder; 141 142 /++ 143 Action When Button Was Pressed The mouse button that was pressed at the moment is passed as arguments. 144 +/ 145 ActioncClick onClick; 146 147 /// Widget theme. 148 ThemeInfo theme; 149 150 @safe: 151 this(Font font, ThemeInfo theme = DefaultTheme) 152 { 153 this.font = font; 154 this.theme = theme; 155 156 this.position = vecf(0.0f, 0.0f); 157 } 158 159 @Event!Input 160 void inputHandle(EventHandler event) 161 { 162 Vecf mousePosition = vecf(event.mousePosition); 163 164 if (mousePosition.x > releative.x + position.x && 165 mousePosition.y > releative.y + position.y && 166 mousePosition.x < releative.x + position.x + _width && 167 mousePosition.y < releative.y + position.y + _height) 168 { 169 if (!_isHold && onHolder !is null) onHolder(); 170 _isHold = true; 171 172 if (event.isMouseDown) 173 { 174 if (onClick !is null) onClick(event.mouseButton); 175 } 176 } else 177 { 178 _isHold = false; 179 } 180 } 181 182 @Event!Draw 183 void selfDraw(IRenderer render) 184 { 185 Color!ubyte color = _isHold ? theme.backgroundHolder : theme.background; 186 187 Vecf old = position; 188 position += releative; 189 190 if (theme.isRoundedCorners) 191 { 192 float corner = theme.radiusRounded > _height / 2 ? _height / 2 : 193 theme.radiusRounded; 194 195 render.roundrect(position, _width, _height, corner, color, true); 196 } else 197 { 198 render.rectangle(position, _width, _height, color, true); 199 } 200 201 if (label.length != 0) 202 { 203 auto syms = new Text(font).toSymbols(label, theme.text); 204 205 immutable ycenter = height / 2; 206 immutable h = font.size / 2; 207 immutable xcenter = (width / 2) - (syms.widthSymbols / 2) + 208 (icon !is null ? _height / 2 : 0); 209 210 render.draw(new SymbolRender(syms), position + vecf(xcenter, ycenter - h)); 211 } 212 213 if (icon !is null) 214 { 215 if (label.length != 0) 216 { 217 render.drawEx(icon, position + vecf(2, 2), 0.0f, vecfNaN, 218 vecf(_height - 4, _height - 4), 255); 219 } 220 else 221 { 222 immutable xcenter = (_width / 2) - ((_height - 4) / 2); 223 render.drawEx(icon, position + vecf(xcenter, 2), 0.0f, vecfNaN, 224 vecf(_height - 4, _height - 4), 255); 225 } 226 } 227 228 position = old; 229 } 230 231 override: 232 @property uint width() 233 { 234 return _width; 235 } 236 237 @property uint height() 238 { 239 return _height; 240 } 241 242 void resize(uint w, uint h) 243 { 244 _width = w; 245 _height = h; 246 } 247 248 @property ref Vecf widgetPosition() 249 { 250 return this.releative; 251 } 252 } 253 254 /++ 255 Scrolling widget. Widget for managing or showing the approximate value 256 of any maximum value. 257 +/ 258 class UIScroll : Instance, UIWidget 259 { 260 private: 261 alias ActionFactor = void delegate(float) @safe; 262 263 uint _width = 128; 264 uint _height = 8; 265 Vecf releative = vecfNaN; 266 ThemeInfo theme; 267 bool _isMove = false; 268 269 float k = 0.0f; 270 271 public: 272 /++ 273 Is it possible to change the value of the widget 274 +/ 275 bool isEditable = true; 276 277 /++ 278 The action when the user changes the value. 279 +/ 280 ActionFactor onMoveScroll; 281 282 @safe: 283 this(ThemeInfo theme = DefaultTheme) 284 { 285 this.theme = theme; 286 287 this.position = vecf(0.0f, 0.0f); 288 } 289 290 /++ 291 The value of the factor of the widget. 292 +/ 293 @property float value() inout 294 { 295 return k; 296 } 297 298 @Event!Input 299 void onInput(EventHandler event) 300 { 301 if (!isEditable) return; 302 303 Vecf mousePosition = vecf(event.mousePosition); 304 305 if (mousePosition.x > releative.x + position.x && 306 mousePosition.y > releative.y + position.y && 307 mousePosition.x < releative.x + position.x + _width && 308 mousePosition.y < releative.y + position.y + _height) 309 { 310 if (event.mouseDownButton == MouseButton.left) 311 { 312 _isMove = true; 313 } 314 } 315 316 if (event.mouseUpButton == MouseButton.left) 317 { 318 _isMove = false; 319 } 320 321 if (_isMove) 322 { 323 immutable factor = mousePosition.x - (releative.x + position.x); 324 k = factor / _width; 325 326 if (k < 0.0f) k = 0.0f; 327 if (k > 1.0f) k = 1.0f; 328 329 if (onMoveScroll !is null) onMoveScroll(k); 330 } 331 } 332 333 @Event!Draw 334 void onDraw(IRenderer render) 335 { 336 Vecf old = position; 337 position += releative; 338 339 if (theme.isRoundedCorners) 340 { 341 float corner = theme.radiusRounded > _height / 2 ? _height / 2 : theme.radiusRounded; 342 343 render.roundrect(position, _width, _height, corner, theme.background, true); 344 uint w = cast(uint) ((cast(float) _width) * k); 345 render.roundrect(position, w, _height, corner, theme.progress, true); 346 } else 347 { 348 render.rectangle(position, _width, _height, theme.background, true); 349 uint w = cast(uint) ((cast(float) _width) * k); 350 render.rectangle(position, w, _height, theme.progress, true); 351 } 352 353 position = old; 354 } 355 356 override: 357 @property uint width() 358 { 359 return _width; 360 } 361 362 @property uint height() 363 { 364 return _height; 365 } 366 367 void resize(uint w, uint h) 368 { 369 _width = w; 370 _height = h; 371 } 372 373 @property ref Vecf widgetPosition() 374 { 375 return this.releative; 376 } 377 } 378 379 /++ 380 Widget for entering text data. 381 +/ 382 class UITextBox : Instance, UIWidget 383 { 384 private: 385 uint _width = 128; 386 uint _height = 24; 387 Vecf releative = vecf(0, 0); 388 389 string value; 390 Font font; 391 bool _isFocus = false; 392 393 int cursor = 0; 394 size_t viewbegin = 0; 395 size_t viewend = 0; 396 397 public: 398 /++ 399 Icon for intuition. 400 +/ 401 Image icon; 402 403 /++ 404 Widget theme. 405 +/ 406 ThemeInfo theme; 407 408 @safe: 409 this(Font font, ThemeInfo theme) 410 { 411 this.font = font; 412 this.theme = theme; 413 414 this.position = vecf(0.0f, 0.0f); 415 } 416 417 /++ 418 The entered data. 419 +/ 420 @property string text() inout 421 { 422 return value; 423 } 424 425 private bool exceptChar(EventHandler event, char ch) 426 { 427 return ch > 32; 428 } 429 430 @Event!Input 431 void onInput(EventHandler event) 432 { 433 Vecf mousePosition = vecf(event.mousePosition); 434 435 if (_isFocus) 436 { 437 char ichar = event.inputChar[0]; 438 439 if (event.isInputText() && exceptChar(event, ichar)) 440 { 441 value = value[0 .. cursor] ~ ichar ~ value[cursor .. $]; 442 cursor++; 443 }else 444 { 445 if(event.isKeyDown) 446 { 447 if (event.key == Key.Left) 448 { 449 if (cursor != 0) cursor--; 450 }else 451 if (event.key == Key.Right) 452 { 453 if (cursor != value.length) cursor++; 454 }else 455 if (ichar == Key.Backspace) 456 { 457 if (cursor != 0) 458 { 459 value = value[0 .. cursor - 1] ~ value[cursor .. $]; 460 cursor--; 461 } 462 } 463 } 464 } 465 } 466 467 if (mousePosition.x > releative.x + position.x && 468 mousePosition.y > releative.y + position.y && 469 mousePosition.x < releative.x + position.x + _width && 470 mousePosition.y < releative.y + position.y + _height) 471 { 472 if (event.mouseDownButton == MouseButton.left) 473 { 474 _isFocus = true; 475 } 476 }else 477 { 478 if (event.mouseDownButton == MouseButton.left) 479 { 480 _isFocus = false; 481 } 482 } 483 } 484 485 @Event!Draw 486 void selfDraw(IRenderer render) 487 { 488 Color!ubyte color = _isFocus ? theme.backgroundHolder : theme.background; 489 490 Vecf old = position; 491 position += releative; 492 493 if (theme.isRoundedCorners) 494 { 495 float corner = theme.radiusRounded > _height / 2 ? _height / 2 : theme.radiusRounded; 496 497 render.roundrect(position, _width, _height, corner, color, true); 498 } else 499 { 500 render.rectangle(position, _width, _height, color, true); 501 } 502 503 if (value.length != 0) 504 { 505 auto syms = new Text(font).toSymbols(value, theme.text); 506 507 if (syms.widthSymbols > _width - 8) 508 { 509 viewbegin = 0; 510 viewend = 0; 511 512 for (size_t i = 0; i < syms.length; ++i) 513 { 514 if (syms[0 .. i].widthSymbols > _width - 8) 515 { 516 viewend = i - 1; 517 break; 518 } 519 } 520 521 if (viewend == 0) viewend = syms.length; 522 523 if (cursor > viewend) 524 { 525 for (size_t i = viewend; i < cursor; ++i) 526 { 527 viewend++; 528 viewbegin++; 529 } 530 531 if (viewend > syms.length) viewbegin = syms.length; 532 }else 533 if (cursor <= viewbegin && viewbegin != 0) 534 { 535 for (size_t i = viewbegin; i <= cursor; ++i) 536 { 537 viewbegin--; 538 viewend--; 539 } 540 } 541 }else 542 { 543 viewbegin = 0; 544 viewend = syms.length; 545 } 546 547 immutable ycenter = height / 2; 548 immutable h = font.size / 2; 549 550 render.draw(new SymbolRender(syms[viewbegin .. viewend]), position + vecf(4, ycenter - h)); 551 render.line([ 552 position + vecf(4 + syms[viewbegin .. cursor].widthSymbols, ycenter - h), 553 position + vecf(4 + syms[viewbegin .. cursor].widthSymbols, ycenter - h + (font.size * 2)) 554 ], theme.text); 555 } 556 557 if (icon !is null) 558 { 559 if (value.length != 0) 560 { 561 render.drawEx(icon, position + vecf(2, 2), 0.0f, vecfNaN, vecf(_height - 4, _height - 4), 255); 562 } 563 else 564 { 565 immutable xcenter = (_width / 2) - ((_height - 4) / 2); 566 render.drawEx(icon, position + vecf(xcenter, 2), 0.0f, vecfNaN, vecf(_height - 4, _height - 4), 255); 567 } 568 } 569 570 position = old; 571 } 572 573 override: 574 @property uint width() 575 { 576 return _width; 577 } 578 579 @property uint height() 580 { 581 return _height; 582 } 583 584 void resize(uint w, uint h) 585 { 586 _width = w; 587 _height = h; 588 } 589 590 @property ref Vecf widgetPosition() 591 { 592 return this.releative; 593 } 594 } 595 596 /++ 597 Mini-window aka the context for widgets with which you can personalize the 598 widget panel for the theme and move (or not move) such a panel. 599 +/ 600 class UIWindow : Instance, UIWidget 601 { 602 private: 603 struct UIChild 604 { 605 UIWidget widget; 606 Vecf position; 607 } 608 609 uint _width = 320; 610 uint _height = 240; 611 Font font; 612 UIChild[] childs; 613 614 bool isMove = false; 615 Vecf moveBegin = vecfNaN; 616 617 bool _isTurn = true; 618 619 public: 620 alias ActionDraw = void delegate(IRenderer, Vecf) @safe; 621 622 /++ 623 The width of the title bar of the window. 624 +/ 625 uint titleHeight = 16; 626 627 /++ 628 Window title. 629 +/ 630 string title; 631 632 /++ 633 The background of the window. 634 +/ 635 Color!ubyte background; 636 637 /++ 638 Operations for rendering user data, if the widget does not provide such. 639 +/ 640 ActionDraw[] draws; 641 642 /++ 643 Widget theme. 644 +/ 645 ThemeInfo theme; 646 647 /++ 648 Whether to show the title of the window to handle the movement of the window. 649 +/ 650 bool isTitleView = true; 651 652 @safe: 653 this(Font font, ThemeInfo theme = DefaultTheme) 654 { 655 this.font = font; 656 this.theme = theme; 657 658 this.position = vecf(0.0f, 0.0f); 659 660 background = rgb(255, 255, 255); 661 } 662 663 @Event!Input 664 void onInput(EventHandler event) 665 { 666 Vecf mousePosition = vecf(event.mousePosition); 667 668 if (isTitleView) 669 { 670 if (mousePosition.x > position.x && 671 mousePosition.y > position.y && 672 mousePosition.x < position.x + _width && 673 mousePosition.y < position.y + titleHeight) 674 { 675 if (event.mouseDownButton == MouseButton.left) 676 { 677 isMove = true; 678 moveBegin = mousePosition - position; 679 } 680 681 if (event.mouseUpButton == MouseButton.left) 682 { 683 isMove = false; 684 } 685 } 686 687 immutable spos = position + vecf(_width - 18, titleHeight / 2 - 4); 688 689 if (mousePosition.x > spos.x && 690 mousePosition.y > spos.y && 691 mousePosition.x < spos.x + 10 && 692 mousePosition.y < spos.y + 8) 693 { 694 if (event.mouseDownButton == MouseButton.left) 695 { 696 _isTurn = !_isTurn; 697 698 foreach (ref e; childs) 699 { 700 (cast(Instance) e.widget).active = _isTurn; 701 } 702 } 703 } 704 705 if (isMove) 706 { 707 position = mousePosition - moveBegin; 708 } 709 } 710 } 711 712 @Event!Step 713 void onStep() 714 { 715 foreach (ref e; childs) 716 { 717 (cast(Instance) e.widget).depth = depth - 1; 718 sceneManager.context.sort(); 719 e.widget.widgetPosition = this.position + e.position + 720 (isTitleView ? vecf(0, titleHeight) : vecf(0.0f, 0.0f)); 721 } 722 } 723 724 @Event!Draw 725 void onDraw(IRenderer render) 726 { 727 if (theme.isRoundedCorners) 728 { 729 float corner = theme.radiusRounded > titleHeight / 2 ? titleHeight / 2 : theme.radiusRounded; 730 731 if(isTitleView) 732 { 733 render.roundrect(position, _width, titleHeight, corner, theme.title, true); 734 render.rectangle(position + vecf(0, titleHeight / 2), _width, titleHeight / 2, theme.title, true); 735 } 736 737 if (_isTurn) 738 { 739 immutable vh = isTitleView ? (_height - titleHeight) : _height; 740 741 render.rectangle(position + vecf(0, isTitleView ? titleHeight : 0), 742 _width, vh, background, true); 743 render.rectangle(position + vecf(0, isTitleView ? titleHeight : 0), 744 _width, vh, theme.line, false); 745 } 746 } else 747 { 748 if (isTitleView) 749 { 750 render.rectangle(position, _width, titleHeight, theme.title, true); 751 } 752 753 if (_isTurn) 754 { 755 immutable vh = isTitleView ? (_height - titleHeight) : _height; 756 757 render.rectangle(position + vecf(0, isTitleView ? titleHeight : 0), 758 _width, vh, background, true); 759 render.rectangle(position + vecf(0, isTitleView ? titleHeight : 0), 760 _width, vh, theme.line, false); 761 } 762 } 763 764 if (isTitleView) 765 render.rectangle(position + vecf(_width - 18, titleHeight / 2 - 2), 8, 4, theme.line, true); 766 767 if (title.length != 0 && isTitleView) 768 { 769 render.draw(new Text(font).renderSymbols(title, theme.text), position + vecf(4, 2)); 770 } 771 772 if(_isTurn) 773 foreach(e; draws) 774 e(render, this.position + vecf(0, isTitleView ? titleHeight : 0)); 775 } 776 777 /++ 778 Adds a widget to the inside of the window. 779 +/ 780 void add(UIWidget widget, Vecf pos) 781 { 782 childs ~= UIChild(widget, pos); 783 } 784 785 override: 786 @property uint width() 787 { 788 return _width; 789 } 790 791 @property uint height() 792 { 793 return _height; 794 } 795 796 void resize(uint w, uint h) 797 { 798 _width = w; 799 _height = h; 800 } 801 802 @property ref Vecf widgetPosition() 803 { 804 return this.position; 805 } 806 } 807 808 /++ 809 Optional widget. 810 +/ 811 class UICheckBox : Instance, UIWidget 812 { 813 private: 814 uint _width = 16; 815 uint _height = 16; 816 bool _isCheck = false; 817 bool _isHold = false; 818 Vecf releative = vecf(0.0f, 0.0f); 819 820 public: 821 /// Widget theme 822 ThemeInfo theme; 823 824 /++ 825 Shows the status of the option. 826 +/ 827 @property ref bool isCheck() @safe 828 { 829 return _isCheck; 830 } 831 832 @safe: 833 this(ThemeInfo theme = DefaultTheme) 834 { 835 this.theme = theme; 836 837 this.position = vecf(0.0f, 0.0f); 838 } 839 840 @Event!Input 841 void onInput(EventHandler event) 842 { 843 Vecf mousePosition = vecf(event.mousePosition); 844 845 if (mousePosition.x > releative.x + position.x && 846 mousePosition.y > releative.y + position.y && 847 mousePosition.x < releative.x + position.x + _width && 848 mousePosition.y < releative.y + position.y + _height) 849 { 850 _isHold = true; 851 852 if (event.mouseDownButton == MouseButton.left) 853 { 854 _isCheck = !_isCheck; 855 } 856 } else 857 { 858 _isHold = false; 859 } 860 } 861 862 @Event!Draw 863 void onDraw(IRenderer render) 864 { 865 Color!ubyte color = _isHold ? theme.backgroundHolder : theme.background; 866 867 Vecf old = position; 868 position += releative; 869 870 if (theme.isRoundedCorners) 871 { 872 float corner = theme.radiusRounded > _height / 2 ? _height / 2 : theme.radiusRounded; 873 874 render.roundrect(position, _width, _height, corner, color, true); 875 876 if (_isCheck) 877 { 878 render.roundrect(position + vecf(2, 2), _width - 4, _height - 4, corner, theme.line, true); 879 } 880 } else 881 { 882 render.rectangle(position, _width, _height, color, true); 883 884 if (_isCheck) 885 { 886 render.rectangle(position + vecf(2, 2), _width - 4, _height - 4, theme.line, true); 887 } 888 } 889 890 position = old; 891 } 892 893 override: 894 @property uint width() 895 { 896 return _width; 897 } 898 899 @property uint height() 900 { 901 return _height; 902 } 903 904 void resize(uint w, uint h) 905 { 906 _width = w; 907 _height = h; 908 } 909 910 @property ref Vecf widgetPosition() 911 { 912 return this.releative; 913 } 914 } 915 916 /++ 917 Label widget. Serves for full interactivity of something. 918 +/ 919 class UILabel : Instance, UIWidget 920 { 921 private: 922 uint _width = 128; 923 uint _height = 16; 924 Font font; 925 Vecf releative = vecf(0.0f, 0.0f); 926 927 public: 928 /// Label text 929 string text; 930 931 /// Widget theme 932 ThemeInfo theme; 933 934 @safe: 935 this(Font font, string label = "", ThemeInfo theme = DefaultTheme) 936 { 937 this.font = font; 938 this.theme = theme; 939 940 this.position = vecf(0.0f, 0.0f); 941 this.text = label; 942 } 943 944 @Event!Draw 945 void onDraw(IRenderer render) 946 { 947 if (text.length == 0) return; 948 949 auto syms = new Text(font).toSymbols(text, theme.text); 950 951 size_t end = syms.length; 952 953 if (syms.widthSymbols > _width) 954 { 955 for (size_t i = 0; i < syms.length; ++i) 956 { 957 if (syms[0 .. i].widthSymbols > _width) 958 { 959 end = i - 1; 960 break; 961 } 962 } 963 } 964 965 render.draw(new SymbolRender(syms), this.releative + this.position); 966 } 967 968 override: 969 @property uint width() 970 { 971 return _width; 972 } 973 974 @property uint height() 975 { 976 return _height; 977 } 978 979 void resize(uint w, uint h) 980 { 981 _width = w; 982 _height = h; 983 } 984 985 @property ref Vecf widgetPosition() 986 { 987 return this.releative; 988 } 989 }