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 }