1 /**
2     Gui Manager
3 
4     Copyright: (c) Enalye 2019
5     License: Zlib
6     Authors: Enalye
7 */
8 
9 module atelier.ui.gui_manager;
10 
11 import std.conv : to;
12 import atelier.core, atelier.common, atelier.render;
13 import atelier.ui.gui_element, atelier.ui.gui_overlay, atelier.ui.gui_modal;
14 
15 private {
16     bool _isGuiElementDebug = false;
17     GuiElement[] _rootElements;
18     float _deltaTime;
19 }
20 
21 //-- Public ---
22 
23 /// Add a gui as a top gui (not a child of anything).
24 void prependRoot(GuiElement gui) {
25     _rootElements = gui ~ _rootElements;
26 }
27 
28 /// Add a gui as a top gui (not a child of anything).
29 void appendRoot(GuiElement gui) {
30     _rootElements ~= gui;
31 }
32 
33 /// Remove all the top gui (that aren't a child of anything).
34 void removeRoots() {
35     //_isChildGrabbed = false;
36     _rootElements.length = 0uL;
37 }
38 
39 /// Set those gui as the top guis (replacing the previous ones).
40 void setRoots(GuiElement[] widgets) {
41     _rootElements = widgets;
42 }
43 
44 /// Get all the root gui.
45 GuiElement[] getRoots() {
46     return _rootElements;
47 }
48 
49 /// Show every the hitbox of every gui element.
50 void setDebugGui(bool isDebug) {
51     _isGuiElementDebug = isDebug;
52 }
53 
54 /// Remove the specified gui from roots.
55 void removeRoot(GuiElement gui) {
56     foreach (size_t i, GuiElement child; _rootElements) {
57         if (child is gui) {
58             removeRoot(i);
59             return;
60         }
61     }
62 }
63 
64 /// Remove the gui at the specified index from roots.
65 void removeRoot(size_t index) {
66     if (!_rootElements.length)
67         return;
68     if (index + 1u == _rootElements.length)
69         _rootElements.length--;
70     else if (index == 0u)
71         _rootElements = _rootElements[1 .. $];
72     else
73         _rootElements = _rootElements[0 .. index] ~ _rootElements[index + 1 .. $];
74 }
75 
76 //-- Internal ---
77 
78 /// Update all the guis from the root.
79 package(atelier) void updateRoots(float deltaTime) {
80     _deltaTime = deltaTime;
81     size_t index = 0;
82     while (index < _rootElements.length) {
83         if (_rootElements[index]._isRegistered) {
84             updateRoots(_rootElements[index], null);
85             index++;
86         }
87         else {
88             removeRoot(index);
89         }
90     }
91 }
92 
93 /// Draw all the guis from the root.
94 package(atelier) void drawRoots() {
95     foreach_reverse (GuiElement widget; _rootElements) {
96         drawRoots(widget);
97     }
98 }
99 
100 private {
101     bool _hasClicked, _wasHoveredGuiElementAlreadyHovered;
102     GuiElement _clickedGuiElement;
103     GuiElement _focusedGuiElement;
104     GuiElement _hoveredGuiElement;
105     GuiElement _grabbedGuiElement, _tempGrabbedGuiElement;
106     Canvas _canvas;
107     Vec2f _clickedGuiElementEventPosition = Vec2f.zero;
108     Vec2f _hoveredGuiElementEventPosition = Vec2f.zero;
109     Vec2f _grabbedGuiElementEventPosition = Vec2f.zero;
110     GuiElement[] _hookedGuis;
111 }
112 
113 /// Dispatch global events on the guis from the root. \
114 /// Called by the main event loop.
115 package(atelier) void handleGuiElementEvent(Event event) {
116     if (isOverlay()) {
117         processOverlayEvent(event);
118     }
119 
120     _hasClicked = false;
121     switch (event.type) with (Event.Type) {
122     case mouseDown:
123         _tempGrabbedGuiElement = null;
124         dispatchMouseDownEvent(null, event.mouse.position);
125 
126         if (_tempGrabbedGuiElement) {
127             _grabbedGuiElement = _tempGrabbedGuiElement;
128         }
129 
130         if (_hasClicked && _clickedGuiElement !is null) {
131             _clickedGuiElement.isClicked = true;
132             Event guiEvent = Event.Type.mouseDown;
133             guiEvent.mouse.position = _clickedGuiElementEventPosition;
134             _clickedGuiElement.onEvent(guiEvent);
135         }
136         break;
137     case mouseUp:
138         _grabbedGuiElement = null;
139         dispatchMouseUpEvent(null, event.mouse.position);
140         break;
141     case mouseUpdate:
142         _hookedGuis.length = 0;
143         dispatchMouseUpdateEvent(null, event.mouse.position);
144 
145         if (_hasClicked && _hoveredGuiElement !is null) {
146             _hoveredGuiElement.isHovered = true;
147 
148             if (!_wasHoveredGuiElementAlreadyHovered)
149                 _hoveredGuiElement.onHover();
150 
151             //Compatibility
152             Event guiEvent = Event.Type.mouseUpdate;
153             guiEvent.mouse.position = _hoveredGuiElementEventPosition;
154             _hoveredGuiElement.onEvent(guiEvent);
155         }
156         break;
157     case mouseWheel:
158         dispatchMouseWheelEvent(event.scroll.delta);
159         break;
160     case quit:
161         dispatchQuitEvent(null);
162         if (isModal()) {
163             stopAllModals();
164             dispatchQuitEvent(null);
165         }
166         break;
167     default:
168         dispatchGenericEvents(null, event);
169         break;
170     }
171 }
172 
173 /// Update all children of a gui. \
174 /// Called by the application itself.
175 package(atelier) void updateRoots(GuiElement gui, GuiElement parent) {
176     Vec2f coords = Vec2f.zero;
177 
178     //Calculate transitions
179     if (gui._timer.isRunning) {
180         gui._timer.update(_deltaTime);
181         const float t = gui._targetState.easing(gui._timer.value01);
182         gui._currentState.offset = lerp(gui._initState.offset, gui._targetState.offset, t);
183 
184         gui._currentState.scale = lerp(gui._initState.scale, gui._targetState.scale, t);
185 
186         gui._currentState.color = lerp(gui._initState.color, gui._targetState.color, t);
187 
188         gui._currentState.alpha = lerp(gui._initState.alpha, gui._targetState.alpha, t);
189 
190         gui._currentState.angle = lerp(gui._initState.angle, gui._targetState.angle, t);
191         gui.onColor();
192         if (!gui._timer.isRunning) {
193             if (gui._targetState.callback.length)
194                 gui.onCallback(gui._targetState.callback);
195         }
196     }
197 
198     //Calculate gui location
199     const Vec2f offset = gui._position + (
200             gui._size * gui._currentState.scale / 2f) + gui._currentState.offset;
201     if (parent !is null) {
202         if (parent.hasCanvas && parent.canvas !is null) {
203             if (gui._alignX == GuiAlignX.left)
204                 coords.x = offset.x;
205             else if (gui._alignX == GuiAlignX.right)
206                 coords.x = (parent._size.x * parent._currentState.scale.x) - offset.x;
207             else
208                 coords.x = (parent._size.x * parent._currentState.scale.x) / 2f
209                     + gui._currentState.offset.x + gui.position.x;
210 
211             if (gui._alignY == GuiAlignY.top)
212                 coords.y = offset.y;
213             else if (gui._alignY == GuiAlignY.bottom)
214                 coords.y = (parent._size.y * parent._currentState.scale.y) - offset.y;
215             else
216                 coords.y = (parent._size.y * parent._currentState.scale.y) / 2f
217                     + gui._currentState.offset.y + gui.position.y;
218         }
219         else {
220             if (gui._alignX == GuiAlignX.left)
221                 coords.x = parent.origin.x + offset.x;
222             else if (gui._alignX == GuiAlignX.right)
223                 coords.x = parent.origin.x + (
224                         parent._size.x * parent._currentState.scale.x) - offset.x;
225             else
226                 coords.x = parent.center.x + gui._currentState.offset.x + gui.position.x;
227 
228             if (gui._alignY == GuiAlignY.top)
229                 coords.y = parent.origin.y + offset.y;
230             else if (gui._alignY == GuiAlignY.bottom)
231                 coords.y = parent.origin.y + (
232                         parent._size.y * parent._currentState.scale.y) - offset.y;
233             else
234                 coords.y = parent.center.y + gui._currentState.offset.y + gui.position.y;
235         }
236     }
237     else {
238         if (gui._alignX == GuiAlignX.left)
239             coords.x = offset.x;
240         else if (gui._alignX == GuiAlignX.right)
241             coords.x = getWindowWidth() - offset.x;
242         else
243             coords.x = getWindowCenter().x + gui._currentState.offset.x + gui.position.x;
244 
245         if (gui._alignY == GuiAlignY.top)
246             coords.y = offset.y;
247         else if (gui._alignY == GuiAlignY.bottom)
248             coords.y = getWindowHeight() - offset.y;
249         else
250             coords.y = getWindowCenter().y + gui._currentState.offset.y + gui.position.y;
251     }
252     gui.setScreenCoords(coords);
253     gui.update(_deltaTime);
254 
255     size_t childIndex = 0;
256     while (childIndex < gui.nodes.length) {
257         if (gui.nodes[childIndex]._isRegistered) {
258             updateRoots(gui.nodes[childIndex], gui);
259             childIndex++;
260         }
261         else {
262             gui.removeChild(childIndex);
263         }
264     }
265 }
266 
267 /// Renders a gui and all its children.
268 void drawRoots(GuiElement gui) {
269     if (gui.hasCanvas && gui.canvas !is null) {
270         auto canvas = gui.canvas;
271         canvas.color(gui._currentState.color);
272         canvas.alpha(gui._currentState.alpha);
273         pushCanvas(canvas, true);
274         gui.draw();
275         foreach (GuiElement child; gui.nodes) {
276             drawRoots(child);
277         }
278         popCanvas();
279         canvas.draw(transformRenderSpace(gui._screenCoords),
280                 transformScale() * cast(Vec2f) canvas.renderSize(), Vec4i(0, 0,
281                     canvas.width, canvas.height), gui._currentState.angle, Flip.none, Vec2f.half);
282         const auto origin = gui._origin;
283         const auto center = gui._center;
284         gui._origin = gui._screenCoords - (gui._size * gui._currentState.scale) / 2f;
285         gui._center = gui._screenCoords;
286         gui.drawOverlay();
287         gui._origin = origin;
288         gui._center = center;
289         if (gui.isHovered && gui.hint !is null)
290             openHintWindow(gui.hint);
291     }
292     else {
293         gui.draw();
294         foreach (GuiElement child; gui.nodes) {
295             drawRoots(child);
296         }
297         gui.drawOverlay();
298         if (gui.isHovered && gui.hint !is null)
299             openHintWindow(gui.hint);
300     }
301     if (_isGuiElementDebug) {
302         drawRect(gui.center - (gui._size * gui._currentState.scale) / 2f,
303                 gui._size * gui._currentState.scale, gui.isHovered ? Color.red
304                 : (gui.nodes.length ? Color.blue : Color.green));
305     }
306 }
307 
308 /// Process a mouse down event down the tree.
309 private void dispatchMouseDownEvent(GuiElement gui, Vec2f cursorPosition) {
310     auto children = (gui is null) ? _rootElements : gui.nodes;
311     bool hasCanvas;
312 
313     if (gui !is null) {
314         if (gui.isInteractable && gui.isInside(cursorPosition)) {
315             _clickedGuiElement = gui;
316             _tempGrabbedGuiElement = null;
317 
318             if (gui.hasCanvas && gui.canvas !is null) {
319                 hasCanvas = true;
320                 pushCanvas(gui.canvas, false);
321                 cursorPosition = transformCanvasSpace(cursorPosition, gui._screenCoords);
322             }
323 
324             _clickedGuiElementEventPosition = cursorPosition;
325             _hasClicked = true;
326 
327             if (gui._hasEventHook) {
328                 Event guiEvent = Event.Type.mouseDown;
329                 guiEvent.mouse.position = cursorPosition;
330                 gui.onEvent(guiEvent);
331             }
332 
333             if (gui._isMovable && !_grabbedGuiElement) {
334                 _tempGrabbedGuiElement = gui;
335                 _grabbedGuiElementEventPosition = _clickedGuiElementEventPosition;
336             }
337         }
338         else
339             return;
340     }
341 
342     foreach (child; children)
343         dispatchMouseDownEvent(child, cursorPosition);
344 
345     if (hasCanvas)
346         popCanvas();
347 }
348 
349 /// Process a mouse up event down the tree.
350 private void dispatchMouseUpEvent(GuiElement gui, Vec2f cursorPosition) {
351     auto children = (gui is null) ? _rootElements : gui.nodes;
352     bool hasCanvas;
353 
354     if (gui !is null) {
355         if (gui.isInteractable && gui.isInside(cursorPosition)) {
356             if (gui.hasCanvas && gui.canvas !is null) {
357                 hasCanvas = true;
358                 pushCanvas(gui.canvas, false);
359                 cursorPosition = transformCanvasSpace(cursorPosition, gui._screenCoords);
360             }
361 
362             if (gui._hasEventHook) {
363                 Event guiEvent = Event.Type.mouseUp;
364                 guiEvent.mouse.position = cursorPosition;
365                 gui.onEvent(guiEvent);
366             }
367         }
368         else
369             return;
370     }
371 
372     foreach (child; children)
373         dispatchMouseUpEvent(child, cursorPosition);
374 
375     if (hasCanvas)
376         popCanvas();
377 
378     if (gui !is null && _clickedGuiElement == gui) {
379         //The previous widget is now unfocused.
380         if (_focusedGuiElement !is null) {
381             _focusedGuiElement.hasFocus = false;
382         }
383 
384         //The widget is now focused and receive the onSubmit event.
385         _focusedGuiElement = _clickedGuiElement;
386         _hasClicked = true;
387         gui.hasFocus = true;
388         gui.onSubmit();
389 
390         //Compatibility
391         Event event = Event.Type.mouseUp;
392         event.mouse.position = cursorPosition;
393         gui.onEvent(event);
394     }
395     if (_clickedGuiElement !is null)
396         _clickedGuiElement.isClicked = false;
397 }
398 
399 package void setFocusedElement(GuiElement gui) {
400     if (_focusedGuiElement == gui)
401         return;
402     //The previous widget is now unfocused.
403     if (_focusedGuiElement !is null) {
404         _focusedGuiElement.hasFocus = false;
405     }
406     _focusedGuiElement = gui;
407 }
408 
409 /// Process a mouse update event down the tree.
410 private void dispatchMouseUpdateEvent(GuiElement gui, Vec2f cursorPosition) {
411     auto children = (gui is null) ? _rootElements : gui.nodes;
412     bool hasCanvas, wasHovered;
413 
414     if (gui !is null) {
415         wasHovered = gui.isHovered;
416 
417         if (gui.isInteractable && gui == _grabbedGuiElement) {
418             if (!gui._isMovable) {
419                 _grabbedGuiElement = null;
420             }
421             else {
422                 if (gui.hasCanvas && gui.canvas !is null) {
423                     pushCanvas(gui.canvas, false);
424                     cursorPosition = transformCanvasSpace(cursorPosition, gui._screenCoords);
425                 }
426                 Vec2f deltaPosition = (cursorPosition - _grabbedGuiElementEventPosition);
427                 if (gui._alignX == GuiAlignX.right)
428                     deltaPosition.x = -deltaPosition.x;
429                 if (gui._alignY == GuiAlignY.bottom)
430                     deltaPosition.y = -deltaPosition.y;
431                 gui._position += deltaPosition;
432                 if (gui.hasCanvas && gui.canvas !is null)
433                     popCanvas();
434                 else
435                     _grabbedGuiElementEventPosition = cursorPosition;
436             }
437         }
438 
439         if (gui.isInteractable && gui.isInside(cursorPosition)) {
440             if (gui.hasCanvas && gui.canvas !is null) {
441                 hasCanvas = true;
442                 pushCanvas(gui.canvas, false);
443                 cursorPosition = transformCanvasSpace(cursorPosition, gui._screenCoords);
444             }
445 
446             //Register gui
447             _wasHoveredGuiElementAlreadyHovered = wasHovered;
448             _hoveredGuiElement = gui;
449             _hoveredGuiElementEventPosition = cursorPosition;
450             _hasClicked = true;
451 
452             if (gui._hasEventHook) {
453                 Event guiEvent = Event.Type.mouseUpdate;
454                 guiEvent.mouse.position = cursorPosition;
455                 gui.onEvent(guiEvent);
456                 _hookedGuis ~= gui;
457             }
458         }
459         else {
460             void unHoverRoots(GuiElement gui) {
461                 gui.isHovered = false;
462                 foreach (child; gui.nodes)
463                     unHoverRoots(child);
464             }
465 
466             unHoverRoots(gui);
467             return;
468         }
469     }
470 
471     foreach (child; children)
472         dispatchMouseUpdateEvent(child, cursorPosition);
473 
474     if (hasCanvas)
475         popCanvas();
476 }
477 
478 /// Process a mouse wheel event down the tree.
479 private void dispatchMouseWheelEvent(Vec2f scroll) {
480     Event scrollEvent = Event.Type.mouseWheel;
481     scrollEvent.scroll.delta = scroll;
482 
483     foreach (gui; _hookedGuis) {
484         gui.onEvent(scrollEvent);
485     }
486 
487     if (_clickedGuiElement !is null) {
488         if (_clickedGuiElement.isClicked) {
489             _clickedGuiElement.onEvent(scrollEvent);
490             return;
491         }
492     }
493     if (_hoveredGuiElement !is null) {
494         _hoveredGuiElement.onEvent(scrollEvent);
495         return;
496     }
497 }
498 
499 /// Notify every gui in the tree that we are leaving.
500 private void dispatchQuitEvent(GuiElement gui) {
501     if (gui !is null) {
502         foreach (GuiElement child; gui.nodes)
503             dispatchQuitEvent(child);
504         gui.onQuit();
505     }
506     else {
507         foreach (GuiElement widget; _rootElements)
508             dispatchQuitEvent(widget);
509     }
510 }
511 
512 /// Every other event that doesn't have a specific behavior like mouse events.
513 private void dispatchGenericEvents(GuiElement gui, Event event) {
514     if (gui !is null) {
515         gui.onEvent(event);
516         foreach (GuiElement child; gui.nodes) {
517             dispatchGenericEvents(child, event);
518         }
519     }
520     else {
521         foreach (GuiElement widget; _rootElements) {
522             dispatchGenericEvents(widget, event);
523         }
524     }
525 }
526 /*
527 private void handleGuiElementEvents(GuiElement gui) {
528     switch (event.type) with(Event.Type) {
529     case MouseDown:
530         bool hasClickedGuiElement;
531         foreach(uint id, GuiElement widget; _children) {
532             widget.hasFocus = false;
533             if(!widget.isInteractable)
534                 continue;
535 
536             if(!hasClickedGuiElement && widget.isInside(_isFrame ? transformCanvasSpace(event.mouse.position, _position) : event.mouse.position)) {
537                 widget.hasFocus = true;
538                 widget.isSelected = true;
539                 widget.isHovered = true;
540                 _isChildGrabbed = true;
541                 _idChildGrabbed = id;
542 
543                 if(_isFrame)
544                     event.mouse.position = transformCanvasSpace(event.mouse.position, _position);
545                 widget.onEvent(event);
546                 hasClickedGuiElement = true;
547             }
548         }
549 
550         if(!_isChildGrabbed && _isMovable) {
551             _isGrabbed = true;
552             _lastMousePos = event.mouse.position;
553         }
554         break;
555     case MouseUp:
556         if(_isChildGrabbed) {
557             _isChildGrabbed = false;
558             _children[_idChildGrabbed].isSelected = false;
559 
560             if(_isFrame)
561                 event.mouse.position = transformCanvasSpace(event.mouse.position, _position);
562             _children[_idChildGrabbed].onEvent(event);
563         }
564         else {
565             _isGrabbed = false;
566         }
567         break;
568     case MouseUpdate:
569         _isIterating = false; //Use mouse control
570         Vec2f mousePosition = event.mouse.position;
571         if(_isFrame)
572             event.mouse.position = transformCanvasSpace(event.mouse.position, _position);
573 
574         _isChildHovered = false;
575         foreach(uint id, GuiElement widget; _children) {
576             if(isHovered) {
577                 widget.isHovered = widget.isInside(event.mouse.position);
578                 if(widget.isHovered && widget.isInteractable) {
579                     _isChildHovered = true;
580                     widget.onEvent(event);
581                 }
582             }
583             else
584                 widget.isHovered = false;
585         }
586 
587         if(_isChildGrabbed && !_children[_idChildGrabbed].isHovered)
588             _children[_idChildGrabbed].onEvent(event);
589         else if(_isGrabbed && _isMovable) {
590             Vec2f deltaPosition = (mousePosition - _lastMousePos);
591             if(!_isFrame) {
592                 //Clamp the window in the screen
593                 if(isModal()) {
594                     Vec2f halfSize = _size / 2f;
595                     Vec2f clampedPosition = _position.clamp(halfSize, screenSize - halfSize);
596                     deltaPosition += (clampedPosition - _position);
597                 }
598                 _position += deltaPosition;
599 
600                 foreach(widget; _children)
601                     widget.position = widget.position + deltaPosition;
602             }
603             else
604                 _position += deltaPosition;
605             _lastMousePos = mousePosition;
606         }
607         break;
608     case MouseWheel:
609         foreach(uint id, GuiElement widget; _children) {
610             if(widget.isHovered)
611                 widget.onEvent(event);
612         }
613 
614         if(_isChildGrabbed && !_children[_idChildGrabbed].isHovered)
615             _children[_idChildGrabbed].onEvent(event);
616         break;
617     default:
618         foreach(GuiElement widget; _children)
619             widget.onEvent(event);
620         break;
621     }
622 }*/