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 }