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