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 }