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 }