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 "legacyMouseDrag" version identifier is NOT 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 "legacyMouseDrag" version identifier is NOT 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(legacyMouseDrag) 574 {} 575 else 576 { 577 bool wasDragging = (_context.dragged !is null); 578 } 579 580 _context.stopDragging(); 581 582 version(legacyMouseDrag) 583 {} 584 else 585 { 586 // Enter widget below mouse if a dragged operation was stopped. 587 if (wasDragging) 588 { 589 bool foundOver = mouseMove(x, y, 0, 0, mstate, false); 590 if (!foundOver) 591 _context.setMouseOver(null); 592 } 593 } 594 } 595 596 // to be called at top-level when the mouse wheeled 597 final bool mouseWheel(int x, int y, int wheelDeltaX, int wheelDeltaY, MouseState mstate) 598 { 599 recomputeZOrderedChildren(); 600 601 // Test children that are displayed above this element first 602 foreach(child; _zOrderedChildren[]) 603 { 604 if (child.zOrder >= zOrder) 605 if (child.mouseWheel(x, y, wheelDeltaX, wheelDeltaY, mstate)) 606 return true; 607 } 608 609 bool canBeMouseWheeled = _visibilityStatus; // cannot be mouse-wheeled if invisible 610 if (canBeMouseWheeled && contains(x - _position.min.x, y - _position.min.y)) 611 { 612 if (onMouseWheel(x - _position.min.x, y - _position.min.y, wheelDeltaX, wheelDeltaY, mstate)) 613 return true; 614 } 615 616 // Test children that are displayed below this element last 617 foreach(child; _zOrderedChildren[]) 618 { 619 if (child.zOrder < zOrder) 620 if (child.mouseWheel(x, y, wheelDeltaX, wheelDeltaY, mstate)) 621 return true; 622 } 623 624 return false; 625 } 626 627 // To be called when the mouse moved 628 // Returns: `true` if one child has taken the mouse-over role globally. 629 final bool mouseMove(int x, int y, int dx, int dy, MouseState mstate, bool alreadyFoundMouseOver) 630 { 631 recomputeZOrderedChildren(); 632 633 bool foundMouseOver = alreadyFoundMouseOver; 634 635 // Test children that are displayed above this element first 636 foreach(child; _zOrderedChildren[]) 637 { 638 if (child.zOrder >= zOrder) 639 { 640 bool found = child.mouseMove(x, y, dx, dy, mstate, foundMouseOver); 641 foundMouseOver = foundMouseOver || found; 642 } 643 } 644 645 if (isDragged()) 646 { 647 // EDIT MODE 648 // In debug mode, dragging with the right mouse button move elements around 649 // and dragging with shift + right button resize elements around. 650 // 651 // Additionally, if CTRL is pressed, the increments are only -1 or +1 pixel. 652 // 653 // You can see the _position rectangle thanks to `debugLog`. 654 bool draggingUsed = false; 655 debug 656 { 657 if (mstate.rightButtonDown && mstate.shiftPressed) 658 { 659 if (mstate.ctrlPressed) 660 { 661 if (dx < -1) dx = -1; 662 if (dx > 1) dx = 1; 663 if (dy < -1) dy = -1; 664 if (dy > 1) dy = 1; 665 } 666 int nx = _position.min.x; 667 int ny = _position.min.y; 668 int w = _position.width + dx; 669 int h = _position.height + dy; 670 if (w < 5) w = 5; 671 if (h < 5) h = 5; 672 position = box2i(nx, ny, nx + w, ny + h); 673 draggingUsed = true; 674 675 676 } 677 else if (mstate.rightButtonDown) 678 { 679 if (mstate.ctrlPressed) 680 { 681 if (dx < -1) dx = -1; 682 if (dx > 1) dx = 1; 683 if (dy < -1) dy = -1; 684 if (dy > 1) dy = 1; 685 } 686 int nx = _position.min.x + dx; 687 int ny = _position.min.y + dy; 688 if (nx < 0) nx = 0; 689 if (ny < 0) ny = 0; 690 position = box2i(nx, ny, nx + position.width, ny + position.height); 691 draggingUsed = true; 692 } 693 694 // Output the latest position 695 // This is helpful when developing a plug-in UI. 696 if (draggingUsed) 697 { 698 char[128] buf; 699 snprintf(buf.ptr, 128, "position = box2i.rectangle(%d, %d, %d, %d)\n", _position.min.x, _position.min.y, _position.width, _position.height); 700 debugLog(buf.ptr); 701 } 702 } 703 704 if (!draggingUsed) 705 onMouseDrag(x - _position.min.x, y - _position.min.y, dx, dy, mstate); 706 } 707 708 // Can't be mouse over if not visible. 709 bool canBeMouseOver = _visibilityStatus; 710 711 version(legacyMouseDrag) 712 {} 713 else 714 { 715 // If dragged, it already received `onMouseDrag`. 716 // if something else is dragged, it can be mouse over. 717 if (_context.dragged !is null) 718 canBeMouseOver = false; 719 } 720 721 if (canBeMouseOver && contains(x - _position.min.x, y - _position.min.y)) // FUTURE: something more fine-grained? 722 { 723 // Get the mouse-over crown if not taken 724 if (!foundMouseOver) 725 { 726 foundMouseOver = true; 727 _context.setMouseOver(this); 728 729 version(legacyMouseDrag) 730 {} 731 else 732 { 733 onMouseMove(x - _position.min.x, y - _position.min.y, dx, dy, mstate); 734 } 735 } 736 737 version(legacyMouseDrag) 738 { 739 onMouseMove(x - _position.min.x, y - _position.min.y, dx, dy, mstate); 740 } 741 } 742 743 // Test children that are displayed below this element 744 foreach(child; _zOrderedChildren[]) 745 { 746 if (child.zOrder < zOrder) 747 { 748 bool found = child.mouseMove(x, y, dx, dy, mstate, foundMouseOver); 749 foundMouseOver = foundMouseOver || found; 750 } 751 } 752 return foundMouseOver; 753 } 754 755 // to be called at top-level when a key is pressed 756 final bool keyDown(Key key) 757 { 758 if (onKeyDown(key)) 759 return true; 760 761 foreach(child; _children[]) 762 { 763 if (child.keyDown(key)) 764 return true; 765 } 766 return false; 767 } 768 769 // to be called at top-level when a key is released 770 final bool keyUp(Key key) 771 { 772 if (onKeyUp(key)) 773 return true; 774 775 foreach(child; _children[]) 776 { 777 if (child.keyUp(key)) 778 return true; 779 } 780 return false; 781 } 782 783 // To be called at top-level periodically. 784 void animate(double dt, double time) 785 { 786 if (isAnimated) 787 onAnimate(dt, time); 788 789 // For some rare widgets, it is important that children are animated 790 // _after_ their parent. 791 foreach(child; _children[]) 792 child.animate(dt, time); 793 } 794 795 final UIContext context() 796 { 797 return _context; 798 } 799 800 /// A widget is "visible" when it has a true visibility flag, and its parent is itself visible. 801 /// Returns: Last computed visibility status. 802 final bool isVisible() pure const 803 { 804 return _visibilityStatus; 805 } 806 807 /// Get visibility flag of the widget. 808 /// A widget might still be invisible, if one of its parent is not visible. 809 final bool visibility() pure const 810 { 811 return _visibleFlag; 812 } 813 814 /// Change visibility flag of the widget. This show and hide all children of this UIElement, 815 /// regardless of their position on screen, invalidating their graphics if need be much 816 /// like a position change. 817 final void visibility(bool visible) 818 { 819 if (_visibleFlag == visible) 820 return; // Nothing to do, this wouldn't change any visibility status in sub-tree. 821 822 _visibleFlag = visible; 823 824 // Get parent visibility status. 825 bool parentVisibleStatus = (parent !is null) ? parent.isVisible() : true; 826 827 // Recompute own visibility status. 828 recomputeVisibilityStatus(parentVisibleStatus); 829 } 830 831 final int zOrder() pure const 832 { 833 return _zOrder; 834 } 835 836 // TODO: how to depreciate that? Wren will stumble upon every deprecated fields unfortunately. 837 alias setZOrder = zOrder; 838 839 final void zOrder(int zOrder) 840 { 841 if (_zOrder != zOrder) 842 { 843 setDirtyWhole(); 844 _zOrder = zOrder; 845 } 846 } 847 848 /// Mark this element as wholly dirty. 849 /// 850 /// Params: 851 /// layer which layers need to be redrawn. 852 /// 853 /// Important: you _can_ call this from the audio thread, HOWEVER it is 854 /// much more efficient to mark the widget dirty with an atomic 855 /// and call `setDirty` in animation callback. 856 void setDirtyWhole(UILayer layer = UILayer.guessFromFlags) 857 { 858 addDirtyRect(_position, layer); 859 } 860 861 /// Mark a part of the element dirty. 862 /// This part must be a subrect of its _position. 863 /// 864 /// Params: 865 /// rect = Position of the dirtied rectangle, in widget coordinates. 866 /// 867 /// Important: you could call this from the audio thread, however it is 868 /// much more efficient to mark the widget dirty with an atomic 869 /// and call setDirty in animation callback. 870 void setDirty(box2i rect, UILayer layer = UILayer.guessFromFlags) 871 { 872 /// BUG: it is problematic to allow this from the audio thread, 873 /// because the access to _position isn't protected and it could 874 /// create a race in case of concurrent reflow(). Puhsed rectangles 875 /// might be out of range, this is workarounded in GUIGraphics currently 876 /// for other reasons. 877 box2i translatedRect = rect.translate(_position.min); 878 assert(_position.contains(translatedRect)); 879 addDirtyRect(translatedRect, layer); 880 } 881 882 /// Returns: Parent element. `null` if detached or root element. 883 final UIElement parent() pure nothrow @nogc 884 { 885 return _parent; 886 } 887 888 /// Returns: Top-level parent. `null` if detached or root element. 889 final UIElement topLevelParent() pure nothrow @nogc 890 { 891 if (_parent is null) 892 return this; 893 else 894 return _parent.topLevelParent(); 895 } 896 897 /// Returns: `true` is this element is hovered by the mouse, and 898 final bool isMouseOver() pure const 899 { 900 version(legacyMouseDrag) 901 {} 902 else 903 { 904 if (_context.mouseOver !is this) 905 { 906 assert(_context.dragged !is this); 907 } 908 } 909 910 return _context.mouseOver is this; 911 } 912 913 final bool isDragged() pure const 914 { 915 version(legacyMouseDrag) 916 {} 917 else 918 { 919 if (_context.dragged is this) 920 assert(isMouseOver()); 921 } 922 923 return _context.dragged is this; 924 } 925 926 final bool isFocused() pure const 927 { 928 return _context.focused is this; 929 } 930 931 final bool drawsToPBR() pure const 932 { 933 return (_flags & flagPBR) != 0; 934 } 935 936 final bool drawsToRaw() pure const 937 { 938 return (_flags & flagRaw) != 0; 939 } 940 941 final bool isAnimated() pure const 942 { 943 return (_flags & flagAnimated) != 0; 944 } 945 946 final bool isDrawAloneRaw() pure const 947 { 948 return (_flags & flagDrawAloneRaw) != 0; 949 } 950 951 final bool isDrawAlonePBR() pure const 952 { 953 return (_flags & flagDrawAlonePBR) != 0; 954 } 955 956 /// Appends the Elements that should be drawn, in order. 957 /// You should empty it before calling this function. 958 /// Everything visible get into the draw list, but that doesn't mean they 959 /// will get drawn if they don't overlap with a dirty area. 960 final void getDrawLists(ref Vec!UIElement listRaw, ref Vec!UIElement listPBR) 961 { 962 if (_visibilityStatus) 963 { 964 if (drawsToRaw()) 965 listRaw.pushBack(this); 966 967 if (drawsToPBR()) 968 listPBR.pushBack(this); 969 970 // Note: if one widget is not visible, the whole sub-tree can be ignored for drawing. 971 // This is because invisibility is inherited without recourse. 972 foreach(child; _children[]) 973 child.getDrawLists(listRaw, listPBR); 974 } 975 } 976 977 MouseCursor cursorWhenDragged() 978 { 979 return _cursorWhenDragged; 980 } 981 982 void setCursorWhenDragged(MouseCursor mouseCursor) 983 { 984 _cursorWhenDragged = mouseCursor; 985 } 986 987 MouseCursor cursorWhenMouseOver() 988 { 989 return _cursorWhenMouseOver; 990 } 991 992 void setCursorWhenMouseOver(MouseCursor mouseCursor) 993 { 994 _cursorWhenMouseOver = mouseCursor; 995 } 996 997 /// Get a user pointer. Allow dplug:gui extensions. 998 final void* getUserPointer(int pointerID) 999 { 1000 return _userPointers[pointerID]; 1001 } 1002 1003 /// Set a user pointer. Allow dplug:gui extensions. 1004 final void setUserPointer(int pointerID, void* userPointer) 1005 { 1006 _userPointers[pointerID] = userPointer; 1007 } 1008 1009 protected: 1010 1011 /// Raw layer draw method. This gives you 1 surface cropped by _position for drawing. 1012 /// Note that you are not forced to draw to the surfaces at all. 1013 /// 1014 /// `UIElement` are drawn by increasing z-order, or lexical order if lack thereof. 1015 /// Those elements who have non-overlapping `_position` are drawn in parallel. 1016 /// Hence you CAN'T draw outside `_position` and receive cropped surfaces. 1017 /// 1018 /// IMPORTANT: you MUST NOT draw outside `dirtyRects`. This allows more fine-grained updates. 1019 /// A `UIElement` that doesn't respect dirtyRects will have PAINFUL display problems. 1020 void onDrawRaw(ImageRef!RGBA rawMap, box2i[] dirtyRects) 1021 { 1022 // empty by default, meaning this UIElement does not draw on the Raw layer 1023 } 1024 1025 /// PBR layer draw method. This gives you 3 surfaces cropped by _position for drawing. 1026 /// Note that you are not forced to draw all to the surfaces at all, in which case the 1027 /// below `UIElement` will be displayed. 1028 /// 1029 /// `UIElement` are drawn by increasing z-order, or lexical order if lack thereof. 1030 /// Those elements who have non-overlapping `_position` are drawn in parallel. 1031 /// Hence you CAN'T draw outside `_position` and receive cropped surfaces. 1032 /// `diffuseMap`, `depthMap` and `materialMap` are made to span _position exactly. 1033 /// 1034 /// IMPORTANT: you MUST NOT draw outside `dirtyRects`. This allows more fine-grained updates. 1035 /// A `UIElement` that doesn't respect dirtyRects will have PAINFUL display problems. 1036 void onDrawPBR(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects) 1037 { 1038 // defaults to filling with a grey pattern 1039 RGBA darkGrey = RGBA(100, 100, 100, 0); 1040 RGBA lighterGrey = RGBA(150, 150, 150, 0); 1041 1042 foreach(dirtyRect; dirtyRects) 1043 { 1044 for (int y = dirtyRect.min.y; y < dirtyRect.max.y; ++y) 1045 { 1046 L16[] depthScan = depthMap.scanline(y); 1047 RGBA[] diffuseScan = diffuseMap.scanline(y); 1048 RGBA[] materialScan = materialMap.scanline(y); 1049 for (int x = dirtyRect.min.x; x < dirtyRect.max.x; ++x) 1050 { 1051 diffuseScan.ptr[x] = ( (x >> 3) ^ (y >> 3) ) & 1 ? darkGrey : lighterGrey; 1052 depthScan.ptr[x] = L16(defaultDepth); 1053 materialScan.ptr[x] = RGBA(defaultRoughness, defaultMetalnessDielectric, defaultSpecular, 255); 1054 } 1055 } 1056 } 1057 } 1058 1059 /// Called periodically for every `UIElement`. 1060 /// Override this to create animations. 1061 /// Using setDirty there allows to redraw an element continuously (like a meter or an animated object). 1062 /// Warning: Summing `dt` will not lead to a time that increase like `time`. 1063 /// `time` can go backwards if the window was reopen. 1064 /// `time` is guaranteed to increase as fast as system time but is not synced to audio time. 1065 void onAnimate(double dt, double time) 1066 { 1067 } 1068 1069 /// Parent element. 1070 /// Following this chain gets to the root element. 1071 UIElement _parent = null; 1072 1073 /// Position is the graphical extent of the element, or something larger. 1074 /// An `UIElement` is not allowed though to draw further than its _position. 1075 /// For efficiency it's best to keep `_position` as small as feasible. 1076 /// This is an absolute "world" positioning data, that doesn't depend on the parent's position. 1077 box2i _position; 1078 1079 /// The list of children UI elements. 1080 Vec!UIElement _children; 1081 1082 /// Flags, for now immutable 1083 immutable(uint) _flags; 1084 1085 /// Higher z-order = above other `UIElement`. 1086 /// By default, every `UIElement` have the same z-order. 1087 /// Because the sort is stable, tree traversal order is the default order (depth first). 1088 /// The children added last with `addChild` is considered above its siblings if you don't have legacyZOrder. 1089 int _zOrder = 0; 1090 1091 private: 1092 1093 /// Reference to owning context. 1094 UIContext _context; 1095 1096 // <visibility privates> 1097 1098 /// If _visibleFlag is false, neither the Element nor its children are drawn. 1099 /// Each UIElement starts its life being visible. 1100 bool _visibleFlag = true; 1101 1102 /// Final visibility value, cached in order to set rectangles dirty. 1103 /// It is always up to date across the whole UI tree. 1104 bool _visibilityStatus = true; 1105 1106 void recomputeVisibilityStatus(bool parentVisibilityStatus) 1107 { 1108 bool newVisibleStatus = _visibleFlag && parentVisibilityStatus; 1109 1110 // has it changed in any way? 1111 if (newVisibleStatus != _visibilityStatus) 1112 { 1113 _visibilityStatus = newVisibleStatus; 1114 1115 // Dirty the widget position 1116 setDirtyWhole(); 1117 1118 // Must inform children of the new status of parent. 1119 foreach(child; _children[]) 1120 child.recomputeVisibilityStatus(newVisibleStatus); 1121 } 1122 } 1123 1124 // </visibility privates> 1125 1126 /// Dirty rectangles buffer, cropped to _position. 1127 Vec!box2i _localRectsBuf; 1128 1129 /// Sorted children in Z-lexical-order (sorted by Z, or else increasing index in _children). 1130 Vec!UIElement _zOrderedChildren; 1131 1132 /// The mouse cursor to display when this element is being dragged 1133 MouseCursor _cursorWhenDragged = MouseCursor.pointer; 1134 1135 /// The mouse cursor to display when this element is being moused over 1136 MouseCursor _cursorWhenMouseOver = MouseCursor.pointer; 1137 1138 /// Identifier storage. 1139 char[maxUIElementIDLength+1] _idStorage; 1140 1141 /// Warning: if you store objects here, keep in mind they won't get destroyed automatically. 1142 /// 4 user pointer in case you'd like to store things in UIElement as a Dplug extension. 1143 /// id 0..1 are reserved for Wren support. 1144 /// id 2..3 are reserved for future Dplug extensions. 1145 /// id 4..7 are for vendor-specific extensions. 1146 void*[8] _userPointers; // Opaque pointers for Wren VM and things. 1147 1148 // Sort children in ascending z-order 1149 // Input: unsorted _children 1150 // Output: sorted _zOrderedChildren 1151 // This is not thread-safe. 1152 // Only one widget in the same UI can sort its children at once, since it uses 1153 // a UIContext buffer to do so. 1154 final void recomputeZOrderedChildren() 1155 { 1156 // Get a z-ordered list of childrens 1157 _zOrderedChildren.clearContents(); 1158 1159 /// See: https://github.com/AuburnSounds/Dplug/issues/652 1160 version(legacyZOrder) 1161 { 1162 foreach(child; _children[]) 1163 _zOrderedChildren.pushBack(child); 1164 } 1165 else 1166 { 1167 // Adding children in reverse, since children added last are considered having a higher Z order. 1168 foreach_reverse(child; _children[]) 1169 _zOrderedChildren.pushBack(child); 1170 } 1171 1172 timSort!UIElement(_zOrderedChildren[], 1173 context.sortingScratchBuffer(), 1174 (a, b) nothrow @nogc 1175 { 1176 if (a.zOrder < b.zOrder) return 1; 1177 else if (a.zOrder > b.zOrder) return -1; 1178 else return 0; 1179 }); 1180 1181 } 1182 1183 final void addDirtyRect(box2i rect, UILayer layer) 1184 { 1185 final switch(layer) 1186 { 1187 case UILayer.guessFromFlags: 1188 if (drawsToPBR()) 1189 { 1190 // Note: even if one UIElement draws to both Raw and PBR layers, we are not 1191 // adding this rectangle in `dirtyListRaw` since the Raw layer is automatically 1192 // updated when the PBR layer below is. 1193 _context.dirtyListPBR.addRect(rect); 1194 } 1195 else if (drawsToRaw()) 1196 { 1197 _context.dirtyListRaw.addRect(rect); 1198 } 1199 break; 1200 1201 case UILayer.rawOnly: 1202 _context.dirtyListRaw.addRect(rect); 1203 break; 1204 1205 case UILayer.allLayers: 1206 // This will lead the Raw layer to be invalidated too 1207 _context.dirtyListPBR.addRect(rect); 1208 break; 1209 } 1210 } 1211 } 1212 1213 version(legacyMouseOver) 1214 { 1215 static assert(false, "legacyMouseOver was removed in Dplug v13. Please see Release Notes."); 1216 }