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