1 module fluid.drag_slot; 2 3 import fluid.tree; 4 import fluid.node; 5 import fluid.slot; 6 import fluid.input; 7 import fluid.utils; 8 import fluid.style; 9 import fluid.backend; 10 import fluid.structs; 11 12 13 @safe: 14 15 16 /// A drag slot is a node slot providing drag & drop functionality. 17 alias dragSlot = simpleConstructor!DragSlot; 18 19 /// ditto 20 class DragSlot : NodeSlot!Node, FluidHoverable { 21 22 mixin makeHoverable; 23 mixin enableInputActions; 24 25 public { 26 27 DragHandle handle; 28 29 /// Current drag action, if applicable. 30 DragAction dragAction; 31 32 } 33 34 private { 35 36 bool _drawDragged; 37 38 /// Size used while the slot is being dragged. 39 Vector2 _size; 40 41 /// Last position when drawing statically (not dragging). 42 Vector2 _staticPosition; 43 44 } 45 46 /// Create a new drag slot and place a node inside of it. 47 this(Node node = null) { 48 49 super(node); 50 this.handle = dragHandle(.layout!"fill"); 51 52 } 53 54 /// If true, this node is currently being dragged. 55 bool isDragged() const { 56 57 return dragAction !is null; 58 59 } 60 61 /// Drag the node. 62 @(FluidInputAction.press, .WhileDown) 63 void drag() 64 in (tree) 65 do { 66 67 // Ignore if already dragging 68 if (dragAction) { 69 70 dragAction._stopDragging = false; 71 72 } 73 74 else { 75 76 // Queue the drag action 77 dragAction = new DragAction(this); 78 tree.queueAction(dragAction); 79 updateSize(); 80 81 } 82 83 } 84 85 private Rectangle dragRectangle(Vector2 offset) const { 86 87 const position = _staticPosition + offset; 88 89 return Rectangle(position.tupleof, _size.tupleof); 90 91 } 92 93 private void drawDragged(Vector2 offset) { 94 95 const rect = dragRectangle(offset); 96 97 _drawDragged = true; 98 minSize = _size; 99 scope (exit) _drawDragged = false; 100 scope (exit) minSize = Vector2(0, 0); 101 102 draw(rect); 103 104 } 105 106 alias isHidden = typeof(super).isHidden; 107 108 @property 109 override bool isHidden() const scope { 110 111 // Don't hide from the draw action 112 if (_drawDragged) 113 return super.isHidden; 114 115 // Hide the node from its parent if it's dragged 116 else return super.isHidden || isDragged; 117 118 } 119 120 override void resizeImpl(Vector2 available) { 121 122 // Resize the slot 123 super.resizeImpl(available); 124 125 // Resize the handle 126 handle.resize(tree, theme, available); 127 128 // Add space for the handle 129 if (!handle.isHidden) { 130 131 minSize.y += handle.minSize.y + style.gap.sideY; 132 133 if (handle.minSize.x > minSize.x) { 134 minSize.x = handle.minSize.x; 135 } 136 137 } 138 139 } 140 141 private void resizeInternal(LayoutTree* tree, Theme theme, Vector2 space) { 142 143 _drawDragged = true; 144 scope (exit) _drawDragged = false; 145 146 resize(tree, theme, space); 147 148 // Save the size 149 _size = minSize; 150 151 } 152 153 override void drawImpl(Rectangle outer, Rectangle inner) { 154 155 const handleWidth = handle.minSize.y; 156 157 auto style = pickStyle; 158 auto handleRect = inner; 159 auto valueRect = inner; 160 161 // Save position 162 if (!_drawDragged) 163 _staticPosition = start(outer); 164 165 // Split the inner rectangle to fit the handle 166 handleRect.h = handleWidth; 167 if (!handle.isHidden) { 168 valueRect.y += handleWidth + style.gap.sideY; 169 valueRect.h -= handleWidth + style.gap.sideY; 170 } 171 172 // Disable the children while dragging 173 const disable = _drawDragged && !tree.isBranchDisabled; 174 175 if (disable) tree.isBranchDisabled = true; 176 177 // Draw the value 178 super.drawImpl(outer, valueRect); 179 180 if (disable) tree.isBranchDisabled = false; 181 182 // Draw the handle 183 handle.draw(handleRect); 184 185 } 186 187 protected override bool hoveredImpl(Rectangle rect, Vector2 position) { 188 189 return Node.hoveredImpl(rect, position); 190 191 } 192 193 override bool isHovered() const { 194 195 return this is tree.hover || super.isHovered(); 196 197 } 198 199 void mouseImpl() { 200 201 } 202 203 } 204 205 /// Draggable handle. 206 alias dragHandle = simpleConstructor!DragHandle; 207 208 /// ditto 209 class DragHandle : Node { 210 211 /// Additional features available for drag handle styling 212 static class Extra : typeof(super).Extra { 213 214 /// Width of the draggable bar 215 float width; 216 217 this(float width) { 218 219 this.width = width; 220 221 } 222 223 } 224 225 /// Get the width of the bar. 226 float width() const { 227 228 const extra = cast(const Extra) style.extra; 229 230 if (extra) 231 return extra.width; 232 else 233 return 0; 234 235 } 236 237 override bool hoveredImpl(Rectangle, Vector2) { 238 239 return false; 240 241 } 242 243 override void resizeImpl(Vector2 available) { 244 245 minSize = Vector2(width * 2, width); 246 247 } 248 249 override void drawImpl(Rectangle outer, Rectangle inner) { 250 251 const width = this.width; 252 253 const radius = width / 2f; 254 const circleVec = Vector2(radius, radius); 255 const color = style.lineColor; 256 const fill = style.cropBox(inner, [radius, radius, 0, 0]); 257 258 io.drawCircle(start(inner) + circleVec, radius, color); 259 io.drawCircle(end(inner) - circleVec, radius, color); 260 io.drawRectangle(fill, color); 261 262 } 263 264 unittest { 265 266 import std.algorithm; 267 268 import fluid.label; 269 import fluid.theme; 270 271 auto theme = nullTheme.derive( 272 rule( 273 gap = 4, 274 ), 275 ); 276 auto io = new HeadlessBackend; 277 auto content = label("a"); 278 auto root = dragSlot(theme, content); 279 root.io = io; 280 root.handle.hide(); 281 root.draw(); 282 283 assert(root.minSize == content.minSize); 284 assert(io.textures.canFind!(a 285 => a.position == Vector2(0, 0) 286 && a.id == content.text.texture.chunks[0].texture.id)); 287 288 } 289 290 } 291 292 class DragAction : TreeAction { 293 294 public { 295 296 DragSlot slot; 297 Vector2 mouseStart; 298 FluidDroppable target; 299 Rectangle targetRectangle; 300 301 } 302 303 private { 304 305 bool _stopDragging; 306 bool _readyToDrop; 307 308 } 309 310 this(DragSlot slot) { 311 312 this.slot = slot; 313 this.mouseStart = slot.io.mousePosition; 314 315 } 316 317 Vector2 offset() const { 318 319 return slot.io.mousePosition - mouseStart; 320 321 } 322 323 Rectangle relativeDragRectangle() { 324 325 const rect = slot.dragRectangle(offset); 326 327 return Rectangle( 328 (rect.start - targetRectangle.start).tupleof, 329 rect.size.tupleof, 330 ); 331 332 } 333 334 override void beforeTree(Node, Rectangle) { 335 336 // Clear the target 337 target = null; 338 339 } 340 341 override void beforeResize(Node node, Vector2 space) { 342 343 // Resizing the root 344 if (node is node.tree.root) { 345 346 // Resize the slot too 347 slot.resizeInternal(node.tree, node.theme, space); 348 349 } 350 351 } 352 353 override void beforeDraw(Node node, Rectangle rectangle) { 354 355 auto droppable = cast(FluidDroppable) node; 356 357 // Find all hovered droppable nodes 358 if (!droppable) return; 359 if (!node.isHovered) return; 360 361 // Make sure this slot can be dropped in 362 if (!droppable.canDrop(slot)) return; 363 364 this.target = droppable; 365 this.targetRectangle = rectangle; 366 367 droppable.dropHover(slot.io.mousePosition, relativeDragRectangle); 368 369 } 370 371 /// Tree drawn, draw the node now. 372 override void afterTree() { 373 374 // Draw the slot 375 slot.drawDragged(offset); 376 _stopDragging = true; 377 378 } 379 380 /// Process input. 381 override void afterInput(ref bool focusHandled) { 382 383 // We should have received a signal from the slot if it is still being dragged 384 if (!_stopDragging) return; 385 386 // Drop the slot if a droppable node was found 387 if (target) { 388 389 // Ready to drop, perform the action 390 if (_readyToDrop) { 391 392 target.drop(slot.io.mousePosition, relativeDragRectangle, slot); 393 394 } 395 396 // Remove it from the original container and wait a frame 397 else { 398 399 slot.toRemove = true; 400 _readyToDrop = true; 401 return; 402 403 } 404 405 } 406 407 // Stop dragging 408 slot.dragAction = null; 409 slot.updateSize(); 410 stop; 411 412 } 413 414 }