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 }