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; 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; 169 valueRect.h -= handleWidth + style.gap; 170 } 171 172 // Draw the value 173 super.drawImpl(outer, valueRect); 174 175 // Draw the handle 176 handle.draw(handleRect); 177 178 } 179 180 protected override bool hoveredImpl(Rectangle rect, Vector2 position) { 181 182 return Node.hoveredImpl(rect, position); 183 184 } 185 186 override bool isHovered() const { 187 188 return this is tree.hover || super.isHovered(); 189 190 } 191 192 void mouseImpl() { 193 194 } 195 196 } 197 198 /// Draggable handle. 199 alias dragHandle = simpleConstructor!DragHandle; 200 201 /// ditto 202 class DragHandle : Node { 203 204 /// Additional features available for drag handle styling 205 static class Extra : typeof(super).Extra { 206 207 /// Width of the draggable bar 208 float width; 209 210 this(float width) { 211 212 this.width = width; 213 214 } 215 216 } 217 218 /// Get the width of the bar. 219 float width() const { 220 221 const extra = cast(const Extra) style.extra; 222 223 if (extra) 224 return extra.width; 225 else 226 return 0; 227 228 } 229 230 override bool hoveredImpl(Rectangle, Vector2) { 231 232 return false; 233 234 } 235 236 override void resizeImpl(Vector2 available) { 237 238 minSize = Vector2(width * 2, width); 239 240 } 241 242 override void drawImpl(Rectangle outer, Rectangle inner) { 243 244 const width = this.width; 245 246 const radius = width / 2f; 247 const circleVec = Vector2(radius, radius); 248 const color = style.lineColor; 249 const fill = style.cropBox(inner, [radius, radius, 0, 0]); 250 251 io.drawCircle(start(inner) + circleVec, radius, color); 252 io.drawCircle(end(inner) - circleVec, radius, color); 253 io.drawRectangle(fill, color); 254 255 } 256 257 unittest { 258 259 import std.algorithm; 260 261 import fluid.label; 262 import fluid.theme; 263 264 auto theme = nullTheme.derive( 265 rule( 266 gap = 4, 267 ), 268 ); 269 auto io = new HeadlessBackend; 270 auto content = label("a"); 271 auto root = dragSlot(theme, content); 272 root.io = io; 273 root.handle.hide(); 274 root.draw(); 275 276 assert(root.minSize == content.minSize); 277 assert(io.textures.canFind!(a 278 => a.position == Vector2(0, 0) 279 && a.id == content.text.texture.chunks[0].texture.id)); 280 281 } 282 283 } 284 285 class DragAction : TreeAction { 286 287 public { 288 289 DragSlot slot; 290 Vector2 mouseStart; 291 FluidDroppable target; 292 Rectangle targetRectangle; 293 294 } 295 296 private { 297 298 bool _stopDragging; 299 bool _readyToDrop; 300 301 } 302 303 this(DragSlot slot) { 304 305 this.slot = slot; 306 this.mouseStart = slot.io.mousePosition; 307 308 } 309 310 Vector2 offset() const { 311 312 return slot.io.mousePosition - mouseStart; 313 314 } 315 316 Rectangle relativeDragRectangle() { 317 318 const rect = slot.dragRectangle(offset); 319 320 return Rectangle( 321 (rect.start - targetRectangle.start).tupleof, 322 rect.size.tupleof, 323 ); 324 325 } 326 327 override void beforeTree(Node, Rectangle) { 328 329 // Clear the target 330 target = null; 331 332 } 333 334 override void beforeResize(Node node, Vector2 space) { 335 336 // Resizing the root 337 if (node is node.tree.root) { 338 339 // Resize the slot too 340 slot.resizeInternal(node.tree, node.theme, space); 341 342 } 343 344 } 345 346 override void beforeDraw(Node node, Rectangle rectangle) { 347 348 auto droppable = cast(FluidDroppable) node; 349 350 // Find all hovered droppable nodes 351 if (!droppable) return; 352 if (!node.isHovered) return; 353 354 // Make sure this slot can be dropped in 355 if (!droppable.canDrop(slot)) return; 356 357 this.target = droppable; 358 this.targetRectangle = rectangle; 359 360 droppable.dropHover(slot.io.mousePosition, relativeDragRectangle); 361 362 } 363 364 /// Tree drawn, draw the node now. 365 override void afterTree() { 366 367 // Draw the slot 368 slot.drawDragged(offset); 369 _stopDragging = true; 370 371 } 372 373 /// Process input. 374 override void afterInput(ref bool focusHandled) { 375 376 // We should have received a signal from the slot if it is still being dragged 377 if (!_stopDragging) return; 378 379 // Drop the slot if a droppable node was found 380 if (target) { 381 382 // Ready to drop, perform the action 383 if (_readyToDrop) { 384 385 target.drop(slot.io.mousePosition, relativeDragRectangle, slot); 386 387 } 388 389 // Remove it from the original container and wait a frame 390 else { 391 392 slot.toRemove = true; 393 _readyToDrop = true; 394 return; 395 396 } 397 398 } 399 400 // Stop dragging 401 slot.dragAction = null; 402 slot.updateSize(); 403 stop; 404 405 } 406 407 }