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 }