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