1 module fluid.slider;
2
3 import std.range;
4
5 import fluid.node;
6 import fluid.utils;
7 import fluid.input;
8 import fluid.style;
9 import fluid.backend;
10 import fluid.structs;
11
12 import fluid.io.hover;
13 import fluid.io.canvas;
14
15 @safe:
16
17
18 ///
19 alias slider(T) = simpleConstructor!(Slider!T);
20
21 /// ditto
22 class Slider(T) : AbstractSlider {
23
24 mixin enableInputActions;
25
26 public {
27
28 /// Value range of the slider.
29 SliderRange!T range;
30
31 }
32
33 /// Create the slider using the given range as the set of possible values/steps.
34 this(R)(R range, size_t index, void delegate() @safe changed = null)
35 if (is(ElementType!R == T)) {
36
37 this(params, range, changed);
38 this.index = index;
39
40 }
41
42 /// ditto
43 this(R)(R range, void delegate() @safe changed = null)
44 if (is(ElementType!R == T)) {
45
46 // TODO special-case empty sliders instead?
47 assert(!range.empty, "Slider range must not be empty.");
48
49 this.range = new SliderRangeImpl!R(range);
50 this.changed = changed;
51
52 }
53
54 override bool isHovered() const {
55
56 return super.isHovered || this is tree.hover;
57
58 }
59
60 override void drawImpl(Rectangle outer, Rectangle inner) {
61
62 super.drawImpl(outer, inner);
63
64 }
65
66 override size_t length() {
67
68 return range.length;
69
70 }
71
72 T value() {
73
74 return range[index];
75
76 }
77
78 }
79
80 ///
81 unittest {
82
83 // To create a slider, pass it a range
84 slider!int(iota(0, 10)); // slider from 0 to 9
85 slider!int(iota(0, 11, 2)); // 0, 2, 4, 6, 8, 10
86
87 // Use any value and any random-access range
88 slider!string(["A", "B", "C"]);
89
90 }
91
92 abstract class AbstractSlider : InputNode!Node {
93
94 enum railWidth = 4;
95 enum minStepDistance = 10;
96
97 CanvasIO canvasIO;
98 HoverIO hoverIO;
99
100 public {
101
102 /// Handle of the slider.
103 SliderHandle handle;
104
105 /// Index/current position of the slider.
106 size_t index;
107
108 }
109
110 protected {
111
112 /// Position of the first step hitbox on the X axis.
113 float firstStepX;
114
115 /// Distance between each step
116 float stepDistance;
117
118 }
119
120 private {
121
122 bool _isPressed;
123
124 }
125
126 this() {
127
128 alias sliderHandle = simpleConstructor!SliderHandle;
129
130 this.handle = sliderHandle();
131
132 }
133
134 bool isPressed() const {
135
136 return _isPressed;
137
138 }
139
140 override void resizeImpl(Vector2 space) {
141
142 use(canvasIO);
143 use(hoverIO);
144
145 resizeChild(handle, space);
146 minSize = handle.minSize;
147
148 }
149
150 override void drawImpl(Rectangle outer, Rectangle inner) {
151
152 auto style = pickStyle();
153
154 const rail = Rectangle(
155 outer.x, center(outer).y - railWidth/2,
156 outer.width, railWidth
157 );
158
159 // Check if the slider is pressed
160 _isPressed = checkIsPressed();
161
162 // Draw the rail
163 style.drawBackground(io, canvasIO, rail);
164
165 const availableWidth = rail.width - handle.size.x;
166 const handleOffset = availableWidth * index / (length - 1f);
167 const handleRect = Rectangle(
168 rail.x + handleOffset, center(rail).y - handle.size.y/2,
169 handle.size.x, handle.size.y,
170 );
171
172 // Draw steps; Only draw beginning and end if there's too many
173 const stepCount = availableWidth / length >= minStepDistance
174 ? length
175 : 2;
176 const visualStepDistance = availableWidth / (stepCount - 1f);
177
178 stepDistance = availableWidth / (length - 1f);
179 firstStepX = rail.x + handle.size.x / 2;
180
181 foreach (step; 0 .. stepCount) {
182
183 const start = Vector2(firstStepX + visualStepDistance * step, end(rail).y);
184 const end = Vector2(start.x, end(outer).y);
185
186 style.drawLine(io, canvasIO, start, end);
187
188 }
189
190 // Draw the handle
191 drawChild(handle, handleRect);
192
193 }
194
195 @(FluidInputAction.press, WhileHeld)
196 protected void press(HoverPointer pointer) {
197
198 // Get mouse position relative to the first step
199 const offset = pointer.position.x - firstStepX + stepDistance/2;
200
201 // Get step based on X axis position
202 const step = cast(size_t) (offset / stepDistance);
203
204 // Validate the value
205 if (step >= length) return;
206
207 // Set the index
208 if (index != step) {
209
210 index = step;
211 if (changed) changed();
212
213 }
214
215 }
216
217 @(FluidInputAction.press, WhileDown)
218 protected void press() {
219
220 // The new I/O system will call the other overload.
221 // Call it as a polyfill for the old system.
222 if (!hoverIO) {
223 HoverPointer pointer;
224 pointer.position = io.mousePosition;
225 press(pointer);
226 }
227
228 }
229
230 @(FluidInputAction.scrollLeft)
231 void decrement() {
232
233 if (index > 0) index--;
234
235 }
236
237 @(FluidInputAction.scrollRight)
238 void increment() {
239
240 if (index + 1 < length) index++;
241
242 }
243
244 /// Length of the range.
245 abstract size_t length();
246
247 }
248
249 interface SliderRange(Element) {
250
251 alias Length = size_t;
252
253 Element front();
254 void popFront();
255 Length length();
256 Element opIndex(Length length);
257
258 }
259
260 class SliderRangeImpl(T) : SliderRange!(ElementType!T) {
261
262 static assert(isRandomAccessRange!T);
263 static assert(hasLength!T);
264
265 T range;
266
267 this(T range) {
268
269 this.range = range;
270
271 }
272
273 ElementType!T front() {
274
275 return range.front;
276
277 }
278
279 void popFront() {
280
281 range.popFront;
282
283 }
284
285 Length length() {
286
287 return range.length;
288
289 }
290
291 ElementType!T opIndex(Length length) {
292
293 return range[length];
294
295 }
296
297 }
298
299 /// Defines the handle of a slider.
300 class SliderHandle : Node {
301
302 CanvasIO canvasIO;
303
304 public {
305
306 Vector2 size = Vector2(16, 20);
307
308 }
309
310 this() {
311
312 ignoreMouse = true;
313
314 }
315
316 override void resizeImpl(Vector2 space) {
317
318 use(canvasIO);
319 minSize = size;
320
321 }
322
323 override void drawImpl(Rectangle outer, Rectangle inner) {
324
325 style.drawBackground(io, canvasIO, outer);
326
327 }
328
329 }