1 /** 2 Gui Manager 3 4 Copyright: (c) Enalye 2019 5 License: Zlib 6 Authors: Enalye 7 */ 8 9 module atelier.ui.gui_manager; 10 11 import std.conv : to; 12 import atelier.core, atelier.common, atelier.render; 13 import atelier.ui.gui_element, atelier.ui.gui_overlay, atelier.ui.gui_modal; 14 15 private { 16 bool _isGuiElementDebug = false; 17 GuiElement[] _rootElements; 18 float _deltaTime; 19 } 20 21 //-- Public --- 22 23 /// Add a gui as a top gui (not a child of anything). 24 void prependRoot(GuiElement gui) { 25 _rootElements = gui ~ _rootElements; 26 } 27 28 /// Add a gui as a top gui (not a child of anything). 29 void appendRoot(GuiElement gui) { 30 _rootElements ~= gui; 31 } 32 33 /// Remove all the top gui (that aren't a child of anything). 34 void removeRoots() { 35 //_isChildGrabbed = false; 36 _rootElements.length = 0uL; 37 } 38 39 /// Set those gui as the top guis (replacing the previous ones). 40 void setRoots(GuiElement[] widgets) { 41 _rootElements = widgets; 42 } 43 44 /// Get all the root gui. 45 GuiElement[] getRoots() { 46 return _rootElements; 47 } 48 49 /// Show every the hitbox of every gui element. 50 void setDebugGui(bool isDebug) { 51 _isGuiElementDebug = isDebug; 52 } 53 54 /// Remove the specified gui from roots. 55 void removeRoot(GuiElement gui) { 56 foreach (size_t i, GuiElement child; _rootElements) { 57 if (child is gui) { 58 removeRoot(i); 59 return; 60 } 61 } 62 } 63 64 /// Remove the gui at the specified index from roots. 65 void removeRoot(size_t index) { 66 if (!_rootElements.length) 67 return; 68 if (index + 1u == _rootElements.length) 69 _rootElements.length--; 70 else if (index == 0u) 71 _rootElements = _rootElements[1 .. $]; 72 else 73 _rootElements = _rootElements[0 .. index] ~ _rootElements[index + 1 .. $]; 74 } 75 76 //-- Internal --- 77 78 /// Update all the guis from the root. 79 package(atelier) void updateRoots(float deltaTime) { 80 _deltaTime = deltaTime; 81 size_t index = 0; 82 while (index < _rootElements.length) { 83 if (_rootElements[index]._isRegistered) { 84 updateRoots(_rootElements[index], null); 85 index++; 86 } 87 else { 88 removeRoot(index); 89 } 90 } 91 } 92 93 /// Draw all the guis from the root. 94 package(atelier) void drawRoots() { 95 foreach_reverse (GuiElement widget; _rootElements) { 96 drawRoots(widget); 97 } 98 } 99 100 private { 101 bool _hasClicked, _wasHoveredGuiElementAlreadyHovered; 102 GuiElement _clickedGuiElement; 103 GuiElement _focusedGuiElement; 104 GuiElement _hoveredGuiElement; 105 GuiElement _grabbedGuiElement, _tempGrabbedGuiElement; 106 Canvas _canvas; 107 Vec2f _clickedGuiElementEventPosition = Vec2f.zero; 108 Vec2f _hoveredGuiElementEventPosition = Vec2f.zero; 109 Vec2f _grabbedGuiElementEventPosition = Vec2f.zero; 110 GuiElement[] _hookedGuis; 111 } 112 113 /// Dispatch global events on the guis from the root. \ 114 /// Called by the main event loop. 115 package(atelier) void handleGuiElementEvent(Event event) { 116 if (isOverlay()) { 117 processOverlayEvent(event); 118 } 119 120 _hasClicked = false; 121 switch (event.type) with (Event.Type) { 122 case mouseDown: 123 _tempGrabbedGuiElement = null; 124 dispatchMouseDownEvent(null, event.mouse.position); 125 126 if (_tempGrabbedGuiElement) { 127 _grabbedGuiElement = _tempGrabbedGuiElement; 128 } 129 130 if (_hasClicked && _clickedGuiElement !is null) { 131 _clickedGuiElement.isClicked = true; 132 Event guiEvent = Event.Type.mouseDown; 133 guiEvent.mouse.position = _clickedGuiElementEventPosition; 134 _clickedGuiElement.onEvent(guiEvent); 135 } 136 break; 137 case mouseUp: 138 _grabbedGuiElement = null; 139 dispatchMouseUpEvent(null, event.mouse.position); 140 break; 141 case mouseUpdate: 142 _hookedGuis.length = 0; 143 dispatchMouseUpdateEvent(null, event.mouse.position); 144 145 if (_hasClicked && _hoveredGuiElement !is null) { 146 _hoveredGuiElement.isHovered = true; 147 148 if (!_wasHoveredGuiElementAlreadyHovered) 149 _hoveredGuiElement.onHover(); 150 151 //Compatibility 152 Event guiEvent = Event.Type.mouseUpdate; 153 guiEvent.mouse.position = _hoveredGuiElementEventPosition; 154 _hoveredGuiElement.onEvent(guiEvent); 155 } 156 break; 157 case mouseWheel: 158 dispatchMouseWheelEvent(event.scroll.delta); 159 break; 160 case quit: 161 dispatchQuitEvent(null); 162 if (isModal()) { 163 stopAllModals(); 164 dispatchQuitEvent(null); 165 } 166 break; 167 default: 168 dispatchGenericEvents(null, event); 169 break; 170 } 171 } 172 173 /// Update all children of a gui. \ 174 /// Called by the application itself. 175 package(atelier) void updateRoots(GuiElement gui, GuiElement parent) { 176 Vec2f coords = Vec2f.zero; 177 178 //Calculate transitions 179 if (gui._timer.isRunning) { 180 gui._timer.update(_deltaTime); 181 const float t = gui._targetState.easing(gui._timer.value01); 182 gui._currentState.offset = lerp(gui._initState.offset, gui._targetState.offset, t); 183 184 gui._currentState.scale = lerp(gui._initState.scale, gui._targetState.scale, t); 185 186 gui._currentState.color = lerp(gui._initState.color, gui._targetState.color, t); 187 188 gui._currentState.alpha = lerp(gui._initState.alpha, gui._targetState.alpha, t); 189 190 gui._currentState.angle = lerp(gui._initState.angle, gui._targetState.angle, t); 191 gui.onColor(); 192 if (!gui._timer.isRunning) { 193 if (gui._targetState.callback.length) 194 gui.onCallback(gui._targetState.callback); 195 } 196 } 197 198 //Calculate gui location 199 const Vec2f offset = gui._position + ( 200 gui._size * gui._currentState.scale / 2f) + gui._currentState.offset; 201 if (parent !is null) { 202 if (parent.hasCanvas && parent.canvas !is null) { 203 if (gui._alignX == GuiAlignX.left) 204 coords.x = offset.x; 205 else if (gui._alignX == GuiAlignX.right) 206 coords.x = (parent._size.x * parent._currentState.scale.x) - offset.x; 207 else 208 coords.x = (parent._size.x * parent._currentState.scale.x) / 2f 209 + gui._currentState.offset.x + gui.position.x; 210 211 if (gui._alignY == GuiAlignY.top) 212 coords.y = offset.y; 213 else if (gui._alignY == GuiAlignY.bottom) 214 coords.y = (parent._size.y * parent._currentState.scale.y) - offset.y; 215 else 216 coords.y = (parent._size.y * parent._currentState.scale.y) / 2f 217 + gui._currentState.offset.y + gui.position.y; 218 } 219 else { 220 if (gui._alignX == GuiAlignX.left) 221 coords.x = parent.origin.x + offset.x; 222 else if (gui._alignX == GuiAlignX.right) 223 coords.x = parent.origin.x + ( 224 parent._size.x * parent._currentState.scale.x) - offset.x; 225 else 226 coords.x = parent.center.x + gui._currentState.offset.x + gui.position.x; 227 228 if (gui._alignY == GuiAlignY.top) 229 coords.y = parent.origin.y + offset.y; 230 else if (gui._alignY == GuiAlignY.bottom) 231 coords.y = parent.origin.y + ( 232 parent._size.y * parent._currentState.scale.y) - offset.y; 233 else 234 coords.y = parent.center.y + gui._currentState.offset.y + gui.position.y; 235 } 236 } 237 else { 238 if (gui._alignX == GuiAlignX.left) 239 coords.x = offset.x; 240 else if (gui._alignX == GuiAlignX.right) 241 coords.x = getWindowWidth() - offset.x; 242 else 243 coords.x = getWindowCenter().x + gui._currentState.offset.x + gui.position.x; 244 245 if (gui._alignY == GuiAlignY.top) 246 coords.y = offset.y; 247 else if (gui._alignY == GuiAlignY.bottom) 248 coords.y = getWindowHeight() - offset.y; 249 else 250 coords.y = getWindowCenter().y + gui._currentState.offset.y + gui.position.y; 251 } 252 gui.setScreenCoords(coords); 253 gui.update(_deltaTime); 254 255 size_t childIndex = 0; 256 while (childIndex < gui.nodes.length) { 257 if (gui.nodes[childIndex]._isRegistered) { 258 updateRoots(gui.nodes[childIndex], gui); 259 childIndex++; 260 } 261 else { 262 gui.removeChild(childIndex); 263 } 264 } 265 } 266 267 /// Renders a gui and all its children. 268 void drawRoots(GuiElement gui) { 269 if (gui.hasCanvas && gui.canvas !is null) { 270 auto canvas = gui.canvas; 271 canvas.color(gui._currentState.color); 272 canvas.alpha(gui._currentState.alpha); 273 pushCanvas(canvas, true); 274 gui.draw(); 275 foreach (GuiElement child; gui.nodes) { 276 drawRoots(child); 277 } 278 popCanvas(); 279 canvas.draw(transformRenderSpace(gui._screenCoords), 280 transformScale() * cast(Vec2f) canvas.renderSize(), Vec4i(0, 0, 281 canvas.width, canvas.height), gui._currentState.angle, Flip.none, Vec2f.half); 282 const auto origin = gui._origin; 283 const auto center = gui._center; 284 gui._origin = gui._screenCoords - (gui._size * gui._currentState.scale) / 2f; 285 gui._center = gui._screenCoords; 286 gui.drawOverlay(); 287 gui._origin = origin; 288 gui._center = center; 289 if (gui.isHovered && gui.hint !is null) 290 openHintWindow(gui.hint); 291 } 292 else { 293 gui.draw(); 294 foreach (GuiElement child; gui.nodes) { 295 drawRoots(child); 296 } 297 gui.drawOverlay(); 298 if (gui.isHovered && gui.hint !is null) 299 openHintWindow(gui.hint); 300 } 301 if (_isGuiElementDebug) { 302 drawRect(gui.center - (gui._size * gui._currentState.scale) / 2f, 303 gui._size * gui._currentState.scale, gui.isHovered ? Color.red 304 : (gui.nodes.length ? Color.blue : Color.green)); 305 } 306 } 307 308 /// Process a mouse down event down the tree. 309 private void dispatchMouseDownEvent(GuiElement gui, Vec2f cursorPosition) { 310 auto children = (gui is null) ? _rootElements : gui.nodes; 311 bool hasCanvas; 312 313 if (gui !is null) { 314 if (gui.isInteractable && gui.isInside(cursorPosition)) { 315 _clickedGuiElement = gui; 316 _tempGrabbedGuiElement = null; 317 318 if (gui.hasCanvas && gui.canvas !is null) { 319 hasCanvas = true; 320 pushCanvas(gui.canvas, false); 321 cursorPosition = transformCanvasSpace(cursorPosition, gui._screenCoords); 322 } 323 324 _clickedGuiElementEventPosition = cursorPosition; 325 _hasClicked = true; 326 327 if (gui._hasEventHook) { 328 Event guiEvent = Event.Type.mouseDown; 329 guiEvent.mouse.position = cursorPosition; 330 gui.onEvent(guiEvent); 331 } 332 333 if (gui._isMovable && !_grabbedGuiElement) { 334 _tempGrabbedGuiElement = gui; 335 _grabbedGuiElementEventPosition = _clickedGuiElementEventPosition; 336 } 337 } 338 else 339 return; 340 } 341 342 foreach (child; children) 343 dispatchMouseDownEvent(child, cursorPosition); 344 345 if (hasCanvas) 346 popCanvas(); 347 } 348 349 /// Process a mouse up event down the tree. 350 private void dispatchMouseUpEvent(GuiElement gui, Vec2f cursorPosition) { 351 auto children = (gui is null) ? _rootElements : gui.nodes; 352 bool hasCanvas; 353 354 if (gui !is null) { 355 if (gui.isInteractable && gui.isInside(cursorPosition)) { 356 if (gui.hasCanvas && gui.canvas !is null) { 357 hasCanvas = true; 358 pushCanvas(gui.canvas, false); 359 cursorPosition = transformCanvasSpace(cursorPosition, gui._screenCoords); 360 } 361 362 if (gui._hasEventHook) { 363 Event guiEvent = Event.Type.mouseUp; 364 guiEvent.mouse.position = cursorPosition; 365 gui.onEvent(guiEvent); 366 } 367 } 368 else 369 return; 370 } 371 372 foreach (child; children) 373 dispatchMouseUpEvent(child, cursorPosition); 374 375 if (hasCanvas) 376 popCanvas(); 377 378 if (gui !is null && _clickedGuiElement == gui) { 379 //The previous widget is now unfocused. 380 if (_focusedGuiElement !is null) { 381 _focusedGuiElement.hasFocus = false; 382 } 383 384 //The widget is now focused and receive the onSubmit event. 385 _focusedGuiElement = _clickedGuiElement; 386 _hasClicked = true; 387 gui.hasFocus = true; 388 gui.onSubmit(); 389 390 //Compatibility 391 Event event = Event.Type.mouseUp; 392 event.mouse.position = cursorPosition; 393 gui.onEvent(event); 394 } 395 if (_clickedGuiElement !is null) 396 _clickedGuiElement.isClicked = false; 397 } 398 399 package void setFocusedElement(GuiElement gui) { 400 if (_focusedGuiElement == gui) 401 return; 402 //The previous widget is now unfocused. 403 if (_focusedGuiElement !is null) { 404 _focusedGuiElement.hasFocus = false; 405 } 406 _focusedGuiElement = gui; 407 } 408 409 /// Process a mouse update event down the tree. 410 private void dispatchMouseUpdateEvent(GuiElement gui, Vec2f cursorPosition) { 411 auto children = (gui is null) ? _rootElements : gui.nodes; 412 bool hasCanvas, wasHovered; 413 414 if (gui !is null) { 415 wasHovered = gui.isHovered; 416 417 if (gui.isInteractable && gui == _grabbedGuiElement) { 418 if (!gui._isMovable) { 419 _grabbedGuiElement = null; 420 } 421 else { 422 if (gui.hasCanvas && gui.canvas !is null) { 423 pushCanvas(gui.canvas, false); 424 cursorPosition = transformCanvasSpace(cursorPosition, gui._screenCoords); 425 } 426 Vec2f deltaPosition = (cursorPosition - _grabbedGuiElementEventPosition); 427 if (gui._alignX == GuiAlignX.right) 428 deltaPosition.x = -deltaPosition.x; 429 if (gui._alignY == GuiAlignY.bottom) 430 deltaPosition.y = -deltaPosition.y; 431 gui._position += deltaPosition; 432 if (gui.hasCanvas && gui.canvas !is null) 433 popCanvas(); 434 else 435 _grabbedGuiElementEventPosition = cursorPosition; 436 } 437 } 438 439 if (gui.isInteractable && gui.isInside(cursorPosition)) { 440 if (gui.hasCanvas && gui.canvas !is null) { 441 hasCanvas = true; 442 pushCanvas(gui.canvas, false); 443 cursorPosition = transformCanvasSpace(cursorPosition, gui._screenCoords); 444 } 445 446 //Register gui 447 _wasHoveredGuiElementAlreadyHovered = wasHovered; 448 _hoveredGuiElement = gui; 449 _hoveredGuiElementEventPosition = cursorPosition; 450 _hasClicked = true; 451 452 if (gui._hasEventHook) { 453 Event guiEvent = Event.Type.mouseUpdate; 454 guiEvent.mouse.position = cursorPosition; 455 gui.onEvent(guiEvent); 456 _hookedGuis ~= gui; 457 } 458 } 459 else { 460 void unHoverRoots(GuiElement gui) { 461 gui.isHovered = false; 462 foreach (child; gui.nodes) 463 unHoverRoots(child); 464 } 465 466 unHoverRoots(gui); 467 return; 468 } 469 } 470 471 foreach (child; children) 472 dispatchMouseUpdateEvent(child, cursorPosition); 473 474 if (hasCanvas) 475 popCanvas(); 476 } 477 478 /// Process a mouse wheel event down the tree. 479 private void dispatchMouseWheelEvent(Vec2f scroll) { 480 Event scrollEvent = Event.Type.mouseWheel; 481 scrollEvent.scroll.delta = scroll; 482 483 foreach (gui; _hookedGuis) { 484 gui.onEvent(scrollEvent); 485 } 486 487 if (_clickedGuiElement !is null) { 488 if (_clickedGuiElement.isClicked) { 489 _clickedGuiElement.onEvent(scrollEvent); 490 return; 491 } 492 } 493 if (_hoveredGuiElement !is null) { 494 _hoveredGuiElement.onEvent(scrollEvent); 495 return; 496 } 497 } 498 499 /// Notify every gui in the tree that we are leaving. 500 private void dispatchQuitEvent(GuiElement gui) { 501 if (gui !is null) { 502 foreach (GuiElement child; gui.nodes) 503 dispatchQuitEvent(child); 504 gui.onQuit(); 505 } 506 else { 507 foreach (GuiElement widget; _rootElements) 508 dispatchQuitEvent(widget); 509 } 510 } 511 512 /// Every other event that doesn't have a specific behavior like mouse events. 513 private void dispatchGenericEvents(GuiElement gui, Event event) { 514 if (gui !is null) { 515 gui.onEvent(event); 516 foreach (GuiElement child; gui.nodes) { 517 dispatchGenericEvents(child, event); 518 } 519 } 520 else { 521 foreach (GuiElement widget; _rootElements) { 522 dispatchGenericEvents(widget, event); 523 } 524 } 525 } 526 /* 527 private void handleGuiElementEvents(GuiElement gui) { 528 switch (event.type) with(Event.Type) { 529 case MouseDown: 530 bool hasClickedGuiElement; 531 foreach(uint id, GuiElement widget; _children) { 532 widget.hasFocus = false; 533 if(!widget.isInteractable) 534 continue; 535 536 if(!hasClickedGuiElement && widget.isInside(_isFrame ? transformCanvasSpace(event.mouse.position, _position) : event.mouse.position)) { 537 widget.hasFocus = true; 538 widget.isSelected = true; 539 widget.isHovered = true; 540 _isChildGrabbed = true; 541 _idChildGrabbed = id; 542 543 if(_isFrame) 544 event.mouse.position = transformCanvasSpace(event.mouse.position, _position); 545 widget.onEvent(event); 546 hasClickedGuiElement = true; 547 } 548 } 549 550 if(!_isChildGrabbed && _isMovable) { 551 _isGrabbed = true; 552 _lastMousePos = event.mouse.position; 553 } 554 break; 555 case MouseUp: 556 if(_isChildGrabbed) { 557 _isChildGrabbed = false; 558 _children[_idChildGrabbed].isSelected = false; 559 560 if(_isFrame) 561 event.mouse.position = transformCanvasSpace(event.mouse.position, _position); 562 _children[_idChildGrabbed].onEvent(event); 563 } 564 else { 565 _isGrabbed = false; 566 } 567 break; 568 case MouseUpdate: 569 _isIterating = false; //Use mouse control 570 Vec2f mousePosition = event.mouse.position; 571 if(_isFrame) 572 event.mouse.position = transformCanvasSpace(event.mouse.position, _position); 573 574 _isChildHovered = false; 575 foreach(uint id, GuiElement widget; _children) { 576 if(isHovered) { 577 widget.isHovered = widget.isInside(event.mouse.position); 578 if(widget.isHovered && widget.isInteractable) { 579 _isChildHovered = true; 580 widget.onEvent(event); 581 } 582 } 583 else 584 widget.isHovered = false; 585 } 586 587 if(_isChildGrabbed && !_children[_idChildGrabbed].isHovered) 588 _children[_idChildGrabbed].onEvent(event); 589 else if(_isGrabbed && _isMovable) { 590 Vec2f deltaPosition = (mousePosition - _lastMousePos); 591 if(!_isFrame) { 592 //Clamp the window in the screen 593 if(isModal()) { 594 Vec2f halfSize = _size / 2f; 595 Vec2f clampedPosition = _position.clamp(halfSize, screenSize - halfSize); 596 deltaPosition += (clampedPosition - _position); 597 } 598 _position += deltaPosition; 599 600 foreach(widget; _children) 601 widget.position = widget.position + deltaPosition; 602 } 603 else 604 _position += deltaPosition; 605 _lastMousePos = mousePosition; 606 } 607 break; 608 case MouseWheel: 609 foreach(uint id, GuiElement widget; _children) { 610 if(widget.isHovered) 611 widget.onEvent(event); 612 } 613 614 if(_isChildGrabbed && !_children[_idChildGrabbed].isHovered) 615 _children[_idChildGrabbed].onEvent(event); 616 break; 617 default: 618 foreach(GuiElement widget; _children) 619 widget.onEvent(event); 620 break; 621 } 622 }*/