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 }