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 std.algorithm.comparison; 11 12 public import gfm.math.vector; 13 public import gfm.math.box; 14 15 public import dplug.graphics; 16 17 public import dplug.window.window; 18 19 public import dplug.core.sync; 20 public import dplug.core.vec; 21 public import dplug.core.nogc; 22 23 public import dplug.graphics.font; 24 public import dplug.graphics.drawex; 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 default value for the Physical channel (completely physical). 39 enum ubyte defaultPhysical = 255; 40 41 /// Reasonable dielectric default value for the Metalness channel. 42 enum ubyte defaultMetalnessDielectric = 25; // ~ 0.08 43 44 /// Reasonable metal default value for the Metalness channel. 45 enum ubyte defaultMetalnessMetal = 255; 46 47 /// Base class of the UI widget hierarchy. 48 /// 49 /// MAYDO: a bunch of stuff in that class is intended specifically for the root element, 50 /// there is probably a batter design to find 51 class UIElement 52 { 53 public: 54 nothrow: 55 @nogc: 56 57 this(UIContext context) 58 { 59 _context = context; 60 _localRectsBuf = makeVec!box2i(); 61 _children = makeVec!UIElement(); 62 _zOrderedChildren = makeVec!UIElement(); 63 } 64 65 ~this() 66 { 67 foreach(child; _children[]) 68 child.destroyFree(); 69 } 70 71 /// Returns: true if was drawn, ie. the buffers have changed. 72 /// This method is called for each item in the drawlist that was visible and dirty. 73 final void render(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, in box2i[] areasToUpdate) 74 { 75 // List of disjointed dirty rectangles intersecting with valid part of _position 76 // A nice thing with intersection is that a disjointed set of rectangles 77 // stays disjointed. 78 79 // we only consider the part of _position that is actually in the surface 80 box2i validPosition = _position.intersection(box2i(0, 0, diffuseMap.w, diffuseMap.h)); 81 82 if (validPosition.empty()) 83 return; // nothing to draw here 84 85 _localRectsBuf.clearContents(); 86 { 87 foreach(rect; areasToUpdate) 88 { 89 box2i inter = rect.intersection(validPosition); 90 91 if (!inter.empty) // don't consider empty rectangles 92 { 93 // Express the dirty rect in local coordinates for simplicity 94 _localRectsBuf.pushBack( inter.translate(-validPosition.min) ); 95 } 96 } 97 } 98 99 if (_localRectsBuf.length == 0) 100 return; // nothing to draw here 101 102 // Crop the diffuse and depth to the valid part of _position 103 // This is because drawing outside of _position is disallowed by design. 104 // Never do that! 105 ImageRef!RGBA diffuseMapCropped = diffuseMap.cropImageRef(validPosition); 106 ImageRef!L16 depthMapCropped = depthMap.cropImageRef(validPosition); 107 ImageRef!RGBA materialMapCropped = materialMap.cropImageRef(validPosition); 108 109 // Should never be an empty area there 110 assert(diffuseMapCropped.w != 0 && diffuseMapCropped.h != 0); 111 onDraw(diffuseMapCropped, depthMapCropped, materialMapCropped, _localRectsBuf[]); 112 } 113 114 /// TODO: useless until we have resizeable UIs. 115 /// Meant to be overriden almost everytime for custom behaviour. 116 /// Default behaviour is to span the whole area and reflow children. 117 /// Any layout algorithm is up to you. 118 /// Like in the DOM, children elements don't need to be inside _position of their parent. 119 void reflow(box2i availableSpace) 120 { 121 // default: span the entire available area, and do the same for children 122 _position = availableSpace; 123 124 foreach(ref child; _children) 125 child.reflow(availableSpace); 126 } 127 128 /// Returns: Position of the element, that will be used for rendering. This 129 /// position is reset when calling reflow. 130 final box2i position() nothrow @nogc 131 { 132 return _position; 133 } 134 135 /// Forces the position of the element. It is typically used in the parent 136 /// reflow() method 137 final box2i position(box2i p) nothrow @nogc 138 { 139 assert(p.isSorted()); 140 return _position = p; 141 } 142 143 final UIElement child(int n) 144 { 145 return _children[n]; 146 } 147 148 // The addChild method is mandatory. 149 // Such a child MUST be created through `dplug.core.nogc.mallocEmplace`. 150 // MAYDO: Should we dirty this place in the case it's not plugin creation? 151 final void addChild(UIElement element) 152 { 153 element._parent = this; 154 _children.pushBack(element); 155 } 156 157 /// Removes a child (but does not destroy it, you take back the ownership of it). 158 /// Useful for creating dynamic UI's. 159 /// MAYDO: there are restrictions for where this is allowed. Find them. 160 final void removeChild(UIElement element) 161 { 162 int index= _children.indexOf(element); 163 if(index >= 0) 164 { 165 // Dirty where the UIElement has been removed 166 element.setDirtyWhole(); 167 168 _children.removeAndReplaceByLastElement(index); 169 } 170 } 171 172 // This function is meant to be overriden. 173 // Happens _before_ checking for children collisions. 174 bool onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate) 175 { 176 return false; 177 } 178 179 // Mouse wheel was turned. 180 // This function is meant to be overriden. 181 // It should return true if the wheel is handled. 182 bool onMouseWheel(int x, int y, int wheelDeltaX, int wheelDeltaY, MouseState mstate) 183 { 184 return false; 185 } 186 187 // Called when mouse move over this Element. 188 // This function is meant to be overriden. 189 void onMouseMove(int x, int y, int dx, int dy, MouseState mstate) 190 { 191 } 192 193 // Called when clicked with left/middle/right button 194 // This function is meant to be overriden. 195 void onBeginDrag() 196 { 197 } 198 199 // Called when mouse drag this Element. 200 // This function is meant to be overriden. 201 void onMouseDrag(int x, int y, int dx, int dy, MouseState mstate) 202 { 203 } 204 205 // Called once drag is finished. 206 // This function is meant to be overriden. 207 void onStopDrag() 208 { 209 } 210 211 // Called when mouse enter this Element. 212 // This function is meant to be overriden. 213 void onMouseEnter() 214 { 215 } 216 217 // Called when mouse enter this Element. 218 // This function is meant to be overriden. 219 void onMouseExit() 220 { 221 } 222 223 // Called when a key is pressed. This event bubbles down-up until being processed. 224 // Return true if treating the message. 225 bool onKeyDown(Key key) 226 { 227 return false; 228 } 229 230 // Called when a key is pressed. This event bubbles down-up until being processed. 231 // Return true if treating the message. 232 bool onKeyUp(Key key) 233 { 234 return false; 235 } 236 237 // Check if given pixel is within the widget. 238 // FUTURE: This will be used to avoid making onMouseClick both the test and the event. 239 final bool contains(vec2i pt) 240 { 241 return _position.contains(pt); 242 } 243 244 // to be called at top-level when the mouse clicked 245 final bool mouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate) 246 { 247 recomputeZOrderedChildren(); 248 249 // Test children that are displayed above this element first 250 foreach(child; _zOrderedChildren[]) 251 { 252 if (child.zOrder >= zOrder) 253 if (child.mouseClick(x, y, button, isDoubleClick, mstate)) 254 return true; 255 } 256 257 // Test for collision with this element 258 if (contains(vec2i(x, y))) 259 { 260 if(onMouseClick(x - _position.min.x, y - _position.min.y, button, isDoubleClick, mstate)) 261 { 262 _context.beginDragging(this); 263 _context.setFocused(this); 264 return true; 265 } 266 } 267 268 // Test children that are displayed below this element last 269 foreach(child; _zOrderedChildren[]) 270 { 271 if (child.zOrder < zOrder) 272 if (child.mouseClick(x, y, button, isDoubleClick, mstate)) 273 return true; 274 } 275 276 return false; 277 } 278 279 // to be called at top-level when the mouse is released 280 final void mouseRelease(int x, int y, int button, MouseState mstate) 281 { 282 _context.stopDragging(); 283 } 284 285 // to be called at top-level when the mouse wheeled 286 final bool mouseWheel(int x, int y, int wheelDeltaX, int wheelDeltaY, MouseState mstate) 287 { 288 recomputeZOrderedChildren(); 289 290 // Test children that are displayed above this element first 291 foreach(child; _zOrderedChildren[]) 292 { 293 if (child.zOrder >= zOrder) 294 if (child.mouseWheel(x, y, wheelDeltaX, wheelDeltaY, mstate)) 295 return true; 296 } 297 298 if (contains(vec2i(x, y))) 299 { 300 if (onMouseWheel(x - _position.min.x, y - _position.min.y, wheelDeltaX, wheelDeltaY, mstate)) 301 return true; 302 } 303 304 // Test children that are displayed below this element last 305 foreach(child; _zOrderedChildren[]) 306 { 307 if (child.zOrder < zOrder) 308 if (child.mouseWheel(x, y, wheelDeltaX, wheelDeltaY, mstate)) 309 return true; 310 } 311 312 return false; 313 } 314 315 // to be called when the mouse moved 316 final void mouseMove(int x, int y, int dx, int dy, MouseState mstate) 317 { 318 if (isDragged) 319 { 320 // in debug mode, dragging with the right mouse button move elements around 321 // and dragging with shift + right button resize elements around 322 bool draggingUsed = false; 323 debug 324 { 325 if (mstate.rightButtonDown && mstate.shiftPressed) 326 { 327 int nx = _position.min.x; 328 int ny = _position.min.y; 329 int w = _position.width + dx; 330 int h = _position.height + dy; 331 if (w < 5) w = 5; 332 if (h < 5) h = 5; 333 setDirtyWhole(); 334 _position = box2i(nx, ny, nx + w, ny + h); 335 setDirtyWhole(); 336 draggingUsed = true; 337 } 338 else if (mstate.rightButtonDown) 339 { 340 int nx = _position.min.x + dx; 341 int ny = _position.min.y + dy; 342 if (nx < 0) nx = 0; 343 if (ny < 0) ny = 0; 344 setDirtyWhole(); 345 _position = box2i(nx, ny, nx + _position.width, ny + _position.height); 346 setDirtyWhole(); 347 draggingUsed = true; 348 } 349 } 350 351 if (!draggingUsed) 352 onMouseDrag(x - _position.min.x, y - _position.min.y, dx, dy, mstate); 353 } 354 355 // Note: no z-order for mouse-move, it's called for everything. Is it right? What would the DOM do? 356 357 foreach(child; _children[]) 358 { 359 child.mouseMove(x, y, dx, dy, mstate); 360 } 361 362 if (contains(vec2i(x, y))) // FUTURE: something more fine-grained? 363 { 364 if (!_mouseOver) 365 onMouseEnter(); 366 onMouseMove(x - _position.min.x, y - _position.min.y, dx, dy, mstate); 367 _mouseOver = true; 368 } 369 else 370 { 371 if (_mouseOver) 372 onMouseExit(); 373 _mouseOver = false; 374 } 375 } 376 377 // to be called at top-level when a key is pressed 378 final bool keyDown(Key key) 379 { 380 if (onKeyDown(key)) 381 return true; 382 383 foreach(child; _children[]) 384 { 385 if (child.keyDown(key)) 386 return true; 387 } 388 return false; 389 } 390 391 // to be called at top-level when a key is released 392 final bool keyUp(Key key) 393 { 394 if (onKeyUp(key)) 395 return true; 396 397 foreach(child; _children[]) 398 { 399 if (child.keyUp(key)) 400 return true; 401 } 402 return false; 403 } 404 405 // To be called at top-level periodically. 406 void animate(double dt, double time) nothrow @nogc 407 { 408 onAnimate(dt, time); 409 foreach(child; _children[]) 410 child.animate(dt, time); 411 } 412 413 final UIContext context() nothrow @nogc 414 { 415 return _context; 416 } 417 418 final bool isVisible() pure const nothrow @nogc 419 { 420 return _visible; 421 } 422 423 final void setVisible(bool visible) pure nothrow @nogc 424 { 425 _visible = visible; 426 } 427 428 final int zOrder() pure const nothrow @nogc 429 { 430 return _zOrder; 431 } 432 433 final void setZOrder(int zOrder) pure nothrow @nogc 434 { 435 _zOrder = zOrder; 436 } 437 438 /// Mark this element as wholly dirty. 439 /// Important: you could call this from the audio thread, however it is 440 /// much more efficient to mark the widget dirty with an atomic 441 /// and call setDirty in animation callback. 442 void setDirtyWhole() nothrow @nogc 443 { 444 _context.dirtyList.addRect(_position); 445 } 446 447 /// Mark a part of the element dirty. 448 /// This part must be a subrect of its _position. 449 /// Params: 450 /// rect = Position of the dirtied rectangle, in widget coordinates. 451 /// Important: you could call this from the audio thread, however it is 452 /// much more efficient to mark the widget dirty with an atomic 453 /// and call setDirty in animation callback. 454 void setDirty(box2i rect) nothrow @nogc 455 { 456 box2i translatedRect = rect.translate(_position.min); 457 assert(_position.contains(translatedRect)); 458 _context.dirtyList.addRect(translatedRect); 459 } 460 461 /// Returns: Parent element. `null` if detached or root element. 462 final UIElement parent() pure nothrow @nogc 463 { 464 return _parent; 465 } 466 467 /// Returns: Top-level parent. `null` if detached or root element. 468 final UIElement topLevelParent() pure nothrow @nogc 469 { 470 if (_parent is null) 471 return this; 472 else 473 return _parent.topLevelParent(); 474 } 475 476 final bool isMouseOver() pure const nothrow @nogc 477 { 478 return _mouseOver; 479 } 480 481 final bool isDragged() pure const nothrow @nogc 482 { 483 return _context.dragged is this; 484 } 485 486 final bool isFocused() pure const nothrow @nogc 487 { 488 return _context.focused is this; 489 } 490 491 /// Appends the Elements that should be drawn, in order. 492 /// You should empty it before calling this function. 493 /// Everything visible get into the draw list, but that doesn't mean they 494 /// will get drawn if they don't overlap with a dirty area. 495 final void getDrawList(ref Vec!UIElement list) nothrow @nogc 496 { 497 if (isVisible()) 498 { 499 list.pushBack(this); 500 foreach(child; _children[]) 501 child.getDrawList(list); 502 } 503 } 504 505 protected: 506 507 /// Draw method. This gives you 3 surfaces cropped by _position for drawing. 508 /// Note that you are not forced to draw all the surfaces at all, in which case the 509 /// below background`UIElement` will be displayed. 510 /// 511 /// `UIElement` are drawn by increasing z-order, or lexical order if lack thereof. 512 /// Those elements who have non-overlapping `_position` are drawn in parallel. 513 /// Hence you CAN'T draw outside `_position` and receive cropped surfaces. 514 /// `diffuseMap`, `depthMap` and `materialMap` are made to span _position exactly. 515 /// 516 /// IMPORTANT: For better efficiency, you SHALL NOT draw part outside `dirtyRects`. 517 /// This allows more fine-grained updates. 518 /// A `UIElement` that doesn't respect dirtyRects will have display problems if it overlaps with another `UIElement`. 519 void onDraw(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects) nothrow @nogc 520 { 521 // defaults to filling with a grey pattern 522 RGBA darkGrey = RGBA(100, 100, 100, 0); 523 RGBA lighterGrey = RGBA(150, 150, 150, 0); 524 525 foreach(dirtyRect; dirtyRects) 526 { 527 for (int y = dirtyRect.min.y; y < dirtyRect.max.y; ++y) 528 { 529 L16[] depthScan = depthMap.scanline(y); 530 RGBA[] diffuseScan = diffuseMap.scanline(y); 531 RGBA[] materialScan = materialMap.scanline(y); 532 for (int x = dirtyRect.min.x; x < dirtyRect.max.x; ++x) 533 { 534 diffuseScan.ptr[x] = ( (x >> 3) ^ (y >> 3) ) & 1 ? darkGrey : lighterGrey; 535 depthScan.ptr[x] = L16(defaultDepth); 536 materialScan.ptr[x] = RGBA(defaultRoughness, defaultMetalnessDielectric, defaultSpecular, defaultPhysical); 537 } 538 } 539 } 540 } 541 542 /// Called periodically for every `UIElement`. 543 /// Override this to create animations. 544 /// Using setDirty there allows to redraw an element continuously (like a meter or an animated object). 545 /// Warning: Summing `dt` will not lead to a time that increase like `time`. 546 /// `time` can go backwards if the window was reopen. 547 /// `time` is guaranteed to increase as fast as system time but is not synced to audio time. 548 void onAnimate(double dt, double time) nothrow @nogc 549 { 550 } 551 552 /// Parent element. 553 /// Following this chain gets to the root element. 554 UIElement _parent = null; 555 556 /// Position is the graphical extent of the element, or something larger. 557 /// An `UIElement` is not allowed though to draw further than its _position. 558 /// For efficiency it's best to keep `_position` as small as feasible. 559 box2i _position; 560 561 /// The list of children UI elements. 562 Vec!UIElement _children; 563 564 /// If _visible is false, neither the Element nor its children are drawn. 565 bool _visible = true; 566 567 /// Higher z-order = above other `UIElement`. 568 /// By default, every `UIElement` have the same z-order. 569 /// Because the sort is stable, tree traversal order is the default order (depth first). 570 int _zOrder = 0; 571 572 private: 573 574 /// Reference to owning context. 575 UIContext _context; 576 577 /// Flag: whether this UIElement has mouse over it or not. 578 bool _mouseOver = false; 579 580 /// Dirty rectangles buffer, cropped to _position. 581 Vec!box2i _localRectsBuf; 582 583 /// Sported children in Z-lexical-order (sorted by Z, or else increasing index in _children). 584 Vec!UIElement _zOrderedChildren; 585 586 // Sort children in ascending z-order 587 // Input: unsorted _children 588 // Output: sorted _zOrderedChildren 589 final void recomputeZOrderedChildren() 590 { 591 // Get a z-ordered list of childrens 592 _zOrderedChildren.clearContents(); 593 foreach(child; _children[]) 594 _zOrderedChildren.pushBack(child); 595 596 // This is a stable sort, so the order of children with same z-order still counts. 597 grailSort!UIElement(_zOrderedChildren[], 598 (a, b) nothrow @nogc 599 { 600 if (a.zOrder < b.zOrder) return 1; 601 else if (a.zOrder > b.zOrder) return -1; 602 else return 0; 603 }); 604 } 605 } 606 607 608