1 module fluid.drag_slot; 2 3 import std.array; 4 import std.range; 5 6 import fluid.tree; 7 import fluid.node; 8 import fluid.slot; 9 import fluid.input; 10 import fluid.utils; 11 import fluid.style; 12 import fluid.backend; 13 import fluid.structs; 14 15 import fluid.io.hover; 16 import fluid.io.canvas; 17 import fluid.io.overlay; 18 19 import fluid.future.context; 20 21 @safe: 22 23 /// A drag slot is a node slot providing drag & drop functionality. 24 alias dragSlot = simpleConstructor!DragSlot; 25 26 /// ditto 27 class DragSlot : NodeSlot!Node, FluidHoverable, Hoverable { 28 29 mixin makeHoverable; 30 mixin FluidHoverable.enableInputActions; 31 mixin Hoverable.enableInputActions; 32 33 HoverIO hoverIO; 34 OverlayIO overlayIO; 35 36 public { 37 38 DragHandle handle; 39 40 /// Current drag action, if applicable. 41 DragAction dragAction; 42 43 /// If used with `OverlayIO`, this node wraps the drag slot to provide the overlay. 44 DragSlotOverlay overlay; 45 46 } 47 48 private { 49 50 bool _drawDragged; 51 52 /// Size used while the slot is being dragged. 53 Vector2 _size; 54 55 /// Last position when drawing statically (not dragging). 56 Vector2 _staticPosition; 57 58 } 59 60 /// Create a new drag slot and place a node inside of it. 61 this(Node node = null) { 62 63 super(node); 64 this.handle = dragHandle(.layout!"fill"); 65 this.overlay = new DragSlotOverlay(this); 66 67 } 68 69 override bool blocksInput() const { 70 return isDisabled || isDisabledInherited; 71 } 72 73 /// If true, this node is currently being dragged. 74 bool isDragged() const { 75 76 return dragAction !is null; 77 78 } 79 80 /// Drag the node. 81 @(FluidInputAction.press, .WhileDown) 82 void drag(HoverPointer pointer) 83 in (tree) 84 do { 85 86 // Ignore if already dragging 87 if (dragAction) { 88 dragAction._stopDragging = false; 89 dragAction.pointerPosition = pointer.position; 90 } 91 92 // Queue the drag action 93 else { 94 dragAction = new DragAction(this, pointer.position); 95 if (overlayIO) { 96 overlayIO.addOverlay(overlay, OverlayIO.types.draggable); 97 } 98 if (hoverIO) { 99 auto hover = cast(Node) hoverIO; 100 hover.startAction(dragAction); 101 } 102 else { 103 tree.queueAction(dragAction); 104 } 105 updateSize(); 106 } 107 108 } 109 110 /// Drag the node. 111 @(FluidInputAction.press, .WhileDown) 112 void drag() 113 in (tree) 114 do { 115 116 // Polyfill for old backend-based I/O 117 if (!hoverIO) { 118 HoverPointer pointer; 119 pointer.position = io.mousePosition; 120 drag(pointer); 121 } 122 123 } 124 125 private Rectangle dragRectangle(Vector2 offset) const nothrow { 126 const position = _staticPosition + offset; 127 return Rectangle(position.tupleof, _size.tupleof); 128 } 129 130 private void drawDragged(Node parent, Rectangle rect) { 131 132 _drawDragged = true; 133 minSize = _size; 134 scope (exit) _drawDragged = false; 135 scope (exit) minSize = Vector2(0, 0); 136 137 parent.drawChild(this, rect); 138 139 } 140 141 alias isHidden = typeof(super).isHidden; 142 143 @property 144 override bool isHidden() const scope { 145 146 // Don't hide from the draw action 147 if (_drawDragged) 148 return super.isHidden; 149 150 // Hide the node from its parent if it's dragged 151 else return super.isHidden || isDragged; 152 153 } 154 155 override void resizeImpl(Vector2 available) { 156 157 use(hoverIO); 158 use(overlayIO); 159 160 // Resize the slot 161 super.resizeImpl(available); 162 163 // Resize the handle 164 resizeChild(handle, available); 165 166 // Add space for the handle 167 if (!handle.isHidden) { 168 169 minSize.y += handle.minSize.y + style.gap.sideY; 170 171 if (handle.minSize.x > minSize.x) { 172 minSize.x = handle.minSize.x; 173 } 174 175 } 176 177 } 178 179 private void resizeInternal(Node parent, Vector2 space) { 180 181 _drawDragged = true; 182 scope (exit) _drawDragged = false; 183 184 parent.resizeChild(this, space); 185 186 // Save the size 187 _size = minSize; 188 189 } 190 191 override void drawImpl(Rectangle outer, Rectangle inner) { 192 193 const handleWidth = handle.minSize.y; 194 195 auto style = pickStyle; 196 auto handleRect = inner; 197 auto valueRect = inner; 198 199 // Save position 200 if (!_drawDragged) 201 _staticPosition = start(outer); 202 203 // Split the inner rectangle to fit the handle 204 handleRect.h = handleWidth; 205 if (!handle.isHidden) { 206 valueRect.y += handleWidth + style.gap.sideY; 207 valueRect.h -= handleWidth + style.gap.sideY; 208 } 209 210 // Disable the children while dragging 211 const disable = _drawDragged && !tree.isBranchDisabled; 212 213 if (disable) tree.isBranchDisabled = true; 214 215 // Draw the value 216 super.drawImpl(outer, valueRect); 217 218 if (disable) tree.isBranchDisabled = false; 219 220 // Draw the handle 221 drawChild(handle, handleRect); 222 223 } 224 225 protected override bool hoveredImpl(Rectangle rect, Vector2 position) { 226 227 return Node.hoveredImpl(rect, position); 228 229 } 230 231 override bool isHovered() const { 232 233 return this is tree.hover || super.isHovered(); 234 235 } 236 237 void mouseImpl() { 238 239 } 240 241 bool hoverImpl(HoverPointer) { 242 return false; 243 } 244 245 alias opEquals = typeof(super).opEquals; 246 247 override bool opEquals(const Object other) const { 248 return super.opEquals(other); 249 } 250 251 } 252 253 /// Wraps the `DragSlot` while it is being dragged. 254 /// 255 /// This is used to detect when `DragSlot` is drawn as an overlay or not. The `DragSlotOverlay` 256 /// is passed to `OverlayIO`, so it is known that if drawn, `DragSlotOverlay` functions 257 /// as an overlay. 258 /// 259 /// **`DragSlotOverlay` does not offer a stable interface.** It may only be a temporary solution 260 /// for the detection problem, before a more general option is added for `OverlayIO`. 261 class DragSlotOverlay : Node, Overlayable { 262 263 DragSlot next; 264 265 this(DragSlot next = null) { 266 this.next = next; 267 } 268 269 override void resizeImpl(Vector2 space) { 270 next.resizeInternal(this, space); 271 minSize = next.minSize; 272 } 273 274 override void drawImpl(Rectangle, Rectangle inner) { 275 next.drawDragged(this, inner); 276 } 277 278 override Rectangle getAnchor(Rectangle) const nothrow { 279 280 // backwards compatibility 281 import std.exception : assumeWontThrow; 282 283 if (next.dragAction) { 284 const position = next._staticPosition + next.dragAction.offset.assumeWontThrow; 285 return Rectangle(position.tupleof, 0, 0); 286 } 287 288 // Not dragged, no valid anchor 289 else return Rectangle.init; 290 291 } 292 293 alias opEquals = typeof(super).opEquals; 294 295 override bool opEquals(const Object other) const { 296 return super.opEquals(other); 297 } 298 299 } 300 301 /// Draggable handle. 302 alias dragHandle = simpleConstructor!DragHandle; 303 304 /// ditto 305 class DragHandle : Node { 306 307 CanvasIO canvasIO; 308 309 /// Additional features available for drag handle styling 310 static class Extra : typeof(super).Extra { 311 312 /// Width of the draggable bar 313 float width; 314 315 this(float width) { 316 317 this.width = width; 318 319 } 320 321 } 322 323 /// Get the width of the bar. 324 float width() const { 325 326 const extra = cast(const Extra) style.extra; 327 328 if (extra) 329 return extra.width; 330 else 331 return 0; 332 333 } 334 335 override bool hoveredImpl(Rectangle, Vector2) { 336 337 return false; 338 339 } 340 341 override void resizeImpl(Vector2 available) { 342 343 use(canvasIO); 344 minSize = Vector2(width * 2, width); 345 346 } 347 348 override void drawImpl(Rectangle outer, Rectangle inner) { 349 350 const width = this.width; 351 352 const radius = width / 2f; 353 const circleVec = Vector2(radius, radius); 354 const color = style.lineColor; 355 const fill = style.cropBox(inner, [radius, radius, 0, 0]); 356 357 if (canvasIO) { 358 canvasIO.drawCircle(start(inner) + circleVec, radius, color); 359 canvasIO.drawCircle(end(inner) - circleVec, radius, color); 360 canvasIO.drawRectangle(fill, color); 361 } 362 else { 363 io.drawCircle(start(inner) + circleVec, radius, color); 364 io.drawCircle(end(inner) - circleVec, radius, color); 365 io.drawRectangle(fill, color); 366 } 367 368 } 369 370 } 371 372 class DragAction : TreeAction { 373 374 public { 375 376 DragSlot slot; 377 Vector2 mouseStart; 378 FluidDroppable target; 379 Rectangle targetRectangle; 380 381 /// Current position of the pointer seen by the action. 382 Vector2 pointerPosition; 383 384 } 385 386 private { 387 388 bool _stopDragging; 389 bool _readyToDrop; 390 391 } 392 393 deprecated this(DragSlot slot) { 394 this(slot, slot.io.mousePosition); 395 } 396 397 this(DragSlot slot, Vector2 pointerPosition) { 398 this.slot = slot; 399 this.pointerPosition = pointerPosition; 400 this.mouseStart = pointerPosition; 401 } 402 403 Vector2 offset() const { 404 405 return pointerPosition - mouseStart; 406 407 } 408 409 Rectangle relativeDragRectangle() { 410 411 const rect = slot.dragRectangle(offset); 412 413 return Rectangle( 414 (rect.start - targetRectangle.start).tupleof, 415 rect.size.tupleof, 416 ); 417 418 } 419 420 override void beforeTree(Node, Rectangle) { 421 422 // Clear the target 423 target = null; 424 425 } 426 427 override void beforeResize(Node node, Vector2 space) { 428 429 // Reside only if OverlayIO is not in use 430 if (slot.overlayIO is null && node is node.tree.root) { 431 slot.resizeInternal(node, space); 432 } 433 434 } 435 436 override void beforeDraw(Node node, Rectangle rectangle, Rectangle outer, Rectangle inner) { 437 438 auto droppable = cast(FluidDroppable) node; 439 440 // Find all hovered droppable nodes 441 if (!droppable) return; 442 // TODO modal support? 443 if (!node.inBounds(outer, inner, pointerPosition).inSelf) return; 444 445 // Make sure this slot can be dropped in 446 if (!droppable.canDrop(slot)) return; 447 448 this.target = droppable; 449 this.targetRectangle = rectangle; 450 451 } 452 453 /// Tree drawn, draw the node now. 454 override void afterTree() { 455 456 if (slot.overlayIO is null ) { 457 drawSlot(slot.tree.root); 458 } 459 460 } 461 462 void drawSlot(Node parent) { 463 464 const rect = slot.dragRectangle(offset); 465 466 // Draw the slot 467 slot.drawDragged(parent, rect); 468 469 } 470 471 /// Process input. 472 override void afterInput(ref bool focusHandled) { 473 474 // We should have received a signal from the slot if it is still being dragged 475 if (!_stopDragging) { 476 _stopDragging = true; 477 if (target) { 478 target.dropHover(pointerPosition, relativeDragRectangle); 479 } 480 return; 481 } 482 483 // Drop the slot if a droppable node was found 484 if (target) { 485 486 // Ready to drop, perform the action 487 if (_readyToDrop) { 488 target.drop(pointerPosition, relativeDragRectangle, slot); 489 } 490 491 // Remove it from the original container and wait a frame 492 else { 493 slot.toRemove = true; 494 slot.overlay.toRemove = true; 495 _readyToDrop = true; 496 return; 497 } 498 499 } 500 501 // Stop dragging 502 slot.dragAction = null; // TODO Don't nullify this 503 slot.updateSize(); 504 stop; 505 506 } 507 508 }