1 /** 2 * `UIElement` is the base class of all widgets. 3 * 4 * Copyright: Copyright Auburn Sounds 2015 and later. 5 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 * Authors: Guillaume Piolat 7 */ 8 module dplug.gui.element; 9 10 import core.stdc.stdio; 11 import core.stdc.string: strlen, strcmp; 12 13 import std.algorithm.comparison; 14 import std.math: round; 15 16 public import dplug.math.vector; 17 public import dplug.math.box; 18 19 public import dplug.graphics; 20 21 public import dplug.window.window; 22 23 public import dplug.core.sync; 24 public import dplug.core.vec; 25 public import dplug.core.nogc; 26 27 public import dplug.gui.boxlist; 28 public import dplug.gui.context; 29 30 /// Reasonable default value for the Depth channel. 31 enum ushort defaultDepth = 15000; 32 33 /// Reasonable default value for the Roughness channel. 34 enum ubyte defaultRoughness = 128; 35 36 /// Reasonable default value for the Specular channel ("everything is shiny"). 37 enum ubyte defaultSpecular = 128; 38 39 /// Reasonable dielectric default value for the Metalness channel. 40 enum ubyte defaultMetalnessDielectric = 25; // ~ 0.08 41 42 /// Reasonable metal default value for the Metalness channel. 43 enum ubyte defaultMetalnessMetal = 255; 44 45 /// Each UIElement class has flags which are used to lessen the number of empty virtual calls. 46 /// Such flags say which callbacks the `UIElement` need. 47 alias UIFlags = uint; 48 enum : UIFlags 49 { 50 /// This `UIElement` draws to the Raw layer and as such `onDrawRaw` should be called when dirtied. 51 /// When calling `setDirty(UILayer.guessFromFlags)`, the Raw layer alone will be invalidated. 52 flagRaw = 1, 53 54 /// This `UIElement` draws to the PBR layer and as such `onDrawPBR` should be called when dirtied. 55 /// Important: `setDirty(UILayer.guessFromFlags)` will lead to BOTH `onDrawPBR` and `onDrawRaw` 56 /// to be called sucessively. 57 flagPBR = 2, 58 59 /// This `UIElement` is animated and as such the `onAnimate` callback should be called regularly. 60 flagAnimated = 4, 61 62 /// Is not drawn in parallel with other widgets, when drawn to the Raw layer. 63 flagDrawAloneRaw = 8, 64 65 /// Is not drawn in parallel with other widgets, when drawn to the PBR layer. 66 flagDrawAlonePBR = 16, 67 } 68 69 /// Used by `setDirty` calls to figure which layer should be invalidated. 70 enum UILayer 71 { 72 /// Use the `UIElement` flags to figure which layers to invalidate. 73 /// This is what you want most of the time. 74 guessFromFlags, 75 76 /// Only the Raw layer is invalidated. 77 /// This is what you want if your `UIElement` draw to both Raw and PBR layer, but this 78 /// time you only want to udpate a fast Raw overlay (ie: any PBR widget that still need to be real-time) 79 rawOnly, 80 81 /// This is only useful for the very first setDirty call, to mark the whole UI dirty. 82 /// For internal Dplug usage. 83 allLayers 84 } 85 86 /// Result of `onMouseClick`, was the mouse click handled? 87 enum Click 88 { 89 handled, /// click handled, no drag. 90 startDrag, /// click handled AND it start new drag. (previously: true) 91 unhandled /// click not handeld, pass it around. (previously: false) 92 } 93 94 /// The maximum length for an UIElement ID. 95 enum maxUIElementIDLength = 63; 96 97 /// Returns: true if a valid UIlement unique identifier. 98 /// Chrome rules applies: can't be empty. 99 static bool isValidElementID(const(char)[] identifier) pure nothrow @nogc @safe 100 { 101 if (identifier.length == 0) return false; 102 if (identifier.length > maxUIElementIDLength) return false; 103 foreach (char ch; identifier) 104 { 105 if (ch == 0) // Note: Chrome does actually accept ID with spaces 106 return false; 107 } 108 return true; 109 } 110 111 // An UIElement has 8 void* user pointers (4 reserved for Dplug + 4 for vendors). 112 // The first two are used by dplug:wren-support. 113 // Official dplug:wren-support optional extension. 114 enum UIELEMENT_POINTERID_WREN_EXPORTED_CLASS = 0; /// The cached Wren class of this UIElement. 115 enum UIELEMENT_POINTERID_WREN_VM_GENERATION = 1; /// The Wren VM count, as it is restarted. Stored as void*, but is an uint. 116 117 /// Base class of the UI widget hierarchy. 118 /// 119 /// MAYDO: a bunch of stuff in that class is intended specifically for the root element, 120 /// there is probably a better design to find. 121 class UIElement 122 { 123 public: 124 nothrow: 125 @nogc: 126 127 this(UIContext context, uint flags) 128 { 129 _context = context; 130 _flags = flags; 131 _localRectsBuf = makeVec!box2i(); 132 _children = makeVec!UIElement(); 133 _zOrderedChildren = makeVec!UIElement(); 134 _idStorage[0] = '\0'; // defaults to no ID 135 136 // Initially set to an empty position 137 assert(_position.empty()); 138 } 139 140 ~this() 141 { 142 foreach(child; _children[]) 143 child.destroyFree(); 144 } 145 146 /// Set this element ID. 147 /// All UIElement can have a string as unique identifier, similar to HTML. 148 /// There is a maximum of 63 characters for this id though. 149 /// This ID is supposed to be unique. If it isn't, a search by ID will return `null`. 150 /// Chrome rules applies: can't contain any space characters. 151 final void setId(const(char)[] identifier) pure 152 { 153 if (!isValidElementID(identifier)) 154 { 155 // Note: assigning an invalid ID is a silent error. The UIElement ends up with no ID. 156 _idStorage[0] = '\0'; 157 return; 158 } 159 _idStorage[0..identifier.length] = identifier[0..$]; 160 _idStorage[identifier.length] = '\0'; 161 } 162 163 /// Get this element ID. 164 /// All UIElement can have a string as unique identifier, similar to HTML. 165 /// Returns the empty string "" if there is no ID. 166 /// Note: this return an interior slice, and could be invalidated if the ID is reassigned. 167 final const(char)[] getId() pure 168 { 169 if (_idStorage[0] == '\0') 170 return ""; 171 else 172 { 173 size_t len = strlen(_idStorage.ptr); 174 return _idStorage[0..len]; 175 } 176 } 177 178 /// Properties to access this element ID. 179 /// See_also: setId, getId. 180 final const(char)[] id() pure 181 { 182 return getId(); 183 } 184 ///ditto 185 final void id(const(char)[] identifier) 186 { 187 setId(identifier); 188 } 189 190 /// Returns: `true` if thie UIElement has an ID. 191 final bool hasId() pure 192 { 193 return _idStorage[0] != '\0'; 194 } 195 196 /// Search subtree for an UIElement with ID `id`. Undefined Behaviour if ID are not unique. 197 final UIElement getElementById(const(char)* id) 198 { 199 if (strcmp(id, _idStorage.ptr) == 0) 200 return this; 201 202 foreach(c; _children) 203 { 204 UIElement r = c.getElementById(id); 205 if (r) return r; 206 } 207 return null; 208 } 209 210 /// This method is called for each item in the drawlist that was visible and has a dirty Raw layer. 211 /// This is called after compositing, starting from the buffer output by the Compositor. 212 final void renderRaw(ImageRef!RGBA rawMap, in box2i[] areasToUpdate) 213 { 214 // We only consider the part of _position that is actually in the surface 215 box2i validPosition = _position.intersection(box2i(0, 0, rawMap.w, rawMap.h)); 216 217 // Note: _position can be outside the bounds of a window. 218 219 if (validPosition.empty()) 220 return; // nothing to draw here 221 222 _localRectsBuf.clearContents(); 223 { 224 foreach(rect; areasToUpdate) 225 { 226 box2i inter = rect.intersection(validPosition); 227 228 if (!inter.empty) // don't consider empty rectangles 229 { 230 // Express the dirty rect in local coordinates for simplicity 231 _localRectsBuf.pushBack( inter.translate(-validPosition.min) ); 232 } 233 } 234 } 235 236 if (_localRectsBuf.length == 0) 237 return; // nothing to draw here 238 239 // Crop the composited map to the valid part of _position 240 // Drawing outside of _position is disallowed by design. 241 ImageRef!RGBA rawMapCropped = rawMap.cropImageRef(validPosition); 242 assert(rawMapCropped.w != 0 && rawMapCropped.h != 0); // Should never be an empty area there 243 onDrawRaw(rawMapCropped, _localRectsBuf[]); 244 } 245 246 /// Returns: true if was drawn, ie. the buffers have changed. 247 /// This method is called for each item in the drawlist that was visible and has a dirty PBR layer. 248 final void renderPBR(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, in box2i[] areasToUpdate) 249 { 250 // we only consider the part of _position that is actually in the surface 251 box2i validPosition = _position.intersection(box2i(0, 0, diffuseMap.w, diffuseMap.h)); 252 253 // Note: _position can be outside the bounds of a window. 254 255 if (validPosition.empty()) 256 return; // nothing to draw here 257 258 _localRectsBuf.clearContents(); 259 { 260 foreach(rect; areasToUpdate) 261 { 262 box2i inter = rect.intersection(validPosition); 263 264 if (!inter.empty) // don't consider empty rectangles 265 { 266 // Express the dirty rect in local coordinates for simplicity 267 _localRectsBuf.pushBack( inter.translate(-validPosition.min) ); 268 } 269 } 270 } 271 272 if (_localRectsBuf.length == 0) 273 return; // nothing to draw here 274 275 // Crop the diffuse, material and depth to the valid part of _position 276 // Drawing outside of _position is disallowed by design. 277 ImageRef!RGBA diffuseMapCropped = diffuseMap.cropImageRef(validPosition); 278 ImageRef!L16 depthMapCropped = depthMap.cropImageRef(validPosition); 279 ImageRef!RGBA materialMapCropped = materialMap.cropImageRef(validPosition); 280 281 // Should never be an empty area there 282 assert(diffuseMapCropped.w != 0 && diffuseMapCropped.h != 0); 283 onDrawPBR(diffuseMapCropped, depthMapCropped, materialMapCropped, _localRectsBuf[]); 284 } 285 286 /// The goal of this method is to update positions of childrens. It is called whenever 287 /// _position changes. 288 /// 289 /// It is called after a widget position is changed. 290 /// Given information with `position` getter, the widget decides the position of its 291 /// children, by calling their `position` setter (which will call `reflow` itself). 292 /// 293 /// `reflow()` cannot be used to set the own position of a widget: it is always done 294 /// externally. You shouldn't call reflow yourself, instead use `position = x;`. 295 /// 296 /// Like in the DOM, children elements don't need to be inside position of their parent. 297 /// The _position field is indeed storing an absolute position. 298 /// 299 /// See_also: `position`, `_position`. 300 void reflow() 301 { 302 // default: do nothing 303 } 304 305 /// Returns: Position of the element, that will be used for rendering. 306 /// This getter is typically used in reflow() to adapt resource and children to the new position. 307 final box2i position() 308 { 309 return _position; 310 } 311 312 /// Changes the position of the element. 313 /// This calls `reflow` if that position has changed. 314 /// IMPORTANT: As of today you are not allowed to assign a position outside the extent of the window. 315 /// This is purely a Dplug limitation. 316 final void position(box2i p) 317 { 318 assert(p.isSorted()); 319 320 bool moved = (p != _position); 321 322 // Make dirty rect in former and new positions. 323 if (moved) 324 { 325 setDirtyWhole(); 326 _position = p; 327 setDirtyWhole(); 328 329 // New in Dplug v11: setting position now calls reflow() if position has changed. 330 reflow(); 331 332 // _position shouldn't be touched by `reflow` calls. 333 assert(p == _position); 334 } 335 } 336 337 /// Changes the position of the element. 338 /// This calls `reflow` if that position has changed. 339 /// Note: Widget coordinates are always integer coordinates. 340 /// The input rectangle is rounded to nearest integer. 341 final void position(box2f p) 342 { 343 int x1 = cast(int) round(p.min.x); 344 int y1 = cast(int) round(p.min.y); 345 int x2 = cast(int) round(p.max.x); 346 int y2 = cast(int) round(p.max.y); 347 box2i r = box2i(x1, y1, x2, y2); 348 position = r; 349 } 350 351 /// Returns: The nth child of this `UIElement`. 352 final UIElement child(int n) 353 { 354 return _children[n]; 355 } 356 357 /// Adds an `UIElement` 358 /// The addChild method is mandatory. 359 /// Such a child MUST be created through `dplug.core.nogc.mallocEmplace`. 360 /// Note: to display a newly added widget, use `position` setter. 361 final void addChild(UIElement element) 362 { 363 element._parent = this; 364 _children.pushBack(element); 365 366 // Recompute visibility of that element. 367 bool parentVisible = isVisible(); 368 element.recomputeVisibilityStatus(parentVisible); 369 } 370 371 /// Removes a child (but does not destroy it, you take back the ownership of it). 372 /// Useful for creating dynamic UI's. 373 /// MAYDO: there are restrictions for where this is allowed. Find them. 374 final void removeChild(UIElement element) 375 { 376 int index= _children.indexOf(element); 377 if(index >= 0) 378 { 379 // Dirty where the UIElement has been removed 380 element.setDirtyWhole(); 381 382 _children.removeAndReplaceByLastElement(index); 383 } 384 } 385 386 /// `onMouseClick` is called for every new click, whether or not you are in a 387 /// dragging operation. 388 /// This function is meant to be overriden. 389 /// 390 /// Returns: 391 /// If you return `Click.handled`, the click is considered processed 392 /// and will not pass to parent window. 393 /// 394 /// If you return `Click.startDrag`, the click is considered processed. 395 /// Any existing dragging is stopped with `onStopDrag`, and a new drag operation 396 /// is started. `onBeginDrag`/`onStopDrag` are also called. (was formerly: returning true) 397 /// 398 /// If you return `Click.unhandled`, the click is unprocessed. 399 /// If may be passed down to the underlying parent window. 400 /// (was formerly: returning false) 401 /// 402 /// Warning: For this reason, check your widgets with several mouse buttons pressed 403 /// at once. 404 Click onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate) 405 { 406 // By default, does not recognize that click. 407 return Click.unhandled; 408 } 409 410 /// Mouse wheel was turned. 411 /// This function is meant to be overriden. 412 /// It should return true if the wheel is handled. 413 bool onMouseWheel(int x, int y, int wheelDeltaX, int wheelDeltaY, MouseState mstate) 414 { 415 return false; 416 } 417 418 /// Called when mouse move over this Element. 419 /// This function is meant to be overriden. 420 /// 421 /// Note: If "legacyMouseDrag" version identifier is used, 422 /// this will be called even during a drag, beware. 423 void onMouseMove(int x, int y, int dx, int dy, MouseState mstate) 424 { 425 } 426 427 /// Called when clicked with left/middle/right button 428 /// This function is meant to be overriden. 429 /// Between `onBeginDrag` and `onStopDrag`, `isDragged` will return `true`. 430 /// 431 /// Note: When a widget is dragged, and "futureMouseDrag" version identifier is used, 432 /// then a dragged widget is always also isMouseOver. 433 /// 434 /// Example: you could call `beginParamEdit` from there or from `onMouseClick`. You probably 435 /// have more context in `onMouseClick`. 436 void onBeginDrag() 437 { 438 } 439 440 /// Called when mouse drag this Element. 441 /// This function is meant to be overriden. 442 /// 443 /// Example: if a mouse click started an edit f a plugin parameter with a drag, this will be a 444 /// preferred place to call `param.setFromGUI`. 445 void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate) 446 { 447 } 448 449 /// Called once a dragging operation is finished. 450 /// This function is meant to be overriden. 451 /// Between `onBeginDrag` and `onStopDrag`, `isDragged` will return `true`. 452 /// 453 /// Note: When a widget is dragged, and "futureMouseDrag" version identifier is used, 454 /// then a dragged widget is always also isMouseOver. 455 /// 456 /// Example: if a mouse click started a modification of a plugin parameter, this will be a 457 /// preferred place to call `param.endParamEdit`. 458 void onStopDrag() 459 { 460 } 461 462 /// Called when mouse enter this Element. 463 /// This function is meant to be overriden. 464 /// Between `onMouseEnter` and `onMouseExit`, `isMouseOver` will return `true`. 465 /// 466 /// Example: use this callback to call `setDirtyWhole` so that you can draw a highlight when 467 /// the widget is pointed to. 468 void onMouseEnter() 469 { 470 } 471 472 /// Called when mouse enter this Element. 473 /// This function is meant to be overriden. 474 /// Between `onMouseEnter` and `onMouseExit`, `isMouseOver` will return `true`. 475 /// 476 /// Example: use this callback to call `setDirtyWhole` so that you can draw a lack of highlight 477 /// when the widget is pointed to. 478 void onMouseExit() 479 { 480 } 481 482 /// Called when this Element is clicked and get the "focus" (ie. keyboard focus). 483 /// This function is meant to be overriden. 484 void onFocusEnter() 485 { 486 } 487 488 /// Called when focus is lost because another Element was clicked. 489 /// This widget then loose the "focus" (ie. keyboard focus). 490 /// This function is meant to be overriden. 491 /// 492 /// Example: if a widget is foldable (like a popup menu), you can us this callback to close it 493 /// once the user click anywhere else. 494 void onFocusExit() 495 { 496 } 497 498 /// Called when a key is pressed. This event bubbles down-up until being processed. 499 /// Return true if treating the message. 500 bool onKeyDown(Key key) 501 { 502 return false; 503 } 504 505 /// Called when a key is pressed. This event bubbles down-up until being processed. 506 /// Return true if treating the message. 507 bool onKeyUp(Key key) 508 { 509 return false; 510 } 511 512 /// Check if given point is within the widget. 513 /// Override this to disambiguate clicks and mouse-over between widgets that 514 /// would otherwise partially overlap. 515 /// 516 /// `x` and `y` are given in local widget coordinates. 517 /// IMPORTANT: a widget CANNOT be clickable beyond its _position. 518 /// For now, there is no good reason for that, but it could be useful 519 /// in the future if we get acceleration structure for picking elements. 520 bool contains(int x, int y) 521 { 522 return (x < cast(uint)(_position.width ) ) 523 && (y < cast(uint)(_position.height) ); 524 } 525 526 // to be called at top-level when the mouse clicked 527 final bool mouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate) 528 { 529 recomputeZOrderedChildren(); 530 531 // Test children that are displayed above this element first 532 foreach(child; _zOrderedChildren[]) 533 { 534 if (child.zOrder >= zOrder) 535 if (child.mouseClick(x, y, button, isDoubleClick, mstate)) 536 return true; 537 } 538 539 // Test for collision with this element 540 bool canBeClicked = _visibilityStatus; // cannot be clicked if invisible 541 if (canBeClicked && contains(x - _position.min.x, y - _position.min.y)) 542 { 543 Click click = onMouseClick(x - _position.min.x, y - _position.min.y, button, isDoubleClick, mstate); 544 545 final switch(click) 546 { 547 case Click.handled: 548 _context.setFocused(this); 549 return true; 550 551 case Click.startDrag: 552 _context.beginDragging(this); 553 goto case Click.handled; 554 555 case Click.unhandled: 556 return false; 557 } 558 } 559 560 // Test children that are displayed below this element last 561 foreach(child; _zOrderedChildren[]) 562 { 563 if (child.zOrder < zOrder) 564 if (child.mouseClick(x, y, button, isDoubleClick, mstate)) 565 return true; 566 } 567 568 return false; 569 } 570 571 // to be called at top-level when the mouse is released 572 final void mouseRelease(int x, int y, int button, MouseState mstate) 573 { 574 version(futureMouseDrag) 575 { 576 bool wasDragging = (_context.dragged !is null); 577 } 578 579 _context.stopDragging(); 580 581 version(futureMouseDrag) 582 { 583 // Enter widget below mouse if a dragged operation was stopped. 584 if (wasDragging) 585 { 586 bool foundOver = mouseMove(x, y, 0, 0, mstate, false); 587 if (!foundOver) 588 _context.setMouseOver(null); 589 } 590 } 591 } 592 593 // to be called at top-level when the mouse wheeled 594 final bool mouseWheel(int x, int y, int wheelDeltaX, int wheelDeltaY, MouseState mstate) 595 { 596 recomputeZOrderedChildren(); 597 598 // Test children that are displayed above this element first 599 foreach(child; _zOrderedChildren[]) 600 { 601 if (child.zOrder >= zOrder) 602 if (child.mouseWheel(x, y, wheelDeltaX, wheelDeltaY, mstate)) 603 return true; 604 } 605 606 bool canBeMouseWheeled = _visibilityStatus; // cannot be mouse-wheeled if invisible 607 if (canBeMouseWheeled && contains(x - _position.min.x, y - _position.min.y)) 608 { 609 if (onMouseWheel(x - _position.min.x, y - _position.min.y, wheelDeltaX, wheelDeltaY, mstate)) 610 return true; 611 } 612 613 // Test children that are displayed below this element last 614 foreach(child; _zOrderedChildren[]) 615 { 616 if (child.zOrder < zOrder) 617 if (child.mouseWheel(x, y, wheelDeltaX, wheelDeltaY, mstate)) 618 return true; 619 } 620 621 return false; 622 } 623 624 // To be called when the mouse moved 625 // Returns: `true` if one child has taken the mouse-over role globally. 626 final bool mouseMove(int x, int y, int dx, int dy, MouseState mstate, bool alreadyFoundMouseOver) 627 { 628 recomputeZOrderedChildren(); 629 630 bool foundMouseOver = alreadyFoundMouseOver; 631 632 // Test children that are displayed above this element first 633 foreach(child; _zOrderedChildren[]) 634 { 635 if (child.zOrder >= zOrder) 636 { 637 bool found = child.mouseMove(x, y, dx, dy, mstate, foundMouseOver); 638 foundMouseOver = foundMouseOver || found; 639 } 640 } 641 642 if (isDragged()) 643 { 644 // EDIT MODE 645 // In debug mode, dragging with the right mouse button move elements around 646 // and dragging with shift + right button resize elements around. 647 // 648 // Additionally, if CTRL is pressed, the increments are only -1 or +1 pixel. 649 // 650 // You can see the _position rectangle thanks to `debugLog`. 651 bool draggingUsed = false; 652 debug 653 { 654 if (mstate.rightButtonDown && mstate.shiftPressed) 655 { 656 if (mstate.ctrlPressed) 657 { 658 dx = clamp(dx, -1, +1); 659 dy = clamp(dy, -1, +1); 660 } 661 int nx = _position.min.x; 662 int ny = _position.min.y; 663 int w = _position.width + dx; 664 int h = _position.height + dy; 665 if (w < 5) w = 5; 666 if (h < 5) h = 5; 667 position = box2i(nx, ny, nx + w, ny + h); 668 draggingUsed = true; 669 670 671 } 672 else if (mstate.rightButtonDown) 673 { 674 if (mstate.ctrlPressed) 675 { 676 dx = clamp(dx, -1, +1); 677 dy = clamp(dy, -1, +1); 678 } 679 int nx = _position.min.x + dx; 680 int ny = _position.min.y + dy; 681 if (nx < 0) nx = 0; 682 if (ny < 0) ny = 0; 683 position = box2i(nx, ny, nx + position.width, ny + position.height); 684 draggingUsed = true; 685 } 686 687 // Output the latest position 688 // This is helpful when developing a plug-in UI. 689 if (draggingUsed) 690 { 691 char[128] buf; 692 snprintf(buf.ptr, 128, "position = box2i.rectangle(%d, %d, %d, %d)\n", _position.min.x, _position.min.y, _position.width, _position.height); 693 debugLog(buf.ptr); 694 } 695 } 696 697 if (!draggingUsed) 698 onMouseDrag(x - _position.min.x, y - _position.min.y, dx, dy, mstate); 699 } 700 701 // Can't be mouse over if not visible. 702 bool canBeMouseOver = _visibilityStatus; 703 704 version(futureMouseDrag) 705 { 706 // If dragged, it already received `onMouseDrag`. 707 // if something else is dragged, it can be mouse over. 708 if (_context.dragged !is null) 709 canBeMouseOver = false; 710 } 711 712 if (canBeMouseOver && contains(x - _position.min.x, y - _position.min.y)) // FUTURE: something more fine-grained? 713 { 714 // Get the mouse-over crown if not taken 715 if (!foundMouseOver) 716 { 717 foundMouseOver = true; 718 _context.setMouseOver(this); 719 720 version(futureMouseDrag) 721 { 722 onMouseMove(x - _position.min.x, y - _position.min.y, dx, dy, mstate); 723 } 724 } 725 726 version(futureMouseDrag) 727 {} 728 else 729 { 730 onMouseMove(x - _position.min.x, y - _position.min.y, dx, dy, mstate); 731 } 732 } 733 734 // Test children that are displayed below this element 735 foreach(child; _zOrderedChildren[]) 736 { 737 if (child.zOrder < zOrder) 738 { 739 bool found = child.mouseMove(x, y, dx, dy, mstate, foundMouseOver); 740 foundMouseOver = foundMouseOver || found; 741 } 742 } 743 return foundMouseOver; 744 } 745 746 // to be called at top-level when a key is pressed 747 final bool keyDown(Key key) 748 { 749 if (onKeyDown(key)) 750 return true; 751 752 foreach(child; _children[]) 753 { 754 if (child.keyDown(key)) 755 return true; 756 } 757 return false; 758 } 759 760 // to be called at top-level when a key is released 761 final bool keyUp(Key key) 762 { 763 if (onKeyUp(key)) 764 return true; 765 766 foreach(child; _children[]) 767 { 768 if (child.keyUp(key)) 769 return true; 770 } 771 return false; 772 } 773 774 // To be called at top-level periodically. 775 void animate(double dt, double time) 776 { 777 if (isAnimated) 778 onAnimate(dt, time); 779 780 // For some rare widgets, it is important that children are animated 781 // _after_ their parent. 782 foreach(child; _children[]) 783 child.animate(dt, time); 784 } 785 786 final UIContext context() 787 { 788 return _context; 789 } 790 791 /// A widget is "visible" when it has a true visibility flag, and its parent is itself visible. 792 /// Returns: Last computed visibility status. 793 final bool isVisible() pure const 794 { 795 return _visibilityStatus; 796 } 797 798 /// Get visibility flag of the widget. 799 /// A widget might still be invisible, if one of its parent is not visible. 800 final bool visibility() pure const 801 { 802 return _visibleFlag; 803 } 804 805 /// Change visibility flag of the widget. This show and hide all children of this UIElement, 806 /// regardless of their position on screen, invalidating their graphics if need be much 807 /// like a position change. 808 final void visibility(bool visible) 809 { 810 if (_visibleFlag == visible) 811 return; // Nothing to do, this wouldn't change any visibility status in sub-tree. 812 813 _visibleFlag = visible; 814 815 // Get parent visibility status. 816 bool parentVisibleStatus = (parent !is null) ? parent.isVisible() : true; 817 818 // Recompute own visibility status. 819 recomputeVisibilityStatus(parentVisibleStatus); 820 } 821 822 final int zOrder() pure const 823 { 824 return _zOrder; 825 } 826 827 // TODO: how to deprecate that? Wren will stumble upon every deprecated fields unfortunately. 828 alias setZOrder = zOrder; 829 830 final void zOrder(int zOrder) 831 { 832 if (_zOrder != zOrder) 833 { 834 setDirtyWhole(); 835 _zOrder = zOrder; 836 } 837 } 838 839 /// Mark this element as wholly dirty. 840 /// 841 /// Params: 842 /// layer which layers need to be redrawn. 843 /// 844 /// Important: you _can_ call this from the audio thread, HOWEVER it is 845 /// much more efficient to mark the widget dirty with an atomic 846 /// and call `setDirty` in animation callback. 847 void setDirtyWhole(UILayer layer = UILayer.guessFromFlags) 848 { 849 addDirtyRect(_position, layer); 850 } 851 852 /// Mark a part of the element dirty. 853 /// This part must be a subrect of its _position. 854 /// 855 /// Params: 856 /// rect = Position of the dirtied rectangle, in widget coordinates. 857 /// 858 /// Important: you could call this from the audio thread, however it is 859 /// much more efficient to mark the widget dirty with an atomic 860 /// and call setDirty in animation callback. 861 void setDirty(box2i rect, UILayer layer = UILayer.guessFromFlags) 862 { 863 /// BUG: it is problematic to allow this from the audio thread, 864 /// because the access to _position isn't protected and it could 865 /// create a race in case of concurrent reflow(). Puhsed rectangles 866 /// might be out of range, this is workarounded in GUIGraphics currently 867 /// for other reasons. 868 box2i translatedRect = rect.translate(_position.min); 869 assert(_position.contains(translatedRect)); 870 addDirtyRect(translatedRect, layer); 871 } 872 873 /// Returns: Parent element. `null` if detached or root element. 874 final UIElement parent() pure nothrow @nogc 875 { 876 return _parent; 877 } 878 879 /// Returns: Top-level parent. `null` if detached or root element. 880 final UIElement topLevelParent() pure nothrow @nogc 881 { 882 if (_parent is null) 883 return this; 884 else 885 return _parent.topLevelParent(); 886 } 887 888 /// Returns: `true` is this element is hovered by the mouse, and 889 final bool isMouseOver() pure const 890 { 891 version(futureMouseDrag) 892 { 893 if (_context.mouseOver !is this) 894 { 895 assert(_context.dragged !is this); 896 } 897 } 898 899 return _context.mouseOver is this; 900 } 901 902 final bool isDragged() pure const 903 { 904 version(futureMouseDrag) 905 { 906 if (_context.dragged is this) 907 assert(isMouseOver()); 908 } 909 910 return _context.dragged is this; 911 } 912 913 final bool isFocused() pure const 914 { 915 return _context.focused is this; 916 } 917 918 final bool drawsToPBR() pure const 919 { 920 return (_flags & flagPBR) != 0; 921 } 922 923 final bool drawsToRaw() pure const 924 { 925 return (_flags & flagRaw) != 0; 926 } 927 928 final bool isAnimated() pure const 929 { 930 return (_flags & flagAnimated) != 0; 931 } 932 933 final bool isDrawAloneRaw() pure const 934 { 935 return (_flags & flagDrawAloneRaw) != 0; 936 } 937 938 final bool isDrawAlonePBR() pure const 939 { 940 return (_flags & flagDrawAlonePBR) != 0; 941 } 942 943 /// Appends the Elements that should be drawn, in order. 944 /// You should empty it before calling this function. 945 /// Everything visible get into the draw list, but that doesn't mean they 946 /// will get drawn if they don't overlap with a dirty area. 947 final void getDrawLists(ref Vec!UIElement listRaw, ref Vec!UIElement listPBR) 948 { 949 if (_visibilityStatus) 950 { 951 if (drawsToRaw()) 952 listRaw.pushBack(this); 953 954 if (drawsToPBR()) 955 listPBR.pushBack(this); 956 957 // Note: if one widget is not visible, the whole sub-tree can be ignored for drawing. 958 // This is because invisibility is inherited without recourse. 959 foreach(child; _children[]) 960 child.getDrawLists(listRaw, listPBR); 961 } 962 } 963 964 MouseCursor cursorWhenDragged() 965 { 966 return _cursorWhenDragged; 967 } 968 969 void setCursorWhenDragged(MouseCursor mouseCursor) 970 { 971 _cursorWhenDragged = mouseCursor; 972 } 973 974 MouseCursor cursorWhenMouseOver() 975 { 976 return _cursorWhenMouseOver; 977 } 978 979 void setCursorWhenMouseOver(MouseCursor mouseCursor) 980 { 981 _cursorWhenMouseOver = mouseCursor; 982 } 983 984 /// Get a user pointer. Allow dplug:gui extensions. 985 final void* getUserPointer(int pointerID) 986 { 987 return _userPointers[pointerID]; 988 } 989 990 /// Set a user pointer. Allow dplug:gui extensions. 991 final void setUserPointer(int pointerID, void* userPointer) 992 { 993 _userPointers[pointerID] = userPointer; 994 } 995 996 protected: 997 998 /// Raw layer draw method. This gives you 1 surface cropped by _position for drawing. 999 /// Note that you are not forced to draw to the surfaces at all. 1000 /// 1001 /// `UIElement` are drawn by increasing z-order, or lexical order if lack thereof. 1002 /// Those elements who have non-overlapping `_position` are drawn in parallel. 1003 /// Hence you CAN'T draw outside `_position` and receive cropped surfaces. 1004 /// 1005 /// IMPORTANT: you MUST NOT draw outside `dirtyRects`. This allows more fine-grained updates. 1006 /// A `UIElement` that doesn't respect dirtyRects will have PAINFUL display problems. 1007 void onDrawRaw(ImageRef!RGBA rawMap, box2i[] dirtyRects) 1008 { 1009 // empty by default, meaning this UIElement does not draw on the Raw layer 1010 } 1011 1012 /// PBR layer draw method. This gives you 3 surfaces cropped by _position for drawing. 1013 /// Note that you are not forced to draw all to the surfaces at all, in which case the 1014 /// below `UIElement` will be displayed. 1015 /// 1016 /// `UIElement` are drawn by increasing z-order, or lexical order if lack thereof. 1017 /// Those elements who have non-overlapping `_position` are drawn in parallel. 1018 /// Hence you CAN'T draw outside `_position` and receive cropped surfaces. 1019 /// `diffuseMap`, `depthMap` and `materialMap` are made to span _position exactly. 1020 /// 1021 /// IMPORTANT: you MUST NOT draw outside `dirtyRects`. This allows more fine-grained updates. 1022 /// A `UIElement` that doesn't respect dirtyRects will have PAINFUL display problems. 1023 void onDrawPBR(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects) 1024 { 1025 // defaults to filling with a grey pattern 1026 RGBA darkGrey = RGBA(100, 100, 100, 0); 1027 RGBA lighterGrey = RGBA(150, 150, 150, 0); 1028 1029 foreach(dirtyRect; dirtyRects) 1030 { 1031 for (int y = dirtyRect.min.y; y < dirtyRect.max.y; ++y) 1032 { 1033 L16[] depthScan = depthMap.scanline(y); 1034 RGBA[] diffuseScan = diffuseMap.scanline(y); 1035 RGBA[] materialScan = materialMap.scanline(y); 1036 for (int x = dirtyRect.min.x; x < dirtyRect.max.x; ++x) 1037 { 1038 diffuseScan.ptr[x] = ( (x >> 3) ^ (y >> 3) ) & 1 ? darkGrey : lighterGrey; 1039 depthScan.ptr[x] = L16(defaultDepth); 1040 materialScan.ptr[x] = RGBA(defaultRoughness, defaultMetalnessDielectric, defaultSpecular, 255); 1041 } 1042 } 1043 } 1044 } 1045 1046 /// Called periodically for every `UIElement`. 1047 /// Override this to create animations. 1048 /// Using setDirty there allows to redraw an element continuously (like a meter or an animated object). 1049 /// Warning: Summing `dt` will not lead to a time that increase like `time`. 1050 /// `time` can go backwards if the window was reopen. 1051 /// `time` is guaranteed to increase as fast as system time but is not synced to audio time. 1052 void onAnimate(double dt, double time) 1053 { 1054 } 1055 1056 /// Parent element. 1057 /// Following this chain gets to the root element. 1058 UIElement _parent = null; 1059 1060 /// Position is the graphical extent of the element, or something larger. 1061 /// An `UIElement` is not allowed though to draw further than its _position. 1062 /// For efficiency it's best to keep `_position` as small as feasible. 1063 /// This is an absolute "world" positioning data, that doesn't depend on the parent's position. 1064 box2i _position; 1065 1066 /// The list of children UI elements. 1067 Vec!UIElement _children; 1068 1069 /// Flags, for now immutable 1070 immutable(uint) _flags; 1071 1072 /// Higher z-order = above other `UIElement`. 1073 /// By default, every `UIElement` have the same z-order. 1074 /// Because the sort is stable, tree traversal order is the default order (depth first). 1075 /// The children added last with `addChild` is considered above its siblings if you don't have legacyZOrder. 1076 int _zOrder = 0; 1077 1078 private: 1079 1080 /// Reference to owning context. 1081 UIContext _context; 1082 1083 // <visibility privates> 1084 1085 /// If _visibleFlag is false, neither the Element nor its children are drawn. 1086 /// Each UIElement starts its life being visible. 1087 bool _visibleFlag = true; 1088 1089 /// Final visibility value, cached in order to set rectangles dirty. 1090 /// It is always up to date across the whole UI tree. 1091 bool _visibilityStatus = true; 1092 1093 void recomputeVisibilityStatus(bool parentVisibilityStatus) 1094 { 1095 bool newVisibleStatus = _visibleFlag && parentVisibilityStatus; 1096 1097 // has it changed in any way? 1098 if (newVisibleStatus != _visibilityStatus) 1099 { 1100 _visibilityStatus = newVisibleStatus; 1101 1102 // Dirty the widget position 1103 setDirtyWhole(); 1104 1105 // Must inform children of the new status of parent. 1106 foreach(child; _children[]) 1107 child.recomputeVisibilityStatus(newVisibleStatus); 1108 } 1109 } 1110 1111 // </visibility privates> 1112 1113 /// Dirty rectangles buffer, cropped to _position. 1114 Vec!box2i _localRectsBuf; 1115 1116 /// Sorted children in Z-lexical-order (sorted by Z, or else increasing index in _children). 1117 Vec!UIElement _zOrderedChildren; 1118 1119 /// The mouse cursor to display when this element is being dragged 1120 MouseCursor _cursorWhenDragged = MouseCursor.pointer; 1121 1122 /// The mouse cursor to display when this element is being moused over 1123 MouseCursor _cursorWhenMouseOver = MouseCursor.pointer; 1124 1125 /// Identifier storage. 1126 char[maxUIElementIDLength+1] _idStorage; 1127 1128 /// Warning: if you store objects here, keep in mind they won't get destroyed automatically. 1129 /// 4 user pointer in case you'd like to store things in UIElement as a Dplug extension. 1130 /// id 0..1 are reserved for Wren support. 1131 /// id 2..3 are reserved for future Dplug extensions. 1132 /// id 4..7 are for vendor-specific extensions. 1133 void*[8] _userPointers; // Opaque pointers for Wren VM and things. 1134 1135 // Sort children in ascending z-order 1136 // Input: unsorted _children 1137 // Output: sorted _zOrderedChildren 1138 // This is not thread-safe. 1139 // Only one widget in the same UI can sort its children at once, since it uses 1140 // a UIContext buffer to do so. 1141 final void recomputeZOrderedChildren() 1142 { 1143 // Get a z-ordered list of childrens 1144 _zOrderedChildren.clearContents(); 1145 1146 /// See: https://github.com/AuburnSounds/Dplug/issues/652 1147 version(legacyZOrder) 1148 { 1149 foreach(child; _children[]) 1150 _zOrderedChildren.pushBack(child); 1151 } 1152 else 1153 { 1154 // Adding children in reverse, since children added last are considered having a higher Z order. 1155 foreach_reverse(child; _children[]) 1156 _zOrderedChildren.pushBack(child); 1157 } 1158 1159 timSort!UIElement(_zOrderedChildren[], 1160 context.sortingScratchBuffer(), 1161 (a, b) nothrow @nogc 1162 { 1163 if (a.zOrder < b.zOrder) return 1; 1164 else if (a.zOrder > b.zOrder) return -1; 1165 else return 0; 1166 }); 1167 1168 } 1169 1170 final void addDirtyRect(box2i rect, UILayer layer) 1171 { 1172 final switch(layer) 1173 { 1174 case UILayer.guessFromFlags: 1175 if (drawsToPBR()) 1176 { 1177 // Note: even if one UIElement draws to both Raw and PBR layers, we are not 1178 // adding this rectangle in `dirtyListRaw` since the Raw layer is automatically 1179 // updated when the PBR layer below is. 1180 _context.dirtyListPBR.addRect(rect); 1181 } 1182 else if (drawsToRaw()) 1183 { 1184 _context.dirtyListRaw.addRect(rect); 1185 } 1186 break; 1187 1188 case UILayer.rawOnly: 1189 _context.dirtyListRaw.addRect(rect); 1190 break; 1191 1192 case UILayer.allLayers: 1193 // This will lead the Raw layer to be invalidated too 1194 _context.dirtyListPBR.addRect(rect); 1195 break; 1196 } 1197 } 1198 } 1199 1200 version(legacyMouseOver) 1201 { 1202 static assert(false, "legacyMouseOver was removed in Dplug v13. Please see Release Notes."); 1203 }