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