1 module rpui.widget;
2 
3 import std.container.array;
4 import std.math;
5 
6 import rpui.primitives;
7 import rpui.view;
8 import rpui.cursor;
9 import rpui.widgets_container;
10 import rpui.events;
11 import rpui.widget_events;
12 import rpui.math;
13 import rpui.focus_navigator;
14 import rpui.widget_locator;
15 import rpui.widget_resolver;
16 import rpui.render.renderer;
17 import rpui.render.components : State;
18 
19 import gapi.vec;
20 
21 /// Interface for scrollable widgets.
22 interface Scrollable {
23     void onMouseWheelHandle(in int dx, in int dy);
24 
25     void scrollToWidget(Widget widget);
26 }
27 
28 /// For scrollable widgets and if this widget allow to focus elements.
29 interface FocusScrollNavigation : Scrollable {
30     /**
31      * Scroll to widget if it out of visible region.
32      * Scroll on top border if widget above and bottom if below visible region.
33      */
34     void borderScrollToWidget(Widget widget);
35 }
36 
37 class Widget : EventsListenerEmpty {
38     /// Type of sizing for width and height.
39     enum SizeType {
40         value,  /// Using value from size.
41         wrapContent,  /// Automatically resize widget by content boundary.
42         matchParent  /// Using parent size.
43     }
44 
45     /**
46      * Field attribute need to tell RPDL which fields are fill
47      * when reading layout file.
48      */
49     struct field {
50         string name = "";  /// Override name of variable.
51     }
52 
53     @field bool isVisible = true;
54     @field bool isEnabled = true;
55     @field bool focusable = true;
56 
57     /// If true, then focus navigation by children will be limited inside this widget.
58     @field bool finalFocus = false;
59 
60     /// Specifies the type of cursor to be displayed when pointing on an element.
61     @field CursorIcon cursor = CursorIcon.normal;
62 
63     @field string name = "";
64 
65     /// Some help information about widget, need to display tooltip.
66     @field utf32string hint = "";
67 
68     /// How to place a widget horizontally.
69     @field Align locationAlign = Align.none;
70 
71     /// How to place a widget vertically.
72     @field VerticalAlign verticalLocationAlign = VerticalAlign.none;
73 
74     /**
75      * If set this option then widget will be pinned to one of the side
76      * declared in the `basic_types.RegionAlign`.
77      */
78     @field RegionAlign regionAlign = RegionAlign.none;
79 
80     /// Used to create space around elements, outside of any defined borders.
81     @field FrameRect margin = FrameRect(0, 0, 0, 0);
82 
83     /// Used to generate space around an element's content, inside of any defined borders.
84     @field FrameRect padding = FrameRect(0, 0, 0, 0);
85 
86     @field vec2 position = vec2(0, 0);
87     @field vec2 size = vec2(0, 0);
88 
89     @field SizeType widthType;  /// Determine how to set width for widget.
90     @field SizeType heightType;  /// Determine how to set height for widget.
91 
92     @field
93     @property float width() { return size.x; }
94     @property void width(in float val) { size.x = val; }
95 
96     @field
97     @property float height() { return size.y; }
98     @property void height(in float val) { size.y = val; }
99 
100     @field
101     @property float left() { return position.x; }
102     @property void left(in float val) { position.x = val; }
103 
104     @field
105     @property float top() { return position.y; }
106     @property void top(in float val) { position.y = val; }
107 
108     @property size_t id() { return p_id; }
109     private size_t p_id;
110 
111     /// Widget root rpdl node from where the data will be extracted.
112     const string style;
113 
114     @property Widget parent() { return p_parent; }
115     package Widget p_parent;
116 
117     // TODO(Andrey): package not working for the some reasons
118     public Widget owner;
119 
120     @property inout(bool) isFocused() inout { return p_isFocused; }
121     package bool p_isFocused;
122 
123     /// Next widget in `parent` children after this.
124     @property Widget nextWidget() { return p_nextWidget; }
125     package Widget p_nextWidget = null;
126 
127     /// Previous widget in `parent` children before this.
128     @property Widget prevWidget() { return p_prevWidget; }
129     package Widget p_prevWidget = null;
130 
131     /// Last widget in `parent` children.
132     @property Widget lastWidget() { return p_lastWidget; }
133     package Widget p_lastWidget = null;
134 
135     /// First widget in `parent` children.
136     @property Widget firstWidget() { return p_firstWidget; }
137     package Widget p_firstWidget = null;
138 
139     // TODO:
140     @property ref WidgetsContainer children() { return p_children; }
141     private WidgetsContainer p_children;
142 
143     @property uint depth() { return p_depth; }
144     uint p_depth = 0;
145 
146     @property WidgetResolver resolver() { return p_resolver; }
147     private WidgetResolver p_resolver;
148 
149     @property FocusNavigator focusNavigator() { return p_focusNavigator; }
150     private FocusNavigator p_focusNavigator;
151 
152     @property WidgetEventsObserver events() { return p_events; }
153     private WidgetEventsObserver p_events;
154 
155     /// Additional rules appart from `isVisible` to set widget visible or not.
156     Array!(bool delegate()) visibleRules;
157 
158     /// Additional rules appart from `enabled` to set widget enabled or not.
159     Array!(bool delegate()) enableRules;
160 
161     /**
162      * Which part of widget need to render, e.g. if it is a button
163      * then `PartDraws.left` tell that only left side and center will be
164      * rendered, this need for grouping rendering of widgets.
165      *
166      * As example consider this layout of grouping: $(I [button1|button2|button3|button4])
167      *
168      * for $(I button1) `PartDraws` will be $(B left), for $(I button2) and $(I button3) $(B center)
169      * and for $(I button4) it will be $(B right).
170      */
171     package enum PartDraws {
172         all,  /// Draw all parts - left, center and right.
173         left,
174         center,
175         right
176     }
177 
178     package PartDraws partDraws = PartDraws.all;
179 
180 package:
181     public @property View view() { return view_; }
182     View view_;
183 
184     bool skipFocus = false;  /// Don't focus this element.
185     bool drawChildren = true;
186     FrameRect extraInnerOffset = FrameRect(0, 0, 0, 0);  /// Extra inner offset besides padding.
187     FrameRect extraOuterOffset = FrameRect(0, 0, 0, 0);  /// Extra outer offset besides margin.
188     bool overlay;
189     vec2 overSize;
190     Rect overlayRect = emptyRect;
191     bool focusOnMousUp = false;
192 
193     bool isEnter;  /// True if pointed on widget.
194     bool overrideIsEnter;  /// Override isEnter state i.e. ignore isEnter value and use overrided value.
195     bool isClick;
196     bool isMouseDown = false;
197 
198     WidgetLocator locator;
199     Renderer renderer;
200 
201     /**
202      * When in rect of element but if another element over this
203      * isOver will still be true.
204      */
205     bool isOver;
206 
207     public @property inout(vec2) absolutePosition() inout { return absolutePosition_; }
208     vec2 absolutePosition_ = vec2(0, 0);
209 
210     /// Size of boundary over childern clamped to size of widget as minimum boundary size.
211     vec2 innerBoundarySizeClamped = vec2(0, 0);
212 
213     vec2 innerBoundarySize = vec2(0, 0);  /// Size of boundary over childern.
214     vec2 contentOffset = vec2(0, 0);  /// Children offset relative their absolute positions.
215     vec2 outerBoundarySize = vec2(0, 0); /// Full region size including inner offsets.
216 
217     Widget associatedWidget = null;
218 
219     /**
220      * Returns string of state declared in theme.
221      */
222     @property inout(State) state() inout {
223         if (isClick) {
224             return State.click;
225         } else if (isEnter || overrideIsEnter) {
226             return State.enter;
227         } else {
228             return State.leave;
229         }
230     }
231 
232     /// Inner size considering the extra innter offsets and paddings.
233     @property vec2 innerSize() {
234         return size - innerOffsetSize;
235     }
236 
237     /// Total inner offset size (width and height) considering the extra inner offsets and paddings.
238     @property vec2 innerOffsetSize() {
239         return vec2(
240             padding.left + padding.right + extraInnerOffset.left + extraInnerOffset.right,
241             padding.top + padding.bottom + extraInnerOffset.top + extraInnerOffset.bottom
242         );
243     }
244 
245     /// Inner padding plus and extra inner offsets.
246     @property FrameRect innerOffset() {
247         return FrameRect(
248             padding.left + extraInnerOffset.left,
249             padding.top + extraInnerOffset.top,
250             padding.right + extraInnerOffset.right,
251             padding.bottom + extraInnerOffset.bottom,
252         );
253     }
254 
255     /// Total size of extra inner offset (width and height).
256     @property vec2 extraInnerOffsetSize() {
257         return vec2(
258             extraInnerOffset.left + extraInnerOffset.right,
259             extraInnerOffset.top + extraInnerOffset.bottom
260         );
261     }
262 
263     @property vec2 extraInnerOffsetStart() {
264         return vec2(extraInnerOffset.left, extraInnerOffset.top);
265     }
266 
267     @property vec2 extraInnerOffsetEnd() {
268         return vec2(extraInnerOffset.right, extraInnerOffset.bottom);
269     }
270 
271     @property vec2 innerOffsetStart() {
272         return vec2(innerOffset.left, innerOffset.top);
273     }
274 
275     @property vec2 innerOffsetEnd() {
276         return vec2(innerOffset.right, innerOffset.bottom);
277     }
278 
279     /// Outer size considering the extra outer offsets and margins.
280     @property vec2 outerSize() {
281         return size + outerOffsetSize;
282     }
283 
284     /// Total outer offset size (width and height) considering the extra outer offsets and margins.
285     @property vec2 outerOffsetSize() {
286         return vec2(
287             margin.left + margin.right + extraOuterOffset.left + extraOuterOffset.right,
288             margin.top + margin.bottom + extraOuterOffset.top + extraOuterOffset.bottom
289         );
290     }
291 
292     /// Total outer offset - margins plus extra outer offsets.
293     @property FrameRect outerOffset() {
294         return FrameRect(
295             margin.left + extraOuterOffset.left,
296             margin.top + extraOuterOffset.top,
297             margin.right + extraOuterOffset.right,
298             margin.bottom + extraOuterOffset.bottom,
299         );
300     }
301 
302     @property vec2 outerOffsetStart() {
303         return vec2(outerOffset.left, outerOffset.top);
304     }
305 
306     @property vec2 outerOffsetEnd() {
307         return vec2(outerOffset.right, outerOffset.bottom);
308     }
309 
310 public:
311     /// Default constructor with default `style`.
312     this() {
313         this.style = "";
314         createComponents();
315     }
316 
317     /// Construct with custom `style`.
318     this(in string style) {
319         this.style = style;
320         createComponents();
321     }
322 
323     package this(View view) {
324         this.style = "";
325         this.view_ = view;
326         createComponents();
327     }
328 
329     private void createComponents() {
330         this.locator = new WidgetLocator(this);
331         this.p_focusNavigator = new FocusNavigator(this);
332         this.p_children = new WidgetsContainer(this);
333         this.p_resolver = new WidgetResolver(this);
334         this.p_events = new WidgetEventsObserver();
335         this.renderer = new DummyRenderer();
336 
337         this.p_events.subscribe!BlurEvent(&onBlur);
338         this.p_events.subscribe!FocusEvent(&onFocus);
339     }
340 
341     void onProgress(in ProgressEvent event) {
342         checkRules();
343         updateBoundary();
344         renderer.onProgress(event);
345     }
346 
347     /// Update widget inner bounary and clamped boundary.
348     protected void updateBoundary() {
349         if (!drawChildren)
350             return;
351 
352         innerBoundarySize = innerOffsetSize;
353 
354         foreach (Widget widget; children) {
355             if (!widget.isVisible)
356                 continue;
357 
358             auto widgetFringePosition = vec2(
359                 widget.position.x + widget.outerSize.x + innerOffset.left,
360                 widget.position.y + widget.outerSize.y + innerOffset.top
361             );
362 
363             if (widget.locationAlign != Align.none) {
364                 widgetFringePosition.x = 0;
365             }
366 
367             if (widget.verticalLocationAlign != VerticalAlign.none) {
368                 widgetFringePosition.y = 0;
369             }
370 
371             if (widget.regionAlign != RegionAlign.right &&
372                 widget.regionAlign != RegionAlign.top &&
373                 widget.regionAlign != RegionAlign.bottom)
374             {
375                 innerBoundarySize.x = fmax(innerBoundarySize.x, widgetFringePosition.x);
376             }
377 
378             if (widget.regionAlign != RegionAlign.bottom &&
379                 widget.regionAlign != RegionAlign.right &&
380                 widget.regionAlign != RegionAlign.left)
381             {
382                 innerBoundarySize.y = fmax(innerBoundarySize.y, widgetFringePosition.y);
383             }
384         }
385 
386         innerBoundarySize += innerOffsetEnd;
387 
388         innerBoundarySizeClamped.x = fmax(innerBoundarySize.x, innerSize.x);
389         innerBoundarySizeClamped.y = fmax(innerBoundarySize.y, innerSize.y);
390     }
391 
392     void checkRules() {
393         if (!visibleRules.empty) {
394             isVisible = true;
395 
396             foreach (bool delegate() rule; visibleRules) {
397                 isVisible = isVisible && rule();
398             }
399         }
400 
401         if (!enableRules.empty) {
402             isEnabled = true;
403 
404             foreach (bool delegate() rule; enableRules) {
405                 isEnabled = isEnabled && rule();
406             }
407         }
408     }
409 
410     protected void reset() {
411         if (associatedWidget !is null)
412             associatedWidget.reset();
413     }
414 
415     final Widget getNonDecoratorParent() {
416         Widget currentParent = parent;
417         bool isDecorator = parent.associatedWidget !is null;
418 
419         while (isDecorator) {
420             currentParent = currentParent.parent;
421             isDecorator = currentParent.associatedWidget !is null;
422         }
423 
424         return currentParent;
425     }
426 
427     void resetChildren() {
428         foreach (Widget widget; children) {
429             widget.reset();
430         }
431     }
432 
433     package final void collectOnProgressQueries() {
434         view.onProgressQueries.insert(this);
435 
436         if (!drawChildren)
437             return;
438 
439         foreach (Widget widget; children) {
440             if (!widget.isVisible && !widget.processPorgress())
441                 continue;
442 
443             view.onProgressQueries.insert(widget);
444             widget.collectOnProgressQueries();
445         }
446     }
447 
448     package bool processPorgress() {
449         return !visibleRules.empty || !enableRules.empty;
450     }
451 
452     /// Render widget in camera view.
453     void onRender() {
454         renderer.onRender();
455 
456         if (drawChildren) {
457             renderChildren();
458         }
459     }
460 
461     void renderChildren() {
462         if (!drawChildren)
463             return;
464 
465         foreach (Widget child; children) {
466             if (!child.isVisible)
467                 continue;
468 
469             child.onRender();
470         }
471     }
472 
473     /// Make focus for widget, and clear focus from focused widget.
474     void focus() {
475         events.notify(FocusEvent());
476 
477         if (view.focusedWidget != this && view.focusedWidget !is null)
478             view.focusedWidget.blur();
479 
480         view.focusedWidget = this;
481         p_isFocused = true;
482 
483         if (!this.skipFocus)
484             focusNavigator.borderScrollToWidget();
485     }
486 
487     /// Clear focus from widget
488     void blur() {
489         isClick = false;
490         p_isFocused = false;
491         view.unfocusedWidgets.insert(this);
492     }
493 
494     void onCreate() {
495         renderer.onCreate(this, style);
496     }
497 
498     void onPostCreate() {
499         foreach (Widget widget; children) {
500             widget.onPostCreate();
501         }
502     }
503 
504     override void onMouseMove(in MouseMoveEvent event) {
505         isClick = isEnter && isMouseDown;
506     }
507 
508     override void onMouseUp(in MouseUpEvent event) {
509         if ((isFocused && isEnter) || (!focusable && isEnter))
510             events.notify(ClickEvent());
511     }
512 
513     void onFocus(in FocusEvent event) {}
514 
515     void onBlur(in BlurEvent event) {}
516 
517     /// Override this method if need change behaviour when system cursor have to be changed.
518     void onCursor() {
519     }
520 
521     void onResize() {
522     }
523 
524     void onClickActionInvoked() {
525     }
526 
527     /// Determine if `point` is inside widget area.
528     final bool pointIsEnter(in vec2i point) {
529         const Rect rect = Rect(absolutePosition.x, absolutePosition.y, size.x, size.y);
530         return pointInRect(point, rect);
531     }
532 
533     /// This method invokes when widget size is updated.
534     void updateSize() {
535         if (widthType == SizeType.matchParent) {
536             locationAlign = Align.none;
537             size.x = parent.innerSize.x - outerOffsetSize.x;
538             position.x = 0;
539         }
540 
541         if (heightType == SizeType.matchParent) {
542             verticalLocationAlign = VerticalAlign.none;
543             size.y = parent.innerSize.y - outerOffsetSize.y;
544             position.y = 0;
545         }
546     }
547 
548     /// Recalculate size and position of widget and children widgets.
549     void updateAll() {
550         locator.updateLocationAlign();
551         locator.updateVerticalLocationAlign();
552         locator.updateRegionAlign();
553         locator.updateAbsolutePosition();
554         updateBoundary();
555         updateSize();
556         renderer.onProgress(ProgressEvent(0));
557 
558         foreach (Widget widget; children) {
559             if (widget.isVisible)
560                 widget.updateAll();
561         }
562     }
563 
564     void freezeUI(bool isNestedFreeze = true) {
565         view.freezeUI(this, isNestedFreeze);
566     }
567 
568     void unfreezeUI() {
569         view.unfreezeUI(this);
570     }
571 
572     bool isFrozen() {
573         return view.isWidgetFrozen(this);
574     }
575 
576     bool isFreezingSource() {
577         return view.isWidgetFreezingSource(this);
578     }
579 }