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 }