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 }