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