1 module nodes.hover_transform;
2 
3 import fluid;
4 import fluid.future.pipe;
5 
6 import nodes.hover_chain;
7 
8 @safe:
9 
10 @("HoverTransform yields transformed pointers when iterated")
11 unittest {
12 
13     auto content = sizeLock!vspace(
14         .sizeLimit(500, 500),
15     );
16     auto transform = hoverTransform(
17         Rectangle(50, 50, 100, 100),
18         content
19     );
20     auto hover = hoverChain(
21         .layout!(1, "fill"),
22         transform,
23     );
24     auto root = testSpace(
25         hover
26     );
27 
28     root.draw();
29 
30     auto action = hover.point(50, 50);
31     foreach (HoverPointer pointer; transform) {
32         assert(pointer.position == Vector2(0, 0));
33         assert(pointer.scroll == Vector2(0, 0));
34     }
35 
36     action.move(75, 150).scroll(10, 20);
37     foreach (HoverPointer pointer; transform) {
38         assert(pointer.position == Vector2(125, 500));
39         assert(pointer.scroll == Vector2(10, 20));
40     }
41 
42     hover.point(0, 0);
43     auto index = 0;
44     foreach (HoverPointer pointer; transform) {
45         if (index++ == 0) {
46             assert(pointer.position == Vector2(125, 500));
47             assert(pointer.scroll == Vector2(10, 20));
48         }
49         else {
50             assert(pointer.position == Vector2(-250, -250));
51             assert(pointer.scroll == Vector2(0, 0));
52         }
53     }
54 
55 }
56 
57 @("HoverTransform can fetch and transform nodes")
58 unittest {
59 
60     auto transform = hoverTransform(
61         Rectangle(  50,   50, 100, 100),
62         Rectangle(-100, -100, 100, 100),
63     );
64     auto hover = hoverChain(transform);
65 
66     hover.draw();
67 
68     auto action = hover.point(56, 56).scroll(2, 3);
69     auto pointer = transform.fetch(action.pointer.id);
70     assert(pointer.id       == action.pointer.id);
71     assert(pointer.position == Vector2(-94, -94));
72     assert(pointer.scroll   == Vector2(  2,   3));
73 
74 }
75 
76 @("HoverTransform affects child nodes")
77 unittest {
78 
79     // Target's actual position is the first 50×50 rectangle
80     // Transform takes events from the 50×50 rectangle next to it.
81     auto tracker = sizeLock!hoverTracker(
82         .sizeLimit(50, 50),
83     );
84     auto transform = hoverTransform(Rectangle(50, 0, 50, 50));
85     auto hover = hoverChain();
86     auto root = chain(
87         inputMapChain(.layout!"fill"),
88         hover,
89         transform,
90         tracker,
91     );
92 
93     hover.point(75, 25)
94         .then((a) {
95             assert(tracker.hoverImplCount == 1);
96             assert(tracker.pressHeldCount == 0);
97             a.press(false);
98             return a.stayIdle;
99         })
100         .then((a) {
101             assert(tracker.hoverImplCount == 1);
102             assert(tracker.pressHeldCount == 1);
103             assert(tracker.pressCount == 0);
104             a.press(true);
105             return a.stayIdle;
106         })
107         .runWhileDrawing(root, 3);
108 
109     assert(tracker.hoverImplCount == 1);
110     assert(tracker.pressHeldCount == 2);
111     assert(tracker.pressCount == 1);
112 
113 }
114 
115 @("HoverTransform doesn't trigger active events when outside")
116 unittest {
117 
118     auto tracker = sizeLock!hoverTracker(
119         .sizeLimit(50, 50),
120     );
121     auto transform = hoverTransform(Rectangle(50, 0, 50, 50));
122     auto hover = hoverChain();
123     auto root = chain(
124         inputMapChain(.layout!"fill"),
125         hover,
126         transform,
127         tracker,
128     );
129 
130     root.draw();
131 
132     // Holding and clicking works inside
133     hover.point(75, 25)
134         .then((a) {
135             assert(hover.isHovered(transform));
136             assert(transform.isHovered(tracker));
137             assert(tracker.hoverImplCount == 1);
138             a.press();
139             assert(tracker.pressHeldCount == 1);
140             assert(tracker.pressCount == 1);
141             return a.stayIdle;
142         })
143         .then((a) {
144             a.press(false);
145             assert(tracker.pressHeldCount == 2);
146             assert(tracker.pressCount == 1);
147             return a.move(25, 25);
148         })
149 
150         // Outside the node, only holding works
151         .then((a) {
152             a.press(false);
153             assert(hover.isHovered(transform));
154             assert(transform.isHovered(tracker));
155             assert(tracker.pressHeldCount == 3);
156             assert(tracker.pressCount == 1);
157             return a.stayIdle;
158         })
159         .then((a) {
160             a.press(true);
161             assert(tracker.pressHeldCount == 3);
162             assert(tracker.pressCount == 1);
163             assert(tracker.hoverImplCount == 1);
164         })
165         .runWhileDrawing(root, 5);
166 
167 }
168 
169 @("HoverTransform supports scrolling")
170 unittest {
171 
172     auto tracker = sizeLock!scrollTracker(
173         .sizeLimit(50, 50),
174     );
175     auto transform = hoverTransform(Rectangle(50, 0, 50, 50));
176     auto hover = hoverChain();
177     auto root = chain(
178         inputMapChain(.layout!"fill"),
179         hover,
180         transform,
181         tracker,
182     );
183 
184     hover.point(75, 25).scroll(0, 40)
185         .then((a) {
186             assert(tracker.lastScroll == Vector2(0, 40));
187             assert(tracker.totalScroll == Vector2(0, 40));
188         })
189         .runWhileDrawing(root, 2);
190 
191     hover.point(25, 25).scroll(0, 60)
192         .then((a) {
193             assert(tracker.lastScroll == Vector2(0, 40));
194             assert(tracker.totalScroll == Vector2(0, 40));
195         })
196         .runWhileDrawing(root, 2);
197 
198 }
199 
200 @("HoverTransform children can create pointers")
201 unittest {
202 
203     auto tracker1 = sizeLock!hoverTracker(
204         .sizeLimit(50, 50),
205     );
206     auto tracker2 = sizeLock!hoverTracker(
207         .sizeLimit(50, 50),
208     );
209     auto innerDevice = myHover();
210     auto outerDevice = myHover();
211     auto transform = hoverTransform(
212         .layout!"fill",
213         Rectangle(50, 0, 100, 50),
214         hspace(
215             tracker1,
216             tracker2,
217             innerDevice,
218         ),
219     );
220     auto hover = hoverChain();
221     auto root = chain(
222         inputMapChain(.layout!"fill"),
223         hover,
224         vspace(
225             .layout!"fill",
226             transform,
227             outerDevice,
228         ),
229     );
230 
231     // Point both pointers at the same spot
232     outerDevice.pointers = [
233         outerDevice.makePointer(0, Vector2(75, 25)),
234     ];
235     innerDevice.pointers = [
236         innerDevice.makePointer(0, Vector2(75, 25)),
237     ];
238     root.draw();
239     root.draw();  // 1 frame delay; need to wait for draw
240 
241     const outerPointerID = hover.armedPointerID(outerDevice.pointers[0].id);
242     const innerPointerID = hover.armedPointerID(innerDevice.pointers[0].id);
243     auto outerPointer = hover.fetch(outerPointerID);
244     auto innerPointer = hover.fetch(innerPointerID);
245 
246     // `outerDevice`, just like in other tests, gets transformed and hits `tracker1`.
247     assert(hover.hoverOf(outerPointer).opEquals(transform));
248     assert(transform.hoverOf(outerPointer).opEquals(tracker1));
249 
250     // `innerDevice` exists within the transformed coordinate system, so, within the system, its
251     // position will stay unchanged. It should hit `tracker2`
252     assert(hover.hoverOf(innerPointer).opEquals(transform));
253     assert(transform.hoverOf(innerPointer).opEquals(tracker2));
254 
255     // Press the inner device
256     innerDevice.emit(0, MouseIO.press.left);
257     root.draw();
258     assert(tracker1.pressCount == 0);
259     assert(tracker2.pressCount == 1);
260 
261 }
262 
263 @("HoverTransform supports iterating on hovered items")
264 unittest {
265 
266     auto tracker1 = sizeLock!hoverTracker(
267         .sizeLimit(50, 50),
268     );
269     auto tracker2 = sizeLock!hoverTracker(
270         .sizeLimit(50, 50),
271     );
272     auto tracker3 = sizeLock!hoverTracker(
273         .sizeLimit(50, 50),
274     );
275     auto transform = hoverTransform(Rectangle(50, 0, 200, 50));
276     auto hover = hoverChain();
277     auto root = chain(
278         inputMapChain(.layout!"fill"),
279         hover,
280         transform,
281         hspace(
282             tracker1,
283             sizeLock!vspace(
284                 .sizeLimit(50, 50)
285             ),
286             tracker2,
287             tracker3,
288         ),
289     );
290 
291     auto action1 = hover.point( 75, 25);
292     auto action2 = hover.point(125, 25);
293     auto action3 = hover.point(175, 25);
294 
295     join(action1, action2, action3)
296         .runWhileDrawing(root, 2);
297 
298     assert(action1.isHovered(transform));
299     assert(action2.isHovered(transform));
300     assert(action3.isHovered(transform));
301     action1.stayIdle;  // tracker1
302     action2.stayIdle;  // blank space
303     action3.stayIdle;  // tracker2
304 
305     int matched1, matched2;
306 
307     foreach (Hoverable hoverable; transform) {
308         if (hoverable.opEquals(tracker1)) matched1++;
309         else if (hoverable.opEquals(tracker2)) matched2++;
310         else assert(false);
311     }
312 
313     assert(matched1 == 1);
314     assert(matched2 == 1);
315 
316 }
317 
318 @("HoverTransform can be nested inside a scrollable")
319 unittest {
320 
321     auto innerScroll = sizeLock!scrollTracker(
322         .sizeLimit(100, 100),
323     );
324     auto transform = hoverTransform(
325         .layout!(1, "fill"),
326         Rectangle(250, 250, 250, 250),
327         innerScroll,
328     );
329     auto outerScroll = sizeLock!scrollTracker(
330         .sizeLimit(500, 500),
331         transform,
332     );
333     auto hover = hoverChain(outerScroll);
334     auto root = testSpace(.nullTheme, hover);
335 
336     // outer: (0, 0)–(500, 500)
337     // transform spans the entire area of outer,
338     // accepts input in (250, 250)–(500, 500)
339     // inner: (250, 250)–(350, 350)
340 
341     // Scroll in inner
342     hover.point(300, 300).scroll(1, 2)
343         .then((a) {
344             const armed = hover.armedPointer(a.pointer);
345 
346             assert(hover.hoverOf(a.pointer).opEquals(transform));
347             assert(hover.scrollOf(a.pointer).opEquals(transform));
348             assert(transform.scrollOf(armed).opEquals(innerScroll));
349             assert(outerScroll.lastScroll == Vector2(0, 0));
350             assert(innerScroll.lastScroll == Vector2(1, 2));
351 
352             // Scroll in outer
353             return a.move(200, 200).scroll(3, 4);
354         })
355         .then((a) {
356             assert(hover.scrollOf(a.pointer).opEquals(outerScroll));
357             assert(outerScroll.lastScroll == Vector2(3, 4));
358             assert(innerScroll.lastScroll == Vector2(1, 2));
359         })
360         .runWhileDrawing(root, 3);
361 
362 }
363 
364 @("HoverTransform works with scrollIntoView")
365 unittest {
366 
367     ScrollFrame outerFrame, innerFrame;
368     Button target;
369 
370     auto ui = sizeLock!vspace(
371         .sizeLimit(250, 250),
372         .nullTheme,
373         outerFrame = vscrollFrame(
374             .layout!(1, "fill"),
375             sizeLock!vspace(
376                 .sizeLimit(250, 250),
377             ),
378             hoverTransform(
379                 Rectangle(0, 0, 100, 100),
380                 innerFrame = sizeLock!vscrollFrame(
381                     .sizeLimit(250, 250),
382                     sizeLock!vspace(
383                         .sizeLimit(250, 250),
384                     ),
385                     target = button("Make me visible!", delegate { }),
386                     sizeLock!vspace(
387                         .sizeLimit(250, 250),
388                     ),
389                 ),
390             ),
391         )
392     );
393 
394     auto hover = hoverChain(ui);
395     auto root = testSpace(hover);
396 
397     root.drawAndAssert(
398         outerFrame.isDrawn.at(0, 0, 250, 250),
399         innerFrame.isDrawn.at(0, 250),
400         target.isDrawn.at(0, 500),
401     );
402     target.scrollToTop()
403         .runWhileDrawing(root, 1);
404     root.drawAndAssert(
405         outerFrame.isDrawn.at(0, 0, 250, 250),
406         innerFrame.isDrawn.at(0, 0),
407         target.isDrawn.at(0, 0),
408     );
409 
410     assert(outerFrame.scroll == 250);
411     assert(innerFrame.scroll == 250);
412 
413 
414 }
415 
416 @("HoverTransform can switch between targets")
417 unittest {
418 
419     Button[2] buttons;
420 
421     auto content = resolutionOverride!vspace(
422         Vector2(400, 400),
423         buttons[0] = button(.layout!(1, "fill"), "One", delegate { }),
424         buttons[1] = button(.layout!(1, "fill"), "One", delegate { }),
425     );
426     auto transform = hoverTransform(
427         Rectangle(0, 0, 100, 100),
428         content
429     );
430     auto hover = hoverChain(
431         .layout!(1, "fill"),
432         transform,
433     );
434     auto root = testSpace(hover);
435 
436     hover.point(25, 25)
437         .then((a) {
438             assert(transform.isHovered(buttons[0]));
439             a.press();
440             return a.stayIdle;
441         })
442         .then((a) => a.move(75, 75))
443         .then((a) {
444             assert(transform.isHovered(buttons[1]));
445             a.press();
446         })
447         .runWhileDrawing(root, 4);
448 
449 }
450 
451 @("HoverTransform supports holding scroll")
452 unittest {
453 
454     auto innerButton = button(.layout!(1, "fill"), "Two", delegate { });
455     auto tracker = scrollTracker(
456         .layout!(1, "fill"),
457         innerButton,
458     );
459     auto content = resolutionOverride!vspace(
460         Vector2(400, 400),
461         button(.layout!(1, "fill"), "One", delegate { }),
462         tracker,
463     );
464     auto transform = hoverTransform(
465         Rectangle(0, 0, 100, 100),
466         content
467     );
468     auto hover = hoverChain(
469         .layout!(1, "fill"),
470         transform,
471     );
472     auto root = testSpace(hover);
473 
474     hover.point(75, 75).scroll(0, 25)
475         .then((a) {
476             const pointer = hover.armedPointer(a.pointer);
477             assert(transform.scrollOf(pointer).opEquals(tracker));
478             assert(tracker.lastScroll == Vector2(0, 25));
479             return a.move(25, 25).holdScroll(0, 5);
480         })
481         .then((a) {
482             const pointer = hover.armedPointer(a.pointer);
483             assert(transform.scrollOf(pointer).opEquals(tracker));
484             assert(tracker.lastScroll  == Vector2(0,  5));
485             assert(tracker.totalScroll == Vector2(0, 30));
486             return a.scroll(0, 25);
487         })
488         .runWhileDrawing(root, 4);
489 
490     assert(tracker.totalScroll == Vector2(0, 30));
491 
492 }
493 
494 @("HoverTransform passes focus on press")
495 unittest {
496 
497     auto content = resolutionOverride!button(
498         Vector2(400, 400),
499         "One",
500         delegate { },
501     );
502     auto transform = hoverTransform(
503         Rectangle(0, 0, 100, 100),
504         vspace(
505             resolutionOverride!button(  // This button should never be selected by hoverChain
506                 Vector2(0, 0),
507                 "Zero",
508                 delegate {
509                     assert(false);
510                 }
511             ),
512             content
513         ),
514     );
515     auto hover = hoverChain(transform);
516     auto focus = focusChain(
517         .layout!(1, "fill"),
518         hover,
519     );
520     auto root = testSpace(focus);
521 
522     hover.point(25, 25)
523         .then((a) {
524             a.press();
525             return a.stayIdle();
526         })
527         .then((a) { })
528         .runWhileDrawing(root, 2);
529 
530     assert(content.isFocused);
531     assert(focus.isFocused(content));
532 
533 }
534 
535 version (TODO)  // https://git.samerion.com/Samerion/Fluid/issues/366
536 @("HoverTransform does not block focus")
537 unittest {
538 
539     Button[4] buttons;
540 
541     auto focus = focusChain(
542         vspace(
543             buttons[0] = button("One", delegate { }),
544             hoverTransform(
545                 Rectangle(),
546                 vspace(
547                     buttons[1] = button("Two", delegate { }),
548                     buttons[2] = button("Three", delegate { }),
549                 ),
550             ),
551             buttons[3] = button("Four", delegate { }),
552         ),
553     );
554     auto hover = hoverChain(focus);
555     auto root = hover;
556     focus.currentFocus = buttons[0];
557 
558     root.draw();
559     focus.focusNext()
560         .thenAssertEquals(buttons[1])
561         .runWhileDrawing(root, 1);
562 
563 }