1 module nodes.drag_slot;
2 
3 import fluid;
4 
5 @safe:
6 
7 alias resizable = nodeBuilder!Resizable;
8 
9 class Resizable : Node {
10 
11     Vector2 size;
12 
13     this(Vector2 size) {
14         this.size = size;
15     }
16 
17     override void resizeImpl(Vector2) {
18         minSize = size;
19     }
20 
21     override void drawImpl(Rectangle, Rectangle) {
22 
23     }
24 
25     override IsOpaque inBoundsImpl(Rectangle, Rectangle, Vector2) {
26         return IsOpaque.no;
27     }
28 
29 }
30 
31 Theme testTheme;
32 
33 const frameColor = color("#fdc798");
34 
35 static this() {
36     import fluid.theme;
37     testTheme = nullTheme.derive(
38         rule!Frame(
39             backgroundColor = frameColor,
40         ),
41     );
42 }
43 
44 @("DragSlot ignores gap if the handle is hidden")
45 unittest {
46 
47     import std.algorithm;
48     import fluid.theme;
49 
50     auto theme = nullTheme.derive(
51         rule!DragSlot(gap = 4),
52     );
53     auto content = label("a");
54     auto slot = dragSlot(content);
55     auto root = testSpace(theme, slot);
56     slot.handle.hide();
57     root.draw();
58     assert(slot.getMinSize == content.getMinSize);
59     root.drawAndAssert(
60         content.drawsImage(content.text.texture.chunks[0].image)
61             .at(0, 0)
62     );
63 
64 }
65 
66 @("DragSlot can be dragged")
67 unittest {
68 
69     auto content = label(.ignoreMouse, "a");
70     auto slot = dragSlot(.nullTheme, content);
71     auto hover = hoverChain();
72     auto root = chain(
73         hover,
74         overlayChain(),
75         slot
76     );
77 
78     root.draw();
79     hover.point(4, 4)
80         .then((a) {
81             assert(a.isHovered(slot));
82             assert(!slot.dragAction);
83             a.press(false);
84             return a.move(100, 100);
85         })
86         .then((a) {
87             a.press(false);
88             assert(a.isHovered(slot));
89             assert(slot.dragAction.offset == Vector2(96, 96));
90             return a.move(50, -50);
91         })
92         .then((a) {
93             a.press(false);
94             assert(slot.dragAction.offset == Vector2(46, -54));
95             return root.nextFrame;
96         })
97         .runWhileDrawing(root);
98 
99     assert(!slot.dragAction);
100 
101 }
102 
103 @("DragSlot allows the dragged node to be resized while dragged")
104 unittest {
105 
106     auto content = resizable(Vector2(10, 10));
107     auto slot = dragSlot(.nullTheme, content);
108     auto hover = hoverChain();
109     auto root = chain(
110         hover,
111         overlayChain(),
112         slot
113     );
114 
115     root.draw();
116     hover.point(5, 5)
117         .then((a) {
118             assert(a.isHovered(slot));
119             assert(!slot.dragAction);
120             assert(content.getMinSize == Vector2(10, 10));
121             a.press(false);
122             return a.move(105, 5);
123         })
124 
125         // Resize the node
126         .then((a) {
127             a.press(false);
128             content.size = Vector2(0, 0);
129             content.updateSize();
130             assert(slot.dragAction.offset == Vector2(100, 0));
131             assert(content.getMinSize == Vector2(10, 10));
132             return a.move(205, 5);
133         })
134         .then((a) {
135             a.press(false);
136             assert(slot.dragAction.offset == Vector2(200, 0));
137             assert(content.getMinSize == Vector2(0, 0));
138             return a.move(305, 5);
139         })
140         .then((a) {
141             a.press(false);
142             assert(slot.dragAction.offset == Vector2(300, 0));
143             assert(content.getMinSize == Vector2(0, 0));
144             return root.nextFrame;
145         })
146         .runWhileDrawing(root);
147 
148     assert(!slot.dragAction);
149     assert(content.getMinSize == Vector2(0, 0));
150 
151 }
152 
153 @("DragSlot contents can load I/O systems while dragged")
154 unittest {
155 
156     static class IOTracker : Node {
157 
158         HoverIO hoverIO;
159         CanvasIO canvasIO;
160 
161         override void resizeImpl(Vector2) {
162             use(hoverIO);
163             use(canvasIO);
164         }
165 
166         override void drawImpl(Rectangle, Rectangle) {
167 
168         }
169 
170     }
171 
172     alias ioTracker = nodeBuilder!IOTracker;
173 
174     auto slot = dragSlot();
175     auto overlay = overlayChain();
176     auto hover = hoverChain();
177     auto root = testSpace(
178         chain(
179             focusChain(),
180             hover,
181             overlay,
182             slot,
183         )
184     );
185 
186     root.drawAndAssert(
187         overlay.drawsChild(slot),
188     );
189     assert(slot.hoverIO.opEquals(hover));
190     auto action = hover.point(0, 0);
191     slot.drag(action.pointer);
192     assert(slot.dragAction);
193     root.drawAndAssert(
194         overlay.drawsChild(slot.overlay),
195         slot.isDrawn,
196     );
197     root.drawAndAssertFailure(
198         overlay.drawsChild(slot),
199     );
200     assert(slot.dragAction);
201     assert(slot.hoverIO.opEquals(hover));
202 
203     // Place the tracker in the slot, continue dragging
204     auto tracker = ioTracker();
205     slot = tracker;
206     slot.drag(action.pointer);
207     root.drawAndAssert(
208         overlay.drawsChild(slot.overlay),
209         slot.value.isDrawn,
210     );
211     assert(slot.dragAction);
212     assert(slot.hoverIO.opEquals(hover));
213     assert(slot.canvasIO.opEquals(root));
214     assert(tracker.hoverIO.opEquals(hover));
215     assert(tracker.canvasIO.opEquals(root));
216 
217 }
218 
219 @("Droppable nodes can be nested")
220 unittest {
221 
222     DragSlot slot;
223     Frame inner;
224     Label[2] dummies;
225 
226     const targets = [
227         Vector2(0, 450),  // Control sample
228         Vector2(0, 0),    // Drop into outer
229         Vector2(0, 300),  // Drop into inner
230     ];
231 
232     foreach (index, dropTarget; targets) {
233 
234         auto outer = sizeLock!vframe(
235             .layout!(1, "fill"),
236             .sizeLimit(600, 600),
237             .acceptDrop,
238             dummies[0] = label(
239                 .layout!1,
240                 "Dummy 1",
241             ),
242             inner = vframe(
243                 .layout!(1, "fill"),
244                 .acceptDrop,
245                 dummies[1] = label(
246                     .layout!1,
247                     "Dummy 2"
248                 ),
249                 slot = sizeLock!dragSlot(
250                     .layout!1,
251                     .sizeLimit(100, 100),
252                     label(
253                         .ignoreMouse,
254                         "Drag me"
255                     ),
256                 ),
257             )
258         );
259         auto overlay = overlayChain(.layout!"fill");
260         auto hover = hoverChain(.testTheme, .layout!"fill");
261         auto root = testSpace(
262             chain(hover, overlay, outer)
263         );
264 
265         root.drawAndAssert(
266             slot.isDrawn().at(0, 450),
267         ),
268 
269         hover.point(1, 451)
270             .then((a) {
271                 a.press(false);
272                 return a.move(dropTarget);
273             })
274 
275             // Hover over the target
276             .then((a) {
277                 a.press(false);  // Wait 1 more frame to trigger `afterKeyboard`
278                 root.draw();     // TODO fix this in 0.8.0
279 
280                 // Control sample
281                 a.press(false);
282                 if (index == 0) {
283                     root.drawAndAssert(
284                         dummies[0].isDrawn().at(0, 0),
285                         dummies[1].isDrawn().at(0, 300),
286                     );
287                 }
288                 // Drop into outer
289                 else if (index == 1) {
290                     root.drawAndAssert(
291                         dummies[0].isDrawn().at(0, 100),  // TODO correct expanding behavior
292                         dummies[1].isDrawn().at(0, 400),
293                     );
294                 }
295                 // Drop into inner
296                 else if (index == 2) {
297                     root.drawAndAssert(
298                         dummies[0].isDrawn().at(0, 000),
299                         dummies[1].isDrawn().at(0, 400),
300                     );
301                 }
302                 a.press(false);
303                 root.drawAndAssert(
304                     overlay.drawsChild(slot.overlay),
305                 );
306                 return a.stayIdle;
307             })
308 
309             // Drop it
310             .then((a) {
311                 a.press(true);
312                 root.draw();
313 
314                 if (index == 0) {
315                     root.drawAndAssert(
316                         dummies[0].isDrawn().at(0,   0),
317                         dummies[1].isDrawn().at(0, 300),
318                         slot      .isDrawn().at(0, 450),
319                     );
320                 }
321                 // Drop into outer
322                 else if (index == 1) {
323                     root.drawAndAssert(
324                         slot      .isDrawn().at(0,   0),
325                         dummies[0].isDrawn().at(0, 200),
326                         dummies[1].isDrawn().at(0, 400),
327                     );
328                 }
329                 // Drop into inner
330                 else if (index == 2) {
331                     root.drawAndAssert(
332                         dummies[0].isDrawn().at(0,   0),
333                         slot      .isDrawn().at(0, 300),
334                         dummies[1].isDrawn().at(0, 450),
335                     );
336                 }
337             })
338             .runWhileDrawing(root, 4);
339 
340     }
341 
342 }