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