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, _minValue = 0f, _maxValue = 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 value_) {
32             return _value = _offset = value_;
33         }
34 
35         /// Rounded value between the min and max values specified.
36         int ivalue() const {
37             return cast(int) lerp(_minValue, _maxValue, _value);
38         }
39         /// Ditto
40         int ivalue(int value_) {
41             return cast(int)(_value = _offset = rlerp(_minValue, _maxValue, value_));
42         }
43 
44         /// Value between the min and max values specified.
45         float fvalue() const {
46             return lerp(_minValue, _maxValue, _value);
47         }
48         /// Ditto
49         float fvalue(float value_) {
50             return _value = _offset = rlerp(_minValue, _maxValue, value_);
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 steps() const {
63             return (_step > 0f) ? cast(uint)(1f / _step) : 0u;
64         }
65         /// Ditto
66         uint steps(uint steps_) {
67             if (steps_ < 1u)
68                 _step = 0f;
69             else
70                 _step = 1f / steps_;
71             return steps_;
72         }
73 
74         /// Minimal value possible for the slider. \
75         /// Used by ivalue() and fvalue().
76         float minValue() const {
77             return _minValue;
78         }
79         /// Ditto
80         float minValue(float newMin) {
81             return _minValue = newMin;
82         }
83 
84         /// Maximal value possible for the slider. \
85         /// Used by ivalue() and fvalue().
86         float maxValue() const {
87             return _maxValue;
88         }
89         /// Ditto
90         float maxValue(float newMax) {
91             return _maxValue = 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             if (!isClicked)
108                 _offset = lerp(_offset, _value, deltaTime * 0.25f);
109             triggerCallback();
110         }
111     }
112 
113     override void onEvent(Event event) {
114         if (_step == 0f)
115             return;
116 
117         switch (event.type) with (Event.Type) {
118         case mouseWheel:
119             _offset -= event.scroll.delta.y * _step;
120             _offset = (_offset < -_step) ? -_step : ((_offset > 1f + _step) ? 1f + _step : _offset); //Clamp the value.
121             break;
122         case mouseUpdate:
123             if (!isClicked)
124                 break;
125             relocateSlider(event);
126             break;
127         case mouseDown:
128             relocateSlider(event);
129             break;
130         case mouseUp:
131             relocateSlider(event);
132             break;
133         default:
134             break;
135         }
136         if (isSelected)
137             relocateSlider(event);
138     }
139 
140     /// Process the slider position.
141     protected void relocateSlider(Event event) {
142         if (_step == 0f) {
143             _offset = 0f;
144             _value = 0f;
145             return;
146         }
147         const Vec2f direction = Vec2f.angled(_scrollAngle);
148         const Vec2f startPos = center - direction * 0.5f * _scrollLength;
149         const float coef = direction.y / direction.x;
150         const float b = startPos.y - (coef * startPos.x);
151 
152         const Vec2f closestPoint = Vec2f(
153                 (coef * event.mouse.position.y + event.mouse.position.x - coef * b) / (
154                 coef * coef + 1f),
155                 (coef * coef * event.mouse.position.y + coef * event.mouse.position.x + b) / (
156                     coef * coef + 1f));
157 
158         _offset = ((closestPoint.x - startPos.x) + (closestPoint.y - startPos.y)) / _scrollLength;
159         _offset = (_offset < 0f) ? 0f : ((_offset > 1f) ? 1f : _offset); //Clamp the value.
160     }
161 
162     /// Current coordinate of the slider.
163     protected Vec2f getSliderPosition() {
164         if (_step == 0f)
165             return center;
166         Vec2f direction = Vec2f.angled(_scrollAngle);
167         return center + direction * (_scrollLength * (_offset - 0.5f));
168     }
169 }
170 
171 /// Simple vertical scrollbar with basic rendering.
172 class VScrollbar : Slider {
173     /// Ctor
174     this() {
175         super(90f);
176     }
177 
178     override void draw() {
179         Vec2f sliderPosition = getSliderPosition();
180         float sliderLength = std.algorithm.max((_step > 0f ? _step
181                 : 1f) * _scrollLength, _minimalSliderSize);
182 
183         //Resize the slider to fit in the rail.
184         if (sliderPosition.y - sliderLength / 2f < center.y - _scrollLength / 2f) {
185             const float startPos = center.y - _scrollLength / 2f;
186             const float destination = sliderPosition.y + sliderLength / 2f;
187             sliderLength = destination - startPos;
188             sliderPosition.y = startPos + sliderLength / 2f;
189         }
190         else if (sliderPosition.y + sliderLength / 2f > center.y + _scrollLength / 2f) {
191             const float startPos = sliderPosition.y - sliderLength / 2f;
192             const float destination = center.y + _scrollLength / 2f;
193             sliderLength = destination - startPos;
194             sliderPosition.y = startPos + sliderLength / 2f;
195         }
196 
197         sliderPosition.y = clamp(sliderPosition.y, center.y - _scrollLength / 2f,
198                 center.y + _scrollLength / 2f);
199         if (sliderLength < 0f)
200             sliderLength = 0f;
201 
202         drawFilledRect(origin, size, Color.white * .25f);
203         const Vec2f sliderSize = Vec2f(size.x, sliderLength);
204         drawFilledRect(sliderPosition - sliderSize / 2f, sliderSize, Color.white);
205     }
206 
207     override void onSize() {
208         _scrollLength = size.y;
209     }
210 }
211 
212 /// Simple horizontal scrollbar with basic rendering.
213 class HScrollbar : Slider {
214     /// Ctor
215     this() {
216         super(0f);
217     }
218 
219     override void draw() {
220         Vec2f sliderPosition = getSliderPosition();
221         float sliderLength = std.algorithm.max((_step > 0f ? _step
222                 : 1f) * _scrollLength, _minimalSliderSize);
223 
224         //Resize the slider to fit in the rail.
225         if (sliderPosition.x - sliderLength / 2f < center.x - _scrollLength / 2f) {
226             const float startPos = center.x - _scrollLength / 2f;
227             const float destination = sliderPosition.x + sliderLength / 2f;
228             sliderLength = destination - startPos;
229             sliderPosition.x = startPos + sliderLength / 2f;
230         }
231         else if (sliderPosition.x + sliderLength / 2f > center.x + _scrollLength / 2f) {
232             const float startPos = sliderPosition.x - sliderLength / 2f;
233             const float destination = center.x + _scrollLength / 2f;
234             sliderLength = destination - startPos;
235             sliderPosition.x = startPos + sliderLength / 2f;
236         }
237 
238         sliderPosition.x = clamp(sliderPosition.x, center.x - _scrollLength / 2f,
239                 center.x + _scrollLength / 2f);
240         if (sliderLength < 0f)
241             sliderLength = 0f;
242 
243         drawFilledRect(origin, size, Color.white * .25f);
244         const Vec2f sliderSize = Vec2f(sliderLength, size.y);
245         drawFilledRect(sliderPosition - sliderSize / 2f, sliderSize, Color.white);
246     }
247 
248     override void onSize() {
249         _scrollLength = size.x;
250     }
251 }
252 
253 /// Simple vertical slider with basic rendering.
254 class VSlider : Slider {
255     /// Ctor
256     this() {
257         super(90f);
258     }
259 
260     override void draw() {
261         //Background
262         drawFilledRect(origin, size, Color.white * .25f);
263 
264         //Gauge
265         const float sliderHeight = clamp(getSliderPosition().y - origin.y, 0f, size.y);
266         const Vec2f sliderSize = Vec2f(size.x, sliderHeight);
267         drawFilledRect(origin + Vec2f(0f, size.y - sliderHeight), sliderSize, color);
268     }
269 
270     override void onSize() {
271         _scrollLength = size.y;
272     }
273 }
274 
275 /// Simple horizontal slider with basic rendering.
276 class HSlider : Slider {
277     /// Ctor
278     this() {
279         super(0f);
280     }
281 
282     override void draw() {
283         //Background
284         drawFilledRect(origin, size, Color.white * .25f);
285 
286         //Gauge
287         const float sliderWidth = clamp(getSliderPosition().x - origin.x, 0f, size.x);
288         const Vec2f sliderSize = Vec2f(sliderWidth, size.y);
289         drawFilledRect(origin, sliderSize, color);
290     }
291 
292     override void onSize() {
293         _scrollLength = size.x;
294     }
295 }