1 module nanogui.slider;
2 /*
3     nanogui.slider.d -- Fractional slider widget with mouse control
4     NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>.
5     The widget drawing code is based on the NanoVG demo application
6     by Mikko Mononen.
7     All rights reserved. Use of this source code is governed by a
8     BSD-style license that can be found in the LICENSE.txt file.
9 */
10 
11 import std.algorithm : min, max;
12 
13 import arsd.nanovega;
14 import nanogui.widget : Widget;
15 import nanogui.common : Vector2i, MouseButton, Color, boxGradient, 
16 	fillColor, Vector2f, radialGradient, linearGradient;
17 
18 /// Fractional slider widget with mouse control
19 class Slider(T) : Widget {
20 	import std.typecons : tuple, Tuple;
21 	import std.datetime : dur, Duration;
22 public:
23 
24 	import std.traits : isIntegral, isFloatingPoint;
25 
26 	static assert(isIntegral!T || isFloatingPoint!T || is(T == Duration));
27 
28 	this(Widget parent)
29 	{
30 		super(parent);
31 		static if (is(T == Duration))
32 		{
33 			mValue = dur!"seconds"(0);
34 			mRange = tuple(dur!"seconds"(0), dur!"seconds"(100));
35 		}
36 		else
37 		{
38 			mValue = cast(T) 0;
39 			mRange = tuple(0, 100);
40 		}
41 
42 		highlightedRange = tuple(mRange[0], mValue);
43 		mHighlightColor = Color(255, 80, 80, 70);
44 		mPreferredWidth = 70;
45 	}
46 
47 	final T value() const { return mValue; }
48 	final void  value(T value)
49 	{
50 		if (value < mRange[0])
51 			return;
52 		if (value > mRange[1])
53 			return;
54 		mValue = value;
55 		if (mCallback)
56 			mCallback(mValue);
57 	}
58 
59 	final ref const(Color) highlightColor() const { return mHighlightColor; }
60 	final void highlightColor(ref const(Color) highlightColor) { mHighlightColor = highlightColor; }
61 
62 	final Tuple!(T, T) range() const { return mRange; }
63 	final void range(T lo, T hi)
64 	{
65 		import std.exception : enforce;
66 		enforce(lo < hi, "Low boundary should be less than right boundary");
67 		mRange = tuple(lo, hi);
68 	}
69 
70 	final void range(Tuple!(T, T) range) { mRange = range; }
71 
72 	final Tuple!(T, T) highlightedRange() const
73 	{
74 		static if(is(T == Duration))
75 		{
76 			const total = (mRange[1] - mRange[0]).total!"hnsecs";
77 			T l = dur!"hnsecs"(cast(long)(mHighlightedRange[0]*total)) + mRange[0];
78 			T h = dur!"hnsecs"(cast(long)(mHighlightedRange[1]*total)) + mRange[0];
79 		}
80 		else
81 		{
82 			const total = (mRange[1] - mRange[0]);
83 			T l = cast(T) (mHighlightedRange[0]*total + mRange[0]);
84 			T h = cast(T) (mHighlightedRange[1]*total + mRange[0]);
85 		}
86 		return tuple(l, h);
87 	}
88 
89 	final void highlightedRange(Tuple!(T, T) highlightedRange)
90 	{
91 		static if(is(T == Duration))
92 		{
93 			const total = (mRange[1] - mRange[0]).total!"hnsecs";
94 			mHighlightedRange[0] = (highlightedRange[0] - mRange[0]).total!"hnsecs"/cast(double) total;
95 			mHighlightedRange[1] = (highlightedRange[1] - mRange[0]).total!"hnsecs"/cast(double) total;
96 		}
97 		else
98 		{
99 			const total = (mRange[1] - mRange[0]);
100 			mHighlightedRange[0] = (highlightedRange[0] - mRange[0])/cast(double) total;
101 			mHighlightedRange[1] = (highlightedRange[1] - mRange[0])/cast(double) total;
102 		}
103 	}
104 
105 	final bool delegate(T) callback() const { return mCallback; }
106 	final void callback(bool delegate(T) callback) { mCallback = callback; }
107 
108 	final bool delegate(T) finalCallback() const { return mCallback; }
109 	final void finalCallback(bool delegate(T) callback) { mCallback = callback; }
110 
111 	final void preferredWidth(int width)
112 	{
113 		mPreferredWidth = width;
114 	}
115 
116 	override Vector2i preferredSize(NanoContext ctx) const
117 	{
118 		return Vector2i(mPreferredWidth, 16);
119 	}
120 
121 	private auto calculateNewValue(const Vector2i p)
122 	{
123 		const double kr = cast(int) (mSize.y * 0.4f);
124 		const double kshadow = 3;
125 		const double startX = kr + kshadow + mPos.x - 1;
126 		const double widthX = mSize.x - 2 * (kr + kshadow);
127 
128 		static if (is(T == Duration))
129 		{
130 			import std.conv : to;
131 
132 			// used to save precision during converting from floating to int types
133 			enum factor = 2^^13;
134 			const v1 = factor*(p.x - startX) / cast(double) widthX;
135 			const v2 = to!long(v1);
136 			const v3 = (mRange[1] - mRange[0])*v2/factor + mRange[0];
137 			mValue = min(max(v3, mRange[0]), mRange[1]);
138 		}
139 		else
140 		{
141 			double v = (p.x - startX) / cast(double) widthX;
142 			v = v * (mRange[1] - mRange[0]) + mRange[0];
143 			mValue = cast(T) min(max(v, mRange[0]), mRange[1]);
144 		}
145 	}
146 
147 	override bool mouseDragEvent(const Vector2i p, const Vector2i rel, MouseButton button, int modifiers)
148 	{
149 		if (!mEnabled)
150 			return false;
151 
152 		calculateNewValue(p);
153 
154 		if (mCallback)
155 			mCallback(mValue);
156 		return true;
157 	}
158 
159 	override bool mouseButtonEvent(const Vector2i p, MouseButton button, bool down, int modifiers)
160 	{
161 		if (!mEnabled)
162 			return false;
163 
164 		calculateNewValue(p);
165 
166 		if (mCallback)
167 			mCallback(mValue);
168 		if (mFinalCallback && !down)
169 			mFinalCallback(mValue);
170 		return true;
171 	}
172 
173 	override void draw(NanoContext ctx)
174 	{
175 		Vector2f center = cast(Vector2f) mPos + 0.5f * cast(Vector2f) mSize;
176 		float kr = cast(int) (mSize.y * 0.4f);
177 		float kshadow = 3;
178 
179 		float startX = kr + kshadow + mPos.x;
180 		float widthX = mSize.x - 2*(kr+kshadow);
181 
182 		static if (is(T == Duration))
183 		{
184 			auto knobPos = Vector2f(startX + (mValue - mRange[0]).total!"hnsecs" /
185 					cast(double)(mRange[1] - mRange[0]).total!"hnsecs" * widthX,
186 					center.y + 0.5f);
187 		}
188 		else
189 		{
190 			auto knobPos = Vector2f(startX + (mValue - mRange[0]) /
191 					cast(double)(mRange[1] - mRange[0]) * widthX,
192 					center.y + 0.5f);
193 		}
194 
195 		NVGPaint bg = ctx.boxGradient(
196 			startX, center.y - 3 + 1, widthX, 6, 3, 3,
197 			Color(0, 0, 0, mEnabled ? 32 : 10), Color(0, 0, 0, mEnabled ? 128 : 210));
198 
199 		ctx.beginPath;
200 		ctx.roundedRect(startX, center.y - 3 + 1, widthX, 6, 2);
201 		ctx.fillPaint(bg);
202 		ctx.fill;
203 
204 		if (mHighlightedRange[1] != mHighlightedRange[0]) {
205 			ctx.beginPath;
206 			ctx.roundedRect(startX + mHighlightedRange[0] * mSize.x,
207 				center.y - kshadow + 1,
208 				widthX * (mHighlightedRange[1] - mHighlightedRange[0]),
209 				kshadow * 2, 2);
210 			ctx.fillColor(mHighlightColor);
211 			ctx.fill;
212 		}
213 
214 		NVGPaint knobShadow =
215 			ctx.radialGradient(knobPos.x, knobPos.y, kr - kshadow,
216 							kr + kshadow, Color(0, 0, 0, 64), mTheme.mTransparent);
217 
218 		ctx.beginPath;
219 		ctx.rect(knobPos.x - kr - 5, knobPos.y - kr - 5, kr * 2 + 10,
220 				kr * 2 + 10 + kshadow);
221 		ctx.circle(knobPos.x, knobPos.y, kr);
222 		ctx.pathWinding(NVGSolidity.Hole);
223 		ctx.fillPaint(knobShadow);
224 		ctx.fill;
225 
226 		NVGPaint knob = ctx.linearGradient(
227 			mPos.x, center.y - kr, mPos.x, center.y + kr,
228 			mTheme.mBorderLight, mTheme.mBorderMedium);
229 		NVGPaint knobReverse = ctx.linearGradient(
230 			mPos.x, center.y - kr, mPos.x, center.y + kr,
231 			mTheme.mBorderMedium,
232 			mTheme.mBorderLight);
233 
234 		ctx.beginPath;
235 		ctx.circle(knobPos.x, knobPos.y, kr);
236 		with (mTheme.mBorderDark)
237 			ctx.strokeColor(nvgRGBA(cast(int)(r/255), cast(int)(g/255), cast(int)(b/255), cast(int)(a/255)));
238 		ctx.fillPaint(knob);
239 		ctx.stroke;
240 		ctx.fill;
241 		ctx.beginPath;
242 		ctx.circle(knobPos.x, knobPos.y, kr/2);
243 		ctx.fillColor(Color(150, 150, 150, mEnabled ? 255 : 100));
244 		ctx.strokePaint(knobReverse);
245 		ctx.stroke;
246 		ctx.fill;
247 	}
248 	// void save(Serializer &s) const;
249 	// bool load(Serializer &s);
250 
251 protected:
252 	T mValue;
253 	bool delegate(T) mCallback;
254 	bool delegate(T) mFinalCallback;
255 	Tuple!(T, T) mRange;
256 	Tuple!(float, float) mHighlightedRange;
257 	Color mHighlightColor;
258 	int mPreferredWidth;
259 }
260 
261 // void Slider::save(Serializer &s) const {
262 //     Widget::save(s);
263 //     s.set("value", mValue);
264 //     s.set("range", mRange);
265 //     s.set("highlightedRange", mHighlightedRange);
266 //     s.set("highlightColor", mHighlightColor);
267 // }
268 
269 // bool Slider::load(Serializer &s) {
270 //     if (!Widget::load(s)) return false;
271 //     if (!s.get("value", mValue)) return false;
272 //     if (!s.get("range", mRange)) return false;
273 //     if (!s.get("highlightedRange", mHighlightedRange)) return false;
274 //     if (!s.get("highlightColor", mHighlightColor)) return false;
275 //     return true;
276 // }