1 /**
2     Slider
3 
4     Copyright: (c) Enalye 2017
5     License: Zlib
6     Authors: Enalye
7 */
8 
9 module atelier.ui.slider;
10 
11 import std.math;
12 import std.algorithm.comparison;
13 import std.conv : to;
14 import atelier.core, atelier.render, atelier.common;
15 import atelier.ui.button, atelier.ui.gui_element;
16 
17 /// Base abstract class for any vertical or horizontal slider/scrollbar.
18 abstract class Slider : GuiElement {
19     protected {
20         float _value = 0f, _offset = 0f, _step = 1f, _min = 0f, _max = 1f,
21             _scrollLength = 1f, _minimalSliderSize = 25f, _scrollAngle = 0f;
22         bool _isGrabbed = false;
23     }
24 
25     @property {
26         /// Slider's value between 0 and 1.
27         float value01() const {
28             return _value;
29         }
30         /// Ditto
31         float value01(float newValue) {
32             return _value = _offset = newValue;
33         }
34 
35         /// Rounded value between the min and max values specified.
36         int ivalue() const {
37             return cast(int) lerp(_min, _max, _value);
38         }
39         /// Ditto
40         int ivalue(int newValue) {
41             return cast(int)(_value = _offset = rlerp(_min, _max, newValue));
42         }
43 
44         /// Value between the min and max values specified.
45         float fvalue() const {
46             return lerp(_min, _max, _value);
47         }
48         /// Ditto
49         float fvalue(float newValue) {
50             return _value = _offset = rlerp(_min, _max, newValue);
51         }
52 
53         /// Value (from 0 to 1) before being processed/clamped/etc. \
54         /// Useful for rendering, not for getting its value as data.
55         float offset() const {
56             return _offset;
57         }
58 
59         /// The number of steps of the slider. \
60         /// 1 = The slider jumps directly from start to finish. \
61         /// More = The slider has more intermediate values.
62         uint step() const {
63             return (_step > 0f) ? cast(uint)(1f / _step) : 0u;
64         }
65         /// Ditto
66         uint step(uint newStep) {
67             if (newStep < 1u)
68                 _step = 0f;
69             else
70                 _step = 1f / newStep;
71             return newStep;
72         }
73 
74         /// Minimal value possible for the slider. \
75         /// Used by ivalue() and fvalue().
76         float min() const {
77             return _min;
78         }
79         /// Ditto
80         float min(float newMin) {
81             return _min = newMin;
82         }
83 
84         /// Maximal value possible for the slider. \
85         /// Used by ivalue() and fvalue().
86         float max() const {
87             return _max;
88         }
89         /// Ditto
90         float max(float newMax) {
91             return _max = newMax;
92         }
93     }
94 
95     /// Sets the angle of the slider. \
96     /// 90 = Vertical. \
97     /// 0 = Horizontal.
98     this(float scrollAngle) {
99         _scrollAngle = scrollAngle;
100     }
101 
102     override void update(float deltaTime) {
103         if (!isSelected) {
104             _value = (_offset < 0f) ? 0f : ((_offset > 1f) ? 1f : _offset); //Clamp the value.
105             if (_step > 0f)
106                 _value = std.math.round(_value / _step) * _step; //Snap the value.
107             _offset = lerp(_offset, _value, deltaTime * 0.25f);
108             triggerCallback();
109         }
110     }
111 
112     override void onEvent(Event event) {
113         if (_step == 0f)
114             return;
115 
116         switch (event.type) with (Event.Type) {
117         case mouseWheel:
118             _offset -= event.scroll.delta.y * _step;
119             _offset = (_offset < -_step) ? -_step : ((_offset > 1f + _step) ? 1f + _step : _offset); //Clamp the value.
120             break;
121         case mouseUpdate:
122             if (!isClicked)
123                 break;
124             relocateSlider(event);
125             break;
126         case mouseDown:
127             relocateSlider(event);
128             break;
129         case mouseUp:
130             relocateSlider(event);
131             break;
132         default:
133             break;
134         }
135         if (isSelected)
136             relocateSlider(event);
137     }
138 
139     /// Process the slider position.
140     protected void relocateSlider(Event event) {
141         if (_step == 0f) {
142             _offset = 0f;
143             _value = 0f;
144             return;
145         }
146         const Vec2f direction = Vec2f.angled(_scrollAngle);
147         const Vec2f startPos = center - direction * 0.5f * _scrollLength;
148         const float coef = direction.y / direction.x;
149         const float b = startPos.y - (coef * startPos.x);
150 
151         const Vec2f closestPoint = Vec2f(
152                 (coef * event.mouse.position.y + event.mouse.position.x - coef * b) / (
153                 coef * coef + 1f),
154                 (coef * coef * event.mouse.position.y + coef * event.mouse.position.x + b) / (
155                     coef * coef + 1f));
156 
157         _offset = ((closestPoint.x - startPos.x) + (closestPoint.y - startPos.y)) / _scrollLength;
158         _offset = (_offset < 0f) ? 0f : ((_offset > 1f) ? 1f : _offset); //Clamp the value.
159     }
160 
161     /// Current coordinate of the slider.
162     protected Vec2f getSliderPosition() {
163         if (_step == 0f)
164             return center;
165         Vec2f direction = Vec2f.angled(_scrollAngle);
166         return center + direction * (_scrollLength * (_offset - 0.5f));
167     }
168 }
169 
170 /// Simple vertical scrollbar with basic rendering.
171 class VScrollbar : Slider {
172     /// Ctor
173     this() {
174         super(90f);
175     }
176 
177     override void draw() {
178         Vec2f sliderPosition = getSliderPosition();
179         float sliderLength = std.algorithm.max((_step > 0f ? _step
180                 : 1f) * _scrollLength, _minimalSliderSize);
181 
182         //Resize the slider to fit in the rail.
183         if (sliderPosition.y - sliderLength / 2f < center.y - _scrollLength / 2f) {
184             const float startPos = center.y - _scrollLength / 2f;
185             const float destination = sliderPosition.y + sliderLength / 2f;
186             sliderLength = destination - startPos;
187             sliderPosition.y = startPos + sliderLength / 2f;
188         }
189         else if (sliderPosition.y + sliderLength / 2f > center.y + _scrollLength / 2f) {
190             const float startPos = sliderPosition.y - sliderLength / 2f;
191             const float destination = center.y + _scrollLength / 2f;
192             sliderLength = destination - startPos;
193             sliderPosition.y = startPos + sliderLength / 2f;
194         }
195 
196         sliderPosition.y = clamp(sliderPosition.y, center.y - _scrollLength / 2f,
197                 center.y + _scrollLength / 2f);
198         if (sliderLength < 0f)
199             sliderLength = 0f;
200 
201         drawFilledRect(origin, size, Color.white * .25f);
202         const Vec2f sliderSize = Vec2f(size.x, sliderLength);
203         drawFilledRect(sliderPosition - sliderSize / 2f, sliderSize, Color.white);
204     }
205 
206     override void onSize() {
207         _scrollLength = size.y;
208     }
209 }
210 
211 /// Simple horizontal scrollbar with basic rendering.
212 class HScrollbar : Slider {
213     /// Ctor
214     this() {
215         super(0f);
216     }
217 
218     override void draw() {
219         Vec2f sliderPosition = getSliderPosition();
220         float sliderLength = std.algorithm.max((_step > 0f ? _step
221                 : 1f) * _scrollLength, _minimalSliderSize);
222 
223         //Resize the slider to fit in the rail.
224         if (sliderPosition.x - sliderLength / 2f < center.x - _scrollLength / 2f) {
225             const float startPos = center.x - _scrollLength / 2f;
226             const float destination = sliderPosition.x + sliderLength / 2f;
227             sliderLength = destination - startPos;
228             sliderPosition.x = startPos + sliderLength / 2f;
229         }
230         else if (sliderPosition.x + sliderLength / 2f > center.x + _scrollLength / 2f) {
231             const float startPos = sliderPosition.x - sliderLength / 2f;
232             const float destination = center.x + _scrollLength / 2f;
233             sliderLength = destination - startPos;
234             sliderPosition.x = startPos + sliderLength / 2f;
235         }
236 
237         sliderPosition.x = clamp(sliderPosition.x, center.x - _scrollLength / 2f,
238                 center.x + _scrollLength / 2f);
239         if (sliderLength < 0f)
240             sliderLength = 0f;
241 
242         drawFilledRect(origin, size, Color.white * .25f);
243         const Vec2f sliderSize = Vec2f(sliderLength, size.y);
244         drawFilledRect(sliderPosition - sliderSize / 2f, sliderSize, Color.white);
245     }
246 
247     override void onSize() {
248         _scrollLength = size.x;
249     }
250 }
251 
252 /// Simple vertical slider with basic rendering.
253 class VSlider : Slider {
254     /// Ctor
255     this() {
256         super(90f);
257     }
258 
259     override void draw() {
260         //Background
261         drawFilledRect(origin, size, Color.white * .25f);
262 
263         //Gauge
264         const float sliderHeight = clamp(getSliderPosition().y - origin.y, 0f, size.y);
265         const Vec2f sliderSize = Vec2f(size.x, sliderHeight);
266         drawFilledRect(origin + Vec2f(0f, size.y - sliderHeight), sliderSize, color);
267     }
268 
269     override void onSize() {
270         _scrollLength = size.y;
271     }
272 }
273 
274 /// Simple horizontal slider with basic rendering.
275 class HSlider : Slider {
276     /// Ctor
277     this() {
278         super(0f);
279     }
280 
281     override void draw() {
282         //Background
283         drawFilledRect(origin, size, Color.white * .25f);
284 
285         //Gauge
286         const float sliderWidth = clamp(getSliderPosition().x - origin.x, 0f, size.x);
287         const Vec2f sliderSize = Vec2f(sliderWidth, size.y);
288         drawFilledRect(origin, sliderSize, color);
289     }
290 
291     override void onSize() {
292         _scrollLength = size.x;
293     }
294 }