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 }