1 /**
2 On/Off switch.
3 
4 Copyright: Copyright Auburn Sounds 2015-2017.
5 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 Authors:   Guillaume Piolat
7 */
8 module dplug.pbrwidgets.onoffswitch;
9 
10 import std.math: exp, abs;
11 import dplug.core.math;
12 import dplug.gui.element;
13 import dplug.gui.bufferedelement;
14 import dplug.client.params;
15 
16 class UIOnOffSwitch : UIElement, IParameterListener
17 {
18 public:
19 nothrow:
20 @nogc:
21 
22     enum Orientation
23     {
24         vertical,
25         horizontal
26     }
27     @ScriptProperty RGBA diffuseOff = RGBA(230, 80, 43, 0);
28     @ScriptProperty RGBA diffuseOn = RGBA(230, 80, 43, 200);
29     @ScriptProperty RGBA material = RGBA(192, 10, 128, 255);
30     @ScriptProperty float animationTimeConstant = 10.0f;
31     @ScriptProperty ushort depthLow = 0;
32     @ScriptProperty ushort depthHigh = 30000;
33     @ScriptProperty ushort holeDepth = 0;
34     @ScriptProperty Orientation orientation = Orientation.vertical;
35     @ScriptProperty bool drawDepth = true;
36     @ScriptProperty bool drawDiffuse = true;
37     @ScriptProperty bool drawMaterial = true;
38     @ScriptProperty bool drawHole = true;     // if drawDepth && drawHole, draw Z hole
39     @ScriptProperty bool drawEmissive = true; // if drawEmissive && !drawDiffuse, draw just the emissive channel
40     
41 
42     /// Left and right border, in fraction of the widget's width.
43     /// Cannot be > 0.5f
44     @ScriptProperty float borderHorz = 0.1f;
45 
46     /// Top and bottom border, in fraction of the widget's width.*
47     /// Cannot be > 0.5f
48     @ScriptProperty float borderVert = 0.1f;
49 
50     /// Note: Can take a BoolParameter or IntegerParameter, in which 
51     /// case enabled means 1 and disabled means 0. 
52     /// So it also takes `EnumParameter`.
53     this(UIContext context, Parameter param)
54     {
55         super(context, flagAnimated | flagPBR);
56         _param = param;
57         _param.addListener(this);
58         _animation = 0.0f;
59     }
60 
61     ~this()
62     {
63         _param.removeListener(this);
64     }
65 
66     override void onAnimate(double dt, double time) nothrow @nogc
67     {
68         float target;
69         if (auto bp = cast(BoolParameter)_param)
70         {
71             target = bp.valueAtomic() ? 1 : 0;
72         }
73         else if (auto ip = cast(IntegerParameter)_param)
74         {
75             target = (ip.valueAtomic() != 0) ? 1 : 0;
76         }
77         else
78             assert(false);
79 
80         float newAnimation = lerp(_animation, target, 1.0 - exp(-dt * animationTimeConstant));
81 
82         if (abs(newAnimation - _animation) > 0.001f)
83         {
84             _animation = newAnimation;
85             setDirtyWhole();
86         }
87     }
88 
89     override void onDrawPBR(ImageRef!RGBA diffuseMap, ImageRef!L16 depthMap, ImageRef!RGBA materialMap, box2i[] dirtyRects) nothrow @nogc
90     {
91         // The switch is in a subrect
92         int width = _position.width;
93         int height = _position.height;
94 
95         box2i switchRect = box2i( cast(int)(0.5f + width * borderHorz),
96                                   cast(int)(0.5f + height * borderVert),
97                                   cast(int)(0.5f + width * (1 - borderHorz)),
98                                   cast(int)(0.5f + height * (1 - borderVert)) );
99 
100         ubyte red    = cast(ubyte)(lerp!float(diffuseOff.r, diffuseOn.r, _animation));
101         ubyte green  = cast(ubyte)(lerp!float(diffuseOff.g, diffuseOn.g, _animation));
102         ubyte blue   = cast(ubyte)(lerp!float(diffuseOff.b, diffuseOn.b, _animation));
103         int emissive = cast(ubyte)(lerp!float(diffuseOff.a, diffuseOn.a, _animation));
104 
105         if (isMouseOver || isDragged)
106             emissive += 40;
107 
108         if (emissive > 255)
109             emissive = 255;
110 
111         // Workaround issue https://issues.dlang.org/show_bug.cgi?id=23076
112         // Regular should not be inlined here.
113         static float lerpfloat(float a, float b, float t) pure nothrow @nogc
114         {
115             pragma(inline, false);
116             return t * b + (1 - t) * a;
117         }
118 
119         L16 depthA = L16(cast(short)(lerpfloat(depthHigh, depthLow, _animation)));
120         L16 depthB = L16(cast(short)(lerpfloat(depthLow, depthHigh, _animation)));
121 
122         RGBA diffuseColor = RGBA(red, green, blue, cast(ubyte)emissive);
123 
124         foreach(dirtyRect; dirtyRects)
125         {
126             auto cDepth = depthMap.cropImageRef(dirtyRect);
127 
128             // Write a plain color in the diffuse and material map.
129             box2i validRect = dirtyRect.intersection(switchRect);
130             if (!validRect.empty)
131             {
132                 ImageRef!RGBA cDiffuse = diffuseMap.cropImageRef(validRect);
133                 if (drawDiffuse)
134                 {
135                     cDiffuse.fillAll(diffuseColor);
136                 }
137                 else if (drawEmissive)
138                 {
139                     cDiffuse.fillRectAlpha(0, 0, cDiffuse.w, cDiffuse.h, diffuseColor.a);
140                 }
141                 if (drawMaterial)
142                     materialMap.cropImageRef(validRect).fillAll(material);
143             }
144 
145             // dig a hole
146             if (drawDepth)
147             {
148                 if (drawHole)
149                     cDepth.fillAll(L16(holeDepth));
150 
151                 box2i slopeRect = switchRect.translate(-dirtyRect.min.x, -dirtyRect.min.y);
152 
153                 if (orientation == Orientation.vertical)
154                     verticalSlope(cDepth, slopeRect, depthA, depthB);
155                 else
156                     horizontalSlope(cDepth, slopeRect, depthA, depthB);
157             }
158         }
159     }
160 
161     override Click onMouseClick(int x, int y, int button, bool isDoubleClick, MouseState mstate)
162     {
163         if (!_canBeDragged)
164         {
165             // inside gesture, refuse new clicks that could call
166             // excess beginParamEdit()/endParamEdit()
167             return Click.unhandled;
168         }
169 
170         // ALT + click => set it to default
171         if (mstate.altPressed) // reset on ALT + click
172         {
173             _param.beginParamEdit();
174 
175             if (auto bp = cast(BoolParameter)_param)
176             {
177                 bp.setFromGUI(bp.defaultValue());
178             }
179             else if (auto ip = cast(IntegerParameter)_param)
180             {
181                 ip.setFromGUI(ip.defaultValue()); // possibly not 0 and 1
182             }
183             else
184                 assert(false);
185         }
186         else
187         {
188             // Any click => invert
189             // Note: double-click doesn't reset to default, would be annoying
190             _param.beginParamEdit();
191             if (auto bp = cast(BoolParameter)_param)
192             {
193                 bp.setFromGUI(!bp.value());
194             }
195             else if (auto ip = cast(IntegerParameter)_param)
196             {
197                 // any => 0
198                 // 0   => 1
199                 bool value = ip.value() != 0;
200                 ip.setFromGUI(value ? 0 : 1);
201             }
202             else
203                 assert(false);
204         }
205         _canBeDragged = false;
206         return Click.startDrag;
207     }
208 
209     override void onMouseEnter()
210     {
211         _param.beginParamHover();
212         setDirtyWhole();
213     }
214 
215     override void onMouseExit()
216     {
217          _param.endParamHover();
218         setDirtyWhole();
219     }
220 
221     override void onStopDrag()
222     {
223         // End parameter edit at end of dragging, even if no parameter change happen,
224         // so that touch automation restore previous parameter value at the end of the mouse 
225         // gesture.
226         _param.endParamEdit();
227         _canBeDragged = true;
228     }
229 
230     override void onParameterChanged(Parameter sender) nothrow @nogc
231     {
232         setDirtyWhole();
233     }
234 
235     override void onBeginParameterEdit(Parameter sender)
236     {
237     }
238 
239     override void onEndParameterEdit(Parameter sender)
240     {
241     }
242 
243     override void onBeginParameterHover(Parameter sender)
244     {
245     }
246 
247     override void onEndParameterHover(Parameter sender)
248     {
249     }
250 
251 protected:
252 
253     /// The parameter this switch is linked with.
254     Parameter _param;
255 
256     /// To prevent multiple-clicks having an adverse effect on automation.
257     bool _canBeDragged = true;
258 
259 private:
260     float _animation;
261 }
262 
263 private:
264 void fillRectAlpha(bool CHECKED=true, V)(auto ref V v, int x1, int y1, int x2, int y2, ubyte alpha) nothrow @nogc
265 if (isWritableView!V && is(RGBA : ViewColor!V))
266 {
267     sort2(x1, x2);
268     sort2(y1, y2);
269     static if (CHECKED)
270     {
271         if (x1 >= v.w || y1 >= v.h || x2 <= 0 || y2 <= 0 || x1==x2 || y1==y2) return;
272         if (x1 <    0) x1 =   0;
273         if (y1 <    0) y1 =   0;
274         if (x2 >= v.w) x2 = v.w;
275         if (y2 >= v.h) y2 = v.h;
276     }
277 
278     foreach (y; y1..y2)
279     {
280         RGBA[] scan = v.scanline(y);
281         foreach (x; x1..x2)
282         {
283             scan[x].a = alpha;
284         }
285     }
286 }