1 module rpui.view;
2 
3 import std.path;
4 import std.file;
5 import std.container.array;
6 import std.container.slist;
7 
8 import gapi.vec;
9 import gapi.opengl;
10 import gapi.shader;
11 import gapi.camera;
12 
13 import rpui.platform;
14 import rpui.input;
15 import rpui.cursor;
16 import rpui.widget;
17 import rpui.events_observer;
18 import rpui.events;
19 import rpui.widget_events;
20 import rpui.primitives;
21 import rpui.math;
22 import rpui.theme;
23 import rpui.render.components : CameraView;
24 import rpui.resources.strings;
25 import rpui.resources.images;
26 import rpui.resources.icons;
27 
28 struct ViewResources {
29     StringsRes strings;
30     ImagesRes images;
31     IconsRes icons;
32 }
33 
34 ViewResources createViewResources(in string theme) {
35     auto images = new ImagesRes(theme);
36     auto icons = new IconsRes(images);
37 
38     icons.addIcons("icons", "icons.rdl");
39 
40     return ViewResources(
41         new StringsRes(),
42         images,
43         icons
44     );
45 }
46 
47 final class View : EventsListenerEmpty {
48     Theme theme;
49     EventsObserver events;
50     package Array!Widget onProgressQueries;
51 
52     private Widget p_widgetUnderMouse = null;
53     @property Widget widgetUnderMouse() { return p_widgetUnderMouse; }
54 
55     private Subscriber rootWidgetSubscriber;
56 
57     private uint lastIndex = 0;
58     package Widget rootWidget;
59     package Array!Widget frontWidgets;  // This widgets are drawn last.
60     package Array!Widget frontWidgetsOrdering;  // This widgets are process firstly.
61     package Widget focusedWidget = null;
62     package Array!Widget widgetOrdering;
63     package Array!Widget unfocusedWidgets;
64 
65     package SList!Widget freezeSources;
66     package SList!bool isNestedFreezeStack;
67 
68     public CursorIcon cursor = CursorIcon.inherit;
69     package vec2i mousePos = vec2i(-1, -1);
70     package vec2i mouseClickPos = vec2i(-1, -1);
71     private Array!Rect scissorStack;
72     private uint viewportHeight;
73 
74     public @property inout(CameraView) cameraView() inout { return cameraView_; }
75     package CameraView cameraView_;
76 
77     package ViewResources resources;
78     private CursorManager cursorManager;
79 
80     private void* window;
81 
82     private CameraMatrices screenCameraMatrices;
83     private OthroCameraTransform screenCameraTransform = {
84         viewportSize: vec2(1024, 768),
85         position: vec2(0, 0),
86         zoom: 1f
87     };
88 
89     private this() {
90         events = new EventsObserver();
91     }
92 
93     this(void* window, in string themeName, CursorManager cursorManager, ViewResources resources) {
94         with (rootWidget = new Widget(this)) {
95             isOver = true;
96             finalFocus = true;
97         }
98 
99         events = new EventsObserver();
100         events.join(rootWidget.events);
101 
102         theme = createThemeByName(themeName);
103         this.resources = resources;
104         this.cursorManager = cursorManager;
105         this.window = window;
106     }
107 
108     this(void* window, in string themeName) {
109         with (rootWidget = new Widget(this)) {
110             isOver = true;
111             finalFocus = true;
112         }
113 
114         events = new EventsObserver();
115         events.join(rootWidget.events);
116 
117         theme = createThemeByName(themeName);
118         this.resources = createViewResources(themeName);
119         this.window = window;
120     }
121 
122     /// Invokes all `onProgress` of all widgets and `poll` widgets.
123     void onProgress(in ProgressEvent event) {
124         screenCameraMatrices = createOrthoCameraMatrices(screenCameraTransform);
125         cursor = CursorIcon.inherit;
126 
127         onProgressQueries.clear();
128         rootWidget.collectOnProgressQueries();
129 
130         foreach (Widget widget; frontWidgets) {
131             if (!widget.isVisible && !widget.processPorgress())
132                 continue;
133 
134             widget.collectOnProgressQueries();
135         }
136 
137         blur();
138 
139         foreach (Widget widget; onProgressQueries) {
140             widget.onProgress(event);
141         }
142 
143         foreach_reverse (Widget widget; onProgressQueries) {
144             widget.onProgress(event);
145         }
146 
147         poll();
148 
149         foreach (Widget widget; frontWidgets) {
150             if (!widget.isVisible && !widget.processPorgress())
151                 continue;
152 
153             if (widget.isOver)
154                 cursor = CursorIcon.inherit;
155         }
156 
157         rootWidget.updateAll();
158         cursorManager.setIcon(cursor);
159     }
160 
161     /// Renders all widgets inside `camera` view.
162     void onRender() {
163         cameraView_.mvpMatrix = screenCameraMatrices.mvpMatrix;
164         cameraView_.viewportWidth = screenCameraTransform.viewportSize.x;
165         cameraView_.viewportHeight = screenCameraTransform.viewportSize.y;
166 
167         rootWidget.size.x = screenCameraTransform.viewportSize.x;
168         rootWidget.size.y = screenCameraTransform.viewportSize.y;
169 
170         rootWidget.onRender();
171 
172         foreach (Widget widget; frontWidgets) {
173             if (widget.isVisible) {
174                 widget.onRender();
175             }
176         }
177     }
178 
179     /**
180      * Determines widgets states - check when widget `isEnter` (i.e. mouse inside widget area);
181      * `isClick` (when user clicked to widget) and when widget is over i.e. mouse inside widget area
182      * but widget can be overlapped by another widget.
183      */
184     private void poll() {
185         rootWidget.isOver = true;
186         auto widgetsOrderingChain = widgetOrdering ~ frontWidgetsOrdering;
187 
188         foreach (Widget widget; widgetsOrderingChain) {
189             if (widget is null)
190                 continue;
191 
192             if (!widget.isVisible || !widget.isEnabled) {
193                 widget.isOver = false;
194                 widget.isEnter = false;
195                 widget.isClick = false;
196                 continue;
197             }
198 
199             if (!isWidgetFrozen(widget)) {
200                 widget.onCursor();
201             }
202 
203             widget.isEnter = false;
204 
205             const size = vec2(
206                 widget.overSize.x > 0 ? widget.overSize.x : widget.size.x,
207                 widget.overSize.y > 0 ? widget.overSize.y : widget.size.y
208             );
209 
210             Rect rect;
211 
212             if (widget.overlayRect == emptyRect) {
213                 rect = Rect(widget.absolutePosition, size);
214             } else {
215                 rect = widget.overlayRect;
216             }
217 
218             widget.isOver = widget.parent.isOver && pointInRect(mousePos, rect);
219         }
220 
221         p_widgetUnderMouse = null;
222         Widget found = null;
223 
224         foreach_reverse (Widget widget; widgetsOrderingChain) {
225             if (found !is null && !widget.overlay)
226                 continue;
227 
228             if (widget is null || !widget.isOver || !widget.isVisible)
229                 continue;
230 
231             if (isWidgetFrozen(widget))
232                 continue;
233 
234             if (found !is null) {
235                 found.isEnter = false;
236                 found.isClick = false;
237             }
238 
239             if (widget.pointIsEnter(mousePos)) {
240                 widget.isEnter = true;
241                 p_widgetUnderMouse = widget;
242                 found = widget;
243 
244                 if (cursor == CursorIcon.inherit) {
245                     cursor = widget.cursor;
246                 }
247 
248                 break;
249             }
250         }
251     }
252 
253     /// Add `widget` to root children.
254     void addWidget(Widget widget) {
255         rootWidget.children.addWidget(widget);
256     }
257 
258     /// Delete `widget` from root children.
259     void deleteWidget(Widget widget) {
260         rootWidget.children.deleteWidget(widget);
261     }
262 
263     /// Delete widget by `id` from root children.
264     void deleteWidget(in size_t id) {
265         rootWidget.children.deleteWidget(id);
266     }
267 
268     /// Push scissor to stack.
269     package void pushScissor(in Rect scissor) {
270         if (scissorStack.length == 0)
271             glEnable(GL_SCISSOR_TEST);
272 
273         scissorStack.insertBack(scissor);
274         applyScissor();
275     }
276 
277     /// Pop scissor from stack.
278     package void popScissor() {
279         scissorStack.removeBack(1);
280 
281         if (scissorStack.length == 0) {
282             glDisable(GL_SCISSOR_TEST);
283         } else {
284             applyScissor();
285         }
286     }
287 
288     /// Apply all scissors for clipping widgets in scissors areas.
289     Rect applyScissor() {
290         FrameRect currentScissor = scissorStack.back.absolute;
291 
292         if (scissorStack.length >= 2) {
293             foreach (Rect scissor; scissorStack) {
294                 if (currentScissor.left < scissor.absolute.left)
295                     currentScissor.left = scissor.absolute.left;
296 
297                 if (currentScissor.top < scissor.absolute.top)
298                     currentScissor.top = scissor.absolute.top;
299 
300                 if (currentScissor.right > scissor.absolute.right)
301                     currentScissor.right = scissor.absolute.right;
302 
303                 if (currentScissor.bottom > scissor.absolute.bottom)
304                     currentScissor.bottom = scissor.absolute.bottom;
305             }
306         }
307 
308         auto screenScissor = IntRect(currentScissor);
309         screenScissor.top = viewportHeight - screenScissor.top - screenScissor.height;
310         glScissor(screenScissor.left, screenScissor.top, screenScissor.width, screenScissor.height);
311 
312         return Rect(currentScissor);
313     }
314 
315     /// Focusing next widget after the current focused widget.
316     void focusNext() {
317         if (focusedWidget !is null)
318             focusedWidget.focusNavigator.focusNext();
319     }
320 
321     /// Focusing previous widget before the current focused widget.
322     void focusPrev() {
323         if (focusedWidget !is null)
324             focusedWidget.focusNavigator.focusPrev();
325     }
326 
327 // Events ------------------------------------------------------------------------------------------
328 
329     /**
330      * Root widget to handle all events such as `onKeyPressed`, `onKeyReleased` etc.
331      * Default is `rootWidget` but if UI was freeze by some widget (e.g. dialog window)
332      * then source will be top of freeze sources stack.
333      */
334     @property
335     private Widget eventRootWidget() {
336         return freezeSources.empty ? rootWidget : freezeSources.front;
337     }
338 
339     override void onKeyPressed(in KeyPressedEvent event) {
340         if (focusedWidget !is null && isClickKey(event.key)) {
341             focusedWidget.isClick = true;
342         }
343     }
344 
345     override void onKeyReleased(in KeyReleasedEvent event) {
346         if (focusedWidget !is null && isClickKey(event.key)) {
347             focusedWidget.isClick = false;
348             focusedWidget.onClickActionInvoked();
349             focusedWidget.events.notify(ClickEvent());
350             focusedWidget.events.notify(ClickActionInvokedEvent());
351         }
352     }
353 
354     override void onMouseDown(in MouseDownEvent event) {
355         mouseClickPos.x = event.x;
356         mouseClickPos.y = event.y;
357 
358         foreach_reverse (Widget widget; widgetOrdering) {
359             if (widget is null || isWidgetFrozen(widget))
360                 continue;
361 
362             if (widget.isEnter) {
363                 widget.isClick = true;
364                 widget.isMouseDown = true;
365 
366                 if (!widget.focusOnMousUp)
367                     widget.focus();
368 
369                 break;
370             }
371         }
372     }
373 
374     override void onMouseUp(in MouseUpEvent event) {
375         foreach_reverse (Widget widget; widgetOrdering) {
376             if (widget is null || isWidgetFrozen(widget))
377                 continue;
378 
379             if (widget.isEnter && widget.focusOnMousUp && widget.isMouseDown)
380                 widget.focus();
381 
382             widget.isClick = false;
383             widget.isMouseDown = false;
384         }
385     }
386 
387     override void onMouseWheel(in MouseWheelEvent event) {
388         int horizontalDelta = event.dx;
389         int verticalDelta = event.dy;
390 
391         if (isKeyPressed(KeyCode.Shift)) { // Inverse
392             horizontalDelta = event.dy;
393             verticalDelta = event.dx;
394         }
395 
396         Scrollable scrollable = null;
397         Widget widget = widgetUnderMouse;
398 
399         // Find first scrollable widget
400         while (scrollable is null && widget !is null) {
401             if (isWidgetFrozen(widget))
402                 continue;
403 
404             scrollable = cast(Scrollable) widget;
405             widget = widget.parent;
406         }
407 
408         if (scrollable !is null)
409             scrollable.onMouseWheelHandle(horizontalDelta, verticalDelta);
410     }
411 
412     override void onMouseMove(in MouseMoveEvent event) {
413         mousePos.x = event.x;
414         mousePos.y = event.y;
415     }
416 
417     override void onWindowResize(in WindowResizeEvent event) {
418         viewportHeight = event.height;
419         screenCameraTransform.viewportSize.x = event.width;
420         screenCameraTransform.viewportSize.y = event.height;
421 
422         onProgress(ProgressEvent(0));
423         onRender();
424     }
425 
426     private void blur() {
427         foreach (Widget widget; unfocusedWidgets) {
428             widget.p_isFocused = false;
429             widget.events.notify(BlurEvent());
430         }
431 
432         unfocusedWidgets.clear();
433     }
434 
435     void moveWidgetToFront(Widget widget) {
436 
437         void moveChildrensToFrontOrdering(Widget parentWidget) {
438             frontWidgetsOrdering.insert(parentWidget);
439 
440             foreach (Widget child; parentWidget.children) {
441                 moveChildrensToFrontOrdering(child);
442             }
443         }
444 
445         frontWidgets.insert(widget);
446         moveChildrensToFrontOrdering(widget);
447         widget.parent.children.deleteWidget(widget);
448         widget.p_parent = rootWidget;
449     }
450 
451     @property bool isNestedFreeze() {
452         return !isNestedFreezeStack.empty && isNestedFreezeStack.front;
453     }
454 
455     uint getNextIndex() {
456         ++lastIndex  ;
457         return lastIndex;
458     }
459 
460     /**
461      * Freez UI except `widget`.
462      * If `nestedFreeze` is true then will be frozen all children of widget.
463      */
464     void freezeUI(Widget widget, in bool nestedFreeze = true) {
465         silentPreviousEventsEmitter(widget);
466         freezeSources.insert(widget);
467         isNestedFreezeStack.insert(nestedFreeze);
468         events.join(widget.events);
469     }
470 
471     /**
472      * Unfreeze UI where source of freezing is `widget`.
473      */
474     void unfreezeUI(Widget widget) {
475         if (!freezeSources.empty && freezeSources.front == widget) {
476             freezeSources.removeFront();
477             isNestedFreezeStack.removeFront();
478             unsilentPreviousEventsEmitter(widget);
479             events.unjoin(widget.events);
480         }
481     }
482 
483     private void silentPreviousEventsEmitter(Widget widget) {
484         if (freezeSources.empty) {
485             events.silent(rootWidget.events);
486         } else {
487             events.silent(freezeSources.front.events);
488         }
489     }
490 
491     private void unsilentPreviousEventsEmitter(Widget widget) {
492         if (freezeSources.empty) {
493             events.unsilent(rootWidget.events);
494         } else {
495             events.unsilent(freezeSources.front.events);
496         }
497     }
498 
499     /**
500      * Returns true if the `widget` is frozen.
501      * If not `isNestedFreeze` then check if `widget` inside freezing source
502      * And if `widget` has source parent then this widget is not frozen.
503      */
504     bool isWidgetFrozen(Widget widget) {
505         if (freezeSources.empty || freezeSources.front == widget)
506             return false;
507 
508         if (!isNestedFreeze) {
509             auto freezeSourceParent = widget.resolver.closest(
510                 (Widget parent) => parent.view.freezeSources.front == parent
511             );
512             return freezeSourceParent is null;
513         } else {
514             return true;
515         }
516     }
517 
518     bool isWidgetFreezingSource(Widget widget) {
519         return !freezeSources.empty && freezeSources.front == widget;
520     }
521 
522     void showCursor() {
523         platformShowSystemCursor();
524     }
525 
526     void hideCursor() {
527         platformHideSystemCursor();
528     }
529 
530     void setMousePositon(in int x, in int y) {
531         platformSetMousePosition(window, x, y);
532     }
533 }