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 }