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