1 /**
2     Gui Element
3 
4     Copyright: (c) Enalye 2017
5     License: Zlib
6     Authors: Enalye
7 */
8 
9 module atelier.ui.gui_element;
10 
11 import atelier.render, atelier.core, atelier.common, atelier.render;
12 import atelier.ui.gui_manager, atelier.ui.gui_overlay;
13 
14 /// Alignment on the horizontal axis relative to its parent.
15 enum GuiAlignX {
16     left,
17     center,
18     right
19 }
20 
21 /// Alignment on the vertical axis relative to its parent.
22 enum GuiAlignY {
23     top,
24     center,
25     bottom
26 }
27 
28 /// Single state of a GUI. \
29 /// Used with **addState**, **setState**, **doTransitionState** etc.
30 struct GuiState {
31     /// Position offset relative to its alignment. (0 = No modification)
32     Vec2f offset = Vec2f.zero;
33     /// Size scale of the GUI. (1 = Default)
34     Vec2f scale = Vec2f.one;
35     /// Color of the GUI. (White = Default)
36     Color color = Color.white;
37     /// Opacity of the GUI (1 = Default)
38     float alpha = 1f;
39     /// Blend of the canvas if present (Alpha blending = Default)
40     Blend blend = Blend.alpha;
41     /// Angle of the GUI. (0 = Default)
42     float angle = 0f;
43     /// Time (in seconds) to get to this state with **doTransitionState()**.
44     float time = .5f;
45     /// When fully in this state, **onCallback()** will be called with **callback**.
46     string callback;
47     /// The easing algorithm used to get to this state.
48     EasingFunction easing = &easeLinear;
49 }
50 
51 /// Base class of all GUI elements.
52 class GuiElement {
53     private {
54         Canvas _canvas;
55         bool _hasCanvas;
56     }
57 
58     package {
59         GuiElement[] _children;
60         Hint _hint;
61         bool _isRegistered = true;
62         bool _isLocked, _isMovable, _isHovered, _isClicked, _isSelected,
63             _hasFocus, _isInteractable = true, _hasEventHook;
64         Vec2f _position = Vec2f.zero, _size = Vec2f.zero, _anchor = Vec2f.half,
65             _padding = Vec2f.zero, _center = Vec2f.zero, _origin = Vec2f.zero;
66         GuiElement _callbackGuiElement;
67         string _callbackId;
68 
69         //Iteration
70         bool _isIterating, _isWarping = true;
71         uint _idChildIterator;
72         Timer _iteratorTimer, _iteratorTimeOutTimer;
73 
74         Vec2f _screenCoords;
75         GuiAlignX _alignX = GuiAlignX.left;
76         GuiAlignY _alignY = GuiAlignY.top;
77 
78         //States
79         string _currentStateName = "default";
80         GuiState _currentState, _targetState, _initState;
81         GuiState[string] _states;
82         Timer _timer;
83     }
84 
85     package void setScreenCoords(Vec2f screenCoords) {
86         _screenCoords = screenCoords;
87         if (_hasCanvas && _canvas !is null) {
88             _center = _size / 2f;
89             _origin = Vec2f.zero;
90         }
91         else {
92             _center = screenCoords;
93             _origin = _center - _size / 2f;
94         }
95     }
96 
97     void setCanvas(bool hasCanvas_, bool isSmooth = false) {
98         _hasCanvas = hasCanvas_;
99         if (_hasCanvas) {
100             if (_size.x > 2f && _size.y > 2f) {
101                 _canvas = new Canvas(_size, isSmooth);
102                 _canvas.position = _canvas.size / 2f;
103             }
104             else
105                 _canvas = null;
106         }
107         else {
108             _canvas = null;
109         }
110     }
111 
112     @property {
113         /// If set, the canvas used to be rendered on.
114         final Canvas canvas() {
115             return _canvas;
116         }
117         /// Is this GUI using a canvas ?
118         final bool hasCanvas() const {
119             return _hasCanvas;
120         }
121         /// Ditto
122         final bool hasCanvas(bool hasCanvas_) {
123             setCanvas(hasCanvas_);
124             return _hasCanvas;
125         }
126 
127         /// The hint window to be shown when hovering this GUI.
128         final Hint hint() {
129             return _hint;
130         }
131 
132         /// The list of all its children.
133         const(GuiElement[]) children() const {
134             return _children;
135         }
136         /// Ditto
137         GuiElement[] children() {
138             return _children;
139         }
140         /// The list of all its own children.
141         final GuiElement[] nodes() {
142             return _children;
143         }
144 
145         /// Return the first child gui.
146         GuiElement firstChild() {
147             if (!_children.length)
148                 return null;
149             return _children[0];
150         }
151 
152         /// Return the last child gui.
153         GuiElement lastChild() {
154             if (!_children.length)
155                 return null;
156             return _children[$ - 1];
157         }
158 
159         /// The number of children it currently has.
160         size_t childCount() const {
161             return _children.length;
162         }
163 
164         /// Is this gui locked ? \
165         /// Call **onLock()** on change.
166         final bool isLocked() const {
167             return _isLocked;
168         }
169         /// Ditto
170         final bool isLocked(bool isLocked_) {
171             if (isLocked_ != _isLocked) {
172                 _isLocked = isLocked_;
173                 onLock();
174                 return _isLocked;
175             }
176             return _isLocked = isLocked_;
177         }
178 
179         /// Is this gui movable ? \
180         /// Call **onMovable()** on change.
181         final bool isMovable() const {
182             return _isMovable;
183         }
184         /// Ditto
185         final bool isMovable(bool isMovable_) {
186             if (isMovable_ != _isMovable) {
187                 _isMovable = isMovable_;
188                 onMovable();
189                 return _isMovable;
190             }
191             return _isMovable = isMovable_;
192         }
193 
194         /// Is this gui hovered ? \
195         /// Call **onHover()** on change. (<- Not for now)
196         final bool isHovered() const {
197             return _isHovered;
198         }
199         /// Ditto
200         final bool isHovered(bool isHovered_) {
201             if (isHovered_ != _isHovered) {
202                 _isHovered = isHovered_;
203                 onHover();
204                 return _isHovered;
205             }
206             return _isHovered = isHovered_;
207         }
208 
209         /// Is this gui clicked ?
210         final bool isClicked() const {
211             return _isClicked;
212         }
213         /// Ditto
214         final bool isClicked(bool isClicked_) {
215             return _isClicked = isClicked_;
216         }
217 
218         /// Is this gui selected ? \
219         /// Call **onSelect()** on change.
220         final bool isSelected() const {
221             return _isSelected;
222         }
223         /// Ditto
224         final bool isSelected(bool isSelected_) {
225             if (isSelected_ != _isSelected) {
226                 _isSelected = isSelected_;
227                 onSelect();
228                 return _isSelected;
229             }
230             return _isSelected = isSelected_;
231         }
232 
233         /// Does this gui has focus ? \
234         /// Call **onFocus()** on change.
235         final bool hasFocus() const {
236             return _hasFocus;
237         }
238         /// Ditto
239         final bool hasFocus(bool hasFocus_) {
240             if (hasFocus_ != _hasFocus) {
241                 _hasFocus = hasFocus_;
242                 if (_hasFocus)
243                     setFocusedElement(this);
244                 onFocus();
245                 return _hasFocus;
246             }
247             return _hasFocus = hasFocus_;
248         }
249 
250         /// Is this gui interactable ? \
251         /// Call **onInteractable()** on change.
252         final bool isInteractable() const {
253             return _isInteractable;
254         }
255         /// Ditto
256         final bool isInteractable(bool isInteractable_) {
257             if (isInteractable_ != _isInteractable) {
258                 _isInteractable = isInteractable_;
259                 onInteractable();
260                 return _isInteractable;
261             }
262             return _isInteractable = isInteractable_;
263         }
264 
265         /// The gui position relative to its parent and alignment. \
266         /// Call **onPosition()** and **onDeltaPosition()** and **onCenter()** on change.
267         final Vec2f position() {
268             return _position;
269         }
270         /// Ditto
271         final Vec2f position(Vec2f position_) {
272             auto oldPosition = _position;
273             _position = position_;
274             onDeltaPosition(position_ - oldPosition);
275             onPosition();
276             onCenter();
277             return _position;
278         }
279 
280         /// The scale of the gui (changed by GuiState).
281         final Vec2f scale() const {
282             return _currentState.scale;
283         }
284         /// Ditto
285         final Vec2f scale(Vec2f scale_) {
286             return _currentState.scale = scale_;
287         }
288         /// The total size (size + scale) of the gui.
289         final Vec2f scaledSize() const {
290             return _size * _currentState.scale;
291         }
292 
293         /// The unscaled size of the gui. \
294         /// Call **onSize()** and **onDeltaSize()** and **onCenter()** on change. \
295         /// Resize the canvas if it was set (It reallocate the canvas, so be careful).
296         final Vec2f size() const {
297             return _size;
298         }
299         /// Ditto
300         final Vec2f size(Vec2f size_) {
301             auto oldSize = _size;
302             _size = size_ - _padding;
303 
304             if (_hasCanvas && oldSize != size_) {
305                 if (_size.x > 2f && _size.y > 2f) {
306                     _canvas = new Canvas(_size);
307                     _canvas.position = _canvas.size / 2f;
308                 }
309                 else
310                     _canvas = null;
311             }
312 
313             onDeltaSize(_size - oldSize);
314             onSize();
315             onCenter();
316             return _size;
317         }
318 
319         /// The anchor of the gui. (I don't think it's used right now). \
320         /// Call **onAnchor()** and **onDeltaAnchor()** and **onCenter()** on change.
321         final Vec2f anchor() const {
322             return _anchor;
323         }
324         /// Ditto
325         final Vec2f anchor(Vec2f anchor_) {
326             auto oldAnchor = _anchor;
327             _anchor = anchor_;
328             onDeltaAnchor(anchor_ - oldAnchor);
329             onAnchor();
330             onCenter();
331             return _anchor;
332         }
333 
334         /*
335             Old algorithm:
336             _position + _size * (Vec2f.half - _anchor);
337         */
338 
339         /// Center of the gui. \
340         /// If the canvas is set, only drawOverlay have the coordinate of its parent, the rest are in a relative coordinate.
341         final Vec2f center() const {
342             return _center;
343         }
344         /// The top left corner of the gui. \
345         /// If the canvas is set, only drawOverlay have the coordinate of its parent, the rest are in a relative coordinate.
346         final Vec2f origin() const {
347             return _origin;
348         }
349 
350         /// Extra space on top of its size. \
351         /// Call **onPadding()** and update the gui size.
352         final Vec2f padding() const {
353             return _padding;
354         }
355         /// Ditto
356         final Vec2f padding(Vec2f padding_) {
357             _padding = padding_;
358             size(_size);
359             onPadding();
360             return _padding;
361         }
362 
363         /// Color of the actual state (GuiState) of the gui. \
364         /// Call **onColor()** on change.
365         final Color color() const {
366             return _currentState.color;
367         }
368         /// Ditto
369         final Color color(Color color_) {
370             if (color_ != _currentState.color) {
371                 _currentState.color = color_;
372                 onColor();
373             }
374             return _currentState.color;
375         }
376 
377         /// Alpha of the actual state (GuiState) of the gui. \
378         /// Call **onAlpha()** on change.
379         final float alpha() const {
380             return _currentState.alpha;
381         }
382         /// Ditto
383         final float alpha(float alpha_) {
384             if (alpha_ != _currentState.alpha) {
385                 _currentState.alpha = alpha_;
386                 onAlpha();
387             }
388             return _currentState.alpha;
389         }
390 
391         /// Angle of the actual state (GuiState) of the gui.
392         final float angle() const {
393             return _currentState.angle;
394         }
395         /// Ditto
396         final float angle(float angle_) {
397             _currentState.angle = angle_;
398             onAngle();
399             return _currentState.angle;
400         }
401     }
402 
403     /// Gui initialization options
404     enum Init {
405         /// Default
406         none = 0x0,
407         /// Is focused ?
408         focus = 0x1,
409         /// Initialize the gui locked
410         locked = 0x2,
411         /// The gui can be moved around with the mouse
412         movable = 0x4,
413         /// The gui will ignore mouse events
414         notInteractable = 0x8,
415         /// The gui will receive mouse events that are destined to its children
416         eventHook = 0x10
417     }
418 
419     /// Default ctor.
420     this() {
421     }
422 
423     /// Default ctor.
424     protected final void setInitFlags(int options = Init.none) {
425         _hasFocus = cast(bool)(options & Init.focus);
426         _isLocked = cast(bool)(options & Init.locked);
427         _isMovable = cast(bool)(options & Init.movable);
428         _isInteractable = cast(bool) !(options & Init.notInteractable);
429         _hasEventHook = cast(bool)(options & Init.eventHook);
430     }
431 
432     /// Is it inside the gui ?
433     bool isInside(const Vec2f pos) const {
434         return (_screenCoords - pos).isBetween(-_size / 2f, _size / 2f);
435     }
436 
437     /// Is it inside the gui and the gui is interactable ? \
438     /// Used to capture events.
439     final bool isOnInteractableGuiElement(Vec2f pos) const {
440         if (isInside(pos))
441             return _isInteractable;
442         return false;
443     }
444 
445     /// Update the hint (Text that appear when hovering the gui) of the gui.
446     final void setHint(string text) {
447         _hint = makeHint(text);
448     }
449 
450     /// Set an id that will be sent to the specified gui when **triggerCallback()** is fired.
451     final void setCallback(GuiElement callbackGuiElement, string callback) {
452         _callbackGuiElement = callbackGuiElement;
453         _callbackId = callback;
454     }
455 
456     /// Send a previously set (with **setCallback()**) id to the previously specified gui.
457     final protected void triggerCallback() {
458         if (_callbackGuiElement !is null) {
459             _callbackGuiElement.onCallback(_callbackId);
460         }
461     }
462 
463     /// Start a transition from the current state to the specified state.
464     final void doTransitionState(string stateName) {
465         const auto ptr = stateName in _states;
466         if (!(ptr))
467             throw new Exception("No state " ~ stateName ~ " in GuiElement");
468         _currentStateName = stateName;
469         _initState = _currentState;
470         _targetState = *ptr;
471         _timer.start(_targetState.time);
472     }
473 
474     /// The gui is set *immediately* to the specified state without transition.
475     final void setState(string stateName) {
476         const auto ptr = stateName in _states;
477         if (!(ptr))
478             throw new Exception("No state " ~ stateName ~ " in GuiElement");
479         _currentStateName = stateName;
480         _initState = *ptr;
481         _targetState = *ptr;
482         _currentState = *ptr;
483     }
484 
485     /// Current state name or currently transitioning to.
486     final string getState() const {
487         return _currentStateName;
488     }
489 
490     /// Add a new state to the list.
491     final void addState(string stateName, GuiState state) {
492         _states[stateName] = state;
493     }
494 
495     /// Does this gui receive events (with **onEvent()**) ?
496     final void setEventHook(bool hasHook) {
497         _hasEventHook = hasHook;
498     }
499 
500     /// Sets the alignment relative to its parent. \
501     /// Position will be calculated from the specified alignement.
502     final void setAlign(GuiAlignX x, GuiAlignY y) {
503         _alignX = x;
504         _alignY = y;
505     }
506     /// Ditto
507     final void setAlignX(GuiAlignX x) {
508         _alignX = x;
509     }
510     /// Ditto
511     final void setAlignY(GuiAlignY y) {
512         _alignY = y;
513     }
514 
515     /// Returns the alignment relative to its parent.
516     final GuiAlignX getAlignX() {
517         return _alignX;
518     }
519     /// Ditto
520     final GuiAlignY getAlignY() {
521         return _alignY;
522     }
523 
524     /// Override this to set the gui logic.
525     /// The deltaTime is the time ratio for the last frame to the next.
526     /// It is equivallent to Actual framerate / Nominal framerate.
527     /// Ideally, it's equal to 1.
528     /// ___
529     /// If the canvas is set, the coordinate are those *inside* the canvas.
530     void update(float deltaTime) {
531         cast(void) deltaTime;
532     }
533 
534     /// Override this to render the gui itself. \
535     /// If the canvas is set, the coordinate are those *inside* the canvas.
536     void draw() {
537     }
538 
539     /// Override this to render things above the gui. \
540     /// If the canvas is set, the coordinate are those *outside* the canvas.
541     void drawOverlay() {
542     }
543 
544     /// With the eventHook, receive all events. \
545     /// Useful if you want to control like for text input. 
546     void onEvent(Event event) {
547         cast(void) event;
548     }
549 
550     /// Fired when clicked on.
551     void onSubmit() {
552     }
553 
554     /// Fired upon cancelling.
555     void onCancel() {
556     }
557 
558     /// Fired on the next tab event.
559     void onNextTab() {
560     }
561 
562     /// Fired on the previous tab event.
563     void onPreviousTab() {
564     }
565 
566     /// Fired when going up.
567     void onUp() {
568     }
569 
570     /// Fired when going down.
571     void onDown() {
572     }
573 
574     /// Fired when going left.
575     void onLeft() {
576     }
577 
578     /// Fired when going right.
579     void onRight() {
580     }
581 
582     /// Fired when the application is exiting. \
583     /// Every gui in the tree receive this.
584     void onQuit() {
585     }
586 
587     public {
588         /// Called when the lock state is changed.
589         void onLock() {
590         }
591 
592         /// Called when the movable state is changed.
593         void onMovable() {
594         }
595 
596         /// Called when the hover state is changed.
597         void onHover() {
598         }
599 
600         /// Called when the select state is changed.
601         void onSelect() {
602         }
603 
604         /// Called when the focus state is changed.
605         void onFocus() {
606         }
607 
608         /// Called when the interactable state is changed.
609         void onInteractable() {
610         }
611 
612         /// Called when the position is changed. \
613         /// The delta value is the difference with the last position.
614         void onDeltaPosition(Vec2f delta) {
615             cast(void) delta;
616         }
617 
618         /// Called when the position is changed.
619         void onPosition() {
620         }
621 
622         /// Called when the size is changed. \
623         /// The delta value is the difference with the last size.
624         void onDeltaSize(Vec2f delta) {
625             cast(void) delta;
626         }
627 
628         /// Called when the size is changed.
629         void onSize() {
630         }
631 
632         /// Called when the anchor is changed. \
633         /// The delta value is the difference with the last anchor.
634         void onDeltaAnchor(Vec2f delta) {
635             cast(void) delta;
636         }
637 
638         /// Called when the anchor is changed.
639         void onAnchor() {
640         }
641 
642         /// Called when the center of the gui moved.
643         void onCenter() {
644         }
645 
646         /// Called when the padding change.
647         void onPadding() {
648         }
649 
650         /// Called when the color change.
651         void onColor() {
652         }
653 
654         /// Called when the opacity change.
655         void onAlpha() {
656         }
657 
658         /// Called when the angle change.
659         void onAngle() {
660         }
661 
662         /// Any callback set to this gui will call this.
663         void onCallback(string id) {
664             cast(void) id;
665         }
666     }
667 
668     /// Add a gui as a child of this one.
669     void prependChild(GuiElement child) {
670         _children = child ~ _children;
671     }
672 
673     /// Add a gui as a child of this one.
674     void appendChild(GuiElement child) {
675         _children ~= child;
676     }
677 
678     /// Remove all the children.
679     void removeChildren() {
680         _children.length = 0uL;
681     }
682 
683     /// Remove the child at the specified index.
684     void removeChild(size_t index) {
685         if (!_children.length)
686             return;
687         if (index + 1u == _children.length)
688             _children.length--;
689         else if (index == 0u)
690             _children = _children[1 .. $];
691         else
692             _children = _children[0 .. index] ~ _children[index + 1 .. $];
693     }
694 
695     /// Remove the specified child.
696     void removeChild(GuiElement gui) {
697         foreach (size_t i, GuiElement child; _children) {
698             if (child is gui) {
699                 removeChild(i);
700                 return;
701             }
702         }
703     }
704 
705     /// Unregister itself from its parent or root.
706     void removeSelf() {
707         _isRegistered = false;
708     }
709 }