1 module nodes.hover_chain;
2 
3 import std.array;
4 import fluid;
5 
6 @safe:
7 
8 alias myHover = nodeBuilder!MyHover;
9 
10 class MyHover : Node, MouseIO {
11 
12     HoverIO hoverIO;
13     HoverPointer[] pointers;
14 
15     inout(HoverPointer) makePointer(int number, Vector2 position, bool isDisabled = false) inout {
16         return inout HoverPointer(this, number, position, Vector2(), isDisabled);
17     }
18 
19     void emit(int number, InputEvent event) {
20 
21         foreach (pointer; pointers) {
22             if (pointer.number != number) continue;
23             hoverIO.emitEvent(pointer, event);
24             return;
25         }
26 
27         assert(false);
28 
29     }
30 
31     override void resizeImpl(Vector2 space) {
32         require(hoverIO);
33         loadPointers();
34         minSize = Vector2();
35     }
36 
37     override void drawImpl(Rectangle, Rectangle) {
38         loadPointers();
39     }
40 
41     void loadPointers() {
42 
43         foreach (ref pointer; pointers) {
44             load(hoverIO, pointer);
45         }
46 
47     }
48 
49 }
50 
51 alias hoverTracker = nodeBuilder!HoverTracker;
52 
53 class HoverTracker : Node, Hoverable {
54 
55     mixin enableInputActions;
56 
57     HoverIO hoverIO;
58 
59     int hoverImplCount;
60     int pressHeldCount;
61     int pressCount;
62 
63     HoverPointer lastPointer;
64     Appender!(HoverPointer[]) pointers;
65 
66     override void resizeImpl(Vector2) {
67         require(hoverIO);
68         minSize = Vector2();
69     }
70 
71     override void drawImpl(Rectangle, Rectangle) {
72         pointers.clear();
73         foreach (HoverPointer pointer; hoverIO) {
74             pointers ~= pointer;
75         }
76     }
77 
78     override bool blocksInput() const {
79         return isDisabled || isDisabledInherited;
80     }
81 
82     override bool hoverImpl(HoverPointer) {
83         assert(!blocksInput);
84         hoverImplCount++;
85         return false;
86     }
87 
88     override bool isHovered() const {
89         return hoverIO.isHovered(this);
90     }
91 
92     alias opEquals = typeof(super).opEquals;
93 
94     override bool opEquals(const Object other) const {
95         return super.opEquals(other);
96     }
97 
98     @(FluidInputAction.press, WhileHeld)
99     void pressHeld(HoverPointer pointer) {
100         assert(!blocksInput);
101         pressHeldCount++;
102         lastPointer = pointer;
103     }
104 
105     @(FluidInputAction.press)
106     void press(HoverPointer pointer) {
107         assert(!blocksInput);
108         pressCount++;
109         lastPointer = pointer;
110     }
111 
112 }
113 
114 alias scrollTracker = nodeBuilder!ScrollTracker;
115 
116 class ScrollTracker : Frame, HoverScrollable {
117 
118     bool disableScroll;
119     Vector2 totalScroll;
120     Vector2 lastScroll;
121 
122     this(Node[] nodes...) {
123         super(nodes);
124     }
125 
126     alias opEquals = Frame.opEquals;
127 
128     override bool opEquals(const Object other) const {
129         return super.opEquals(other);
130     }
131 
132     override bool canScroll(const HoverPointer) const {
133         return !disableScroll;
134     }
135 
136     override Rectangle shallowScrollTo(const Node, Rectangle, Rectangle childBox) {
137         return childBox;
138     }
139 
140     override bool scrollImpl(HoverPointer pointer) {
141         totalScroll += pointer.scroll;
142         lastScroll   = pointer.scroll;
143         return true;
144     }
145 
146 }
147 
148 @("HoverChain assigns unique IDs for each pointer number")
149 unittest {
150 
151     MyHover device;
152 
153     auto root = hoverChain(
154         device = myHover(),
155     );
156 
157     device.pointers = [
158         device.makePointer(0, Vector2(1, 1)),
159         device.makePointer(1, Vector2(1, 1)),
160     ];
161     root.draw();
162 
163     assert(device.pointers[0].id != device.pointers[1].id);
164 
165 }
166 
167 @("HoverChain assigns unique IDs for different devices")
168 unittest {
169 
170     MyHover firstDevice, secondDevice;
171 
172     auto root = hoverChain(
173         vspace(
174             firstDevice  = myHover(),
175             secondDevice = myHover()
176         ),
177     );
178 
179     firstDevice.pointers = [
180         firstDevice.makePointer(0, Vector2(1, 1)),
181     ];
182     secondDevice.pointers = [
183         secondDevice.makePointer(0, Vector2(1, 1)),
184     ];
185     root.draw();
186 
187     assert(firstDevice.pointers[0].id != secondDevice.pointers[0].id);
188 
189 }
190 
191 @("HoverChain can list hovered nodes")
192 unittest {
193 
194     MyHover device;
195     Button one, two;
196 
197     auto root = hoverChain(
198         .nullTheme,
199         sizeLock!vspace(
200             .sizeLimit(300, 300),
201             device = myHover(),
202             one    = button(.layout!(1, "fill"), "One", delegate { }),
203                      vframe(.layout!(1, "fill")),
204             two    = button(.layout!(1, "fill"), "Two", delegate { }),
205         ),
206     );
207 
208     root.draw();
209     assert(!root.hovers);
210     assert(!one.isHovered);
211     assert(!two.isHovered);
212 
213     device.pointers = [
214         device.makePointer(0, Vector2(0, 0)),
215     ];
216     root.draw();
217     root.draw();
218     assert(root.hovers(one));
219     assert(root.hoversOnly([one]));
220     assert(one.isHovered);
221     assert(!two.isHovered);
222 
223     device.pointers = [
224         device.makePointer(0, Vector2(0, 120)),
225     ];
226     root.draw();
227     root.draw();
228     assert(!root.hovers());
229 
230     device.pointers = [
231         device.makePointer(0, Vector2(0, 220)),
232     ];
233     root.draw();
234     root.draw();
235     assert(root.hovers());
236     assert(root.hoversOnly([two]));
237     assert(!one.isHovered);
238     assert(two.isHovered);
239 
240 }
241 
242 @("Opaque nodes block hover")
243 unittest {
244 
245     MyHover device;
246     Button btn;
247     Frame frame;
248 
249     auto root = hoverChain(
250         vspace(
251             device = myHover(),
252             onionFrame(
253                 .layout!"fill",
254                 btn   = button("One", delegate { }),
255                 frame = vframe(.layout!"fill"),
256             ),
257         ),
258     );
259 
260     device.pointers = [
261         device.makePointer(0, Vector2(20, 20)),
262     ];
263     root.draw();
264     root.draw();
265     assert(!root.hovers);
266 
267     frame.hide();
268     root.draw();
269     root.draw();
270     assert(root.hovers(btn));
271     assert(btn.isHovered);
272 
273     frame.show();
274     root.draw();
275     root.draw();
276     assert(!root.hovers);
277     assert(!btn.isHovered);
278 
279 }
280 
281 @("HoverChain triggers input actions")
282 unittest {
283 
284     MyHover device;
285     Button btn;
286     HoverChain hover;
287     int pressCount;
288 
289     auto map = InputMapping();
290     map.bindNew!(FluidInputAction.press)(MouseIO.codes.left);
291 
292     auto root = inputMapChain(
293         .nullTheme,
294         map,
295         hover = hoverChain(
296             vspace(
297                 device = myHover(),
298                 btn    = button("One", delegate { pressCount++; }),
299             ),
300         ),
301     );
302 
303     device.pointers = [
304         device.makePointer(0, Vector2(10, 10)),
305     ];
306     root.draw();
307     root.draw();
308 
309     assert(hover.hovers(btn));
310     assert(pressCount == 0);
311 
312     device.emit(0, MouseIO.release.left);
313 
314     assert(pressCount == 0);
315 
316     root.draw();
317     assert(pressCount == 1);
318 
319     root.draw();
320     assert(pressCount == 1);
321 
322 }
323 
324 @("HoverChain actions won't apply if hover changes")
325 unittest {
326 
327     MyHover device;
328     HoverChain hover;
329 
330     int onePressed;
331     int twoPressed;
332 
333     auto map = InputMapping();
334     map.bindNew!(FluidInputAction.press)(MouseIO.codes.left);
335 
336     auto root = inputMapChain(
337         map,
338         hover = hoverChain(
339             .nullTheme,
340             sizeLock!vspace(
341                 .sizeLimit(400, 400),
342                 device = myHover(),
343         		button(.layout!(1, "fill"), "One", delegate { onePressed++; }),
344                 button(.layout!(1, "fill"), "Two", delegate { twoPressed++; }),
345             ),
346         )
347     );
348 
349     device.pointers = [
350         device.makePointer(0, Vector2(100, 100)),
351         device.makePointer(1, Vector2(300, 300)),
352     ];
353     root.draw();
354 
355     // Hold both — no input action is necessary
356     device.emit(0, MouseIO.hold.left);
357     device.emit(1, MouseIO.hold.left);
358     root.draw();
359 
360     // Move them
361     device.pointers = [
362         device.makePointer(0, Vector2(100, 300)),
363         device.makePointer(1, Vector2(300, 100)),
364     ];
365     device.emit(0, MouseIO.hold.left);
366     device.emit(1, MouseIO.hold.left);
367     root.draw();
368 
369     assert(onePressed == 0);
370     assert(twoPressed == 0);
371 
372     // Press them
373     device.emit(0, MouseIO.release.left);
374     device.emit(1, MouseIO.release.left);
375     root.draw();
376     hover.runInputAction!(FluidInputAction.press)(device.pointers[0]);
377     hover.runInputAction!(FluidInputAction.press)(device.pointers[1]);
378 
379     assert(onePressed == 0);
380     assert(twoPressed == 0);
381 
382     // Move outside of the canvas
383     device.pointers = [
384         device.makePointer(0, Vector2(500, 500)),
385     ];
386     device.emit(0, MouseIO.hold.left);
387     root.draw();
388     device.emit(0, MouseIO.hold.left);
389     root.draw();
390     device.emit(0, MouseIO.release.left);
391     root.draw();
392     assert(onePressed == 0);
393 
394 }
395 
396 @("HoverChain triggers hover events, even if moved")
397 unittest {
398 
399     MyHover device;
400     HoverChain hover;
401     HoverTracker tracker1, tracker2;
402 
403     auto map = InputMapping();
404     map.bindNew!(FluidInputAction.press)(MouseIO.codes.left);
405 
406     auto root = inputMapChain(
407         map,
408         hover = hoverChain(
409             .nullTheme,
410             sizeLock!vspace(
411                 .sizeLimit(400, 400),
412                 device   = myHover(),
413                 tracker1 = hoverTracker(.layout!(1, "fill")),
414                 tracker2 = hoverTracker(.layout!(1, "fill")),
415             ),
416         )
417     );
418 
419     device.pointers = [
420         device.makePointer(0, Vector2(100, 100)),
421     ];
422     root.draw();
423     assert(tracker1.hoverImplCount == 1);
424     assert(tracker1.pressHeldCount == 0);
425     assert(tracker1.pressCount == 0);
426     assert(tracker2.hoverImplCount == 0);
427 
428     // Hover
429     root.draw();
430     assert(tracker1.hoverImplCount == 2);
431     assert(tracker1.pressHeldCount == 0);
432 
433     // Hold
434     device.emit(0, MouseIO.hold.left);
435     root.draw();
436     assert(tracker1.hoverImplCount == 2);  // pressHeld overrides this call
437     assert(tracker1.pressHeldCount == 1);
438 
439     device.emit(0, MouseIO.hold.left);
440     root.draw();
441     assert(tracker1.hoverImplCount == 2);
442     assert(tracker1.pressHeldCount == 2);
443     assert(tracker1.pressCount == 0);
444     assert(tracker2.hoverImplCount == 0);
445 
446     // Press
447     device.emit(0, MouseIO.press.left);
448     root.draw();
449     assert(tracker1.hoverImplCount == 2);
450     assert(tracker1.pressHeldCount == 3);
451     assert(tracker1.pressCount == 1);
452     assert(tracker2.hoverImplCount == 0);
453 
454     // Move & press
455     device.pointers = [
456         device.makePointer(0, Vector2(100, 300)),
457     ];
458     device.emit(0, MouseIO.hold.left);
459     root.draw();
460     assert(tracker1.hoverImplCount == 2);
461     assert(tracker1.pressHeldCount == 4);
462     assert(tracker2.hoverImplCount == 0);
463     assert(tracker2.pressHeldCount == 0);
464 
465     device.emit(0, MouseIO.hold.left);
466     root.draw();
467     assert(tracker1.hoverImplCount == 2);  // Hover still calls the old tracker
468     assert(tracker1.pressHeldCount == 5);
469 
470     device.emit(0, MouseIO.press.left);
471     root.draw();
472     assert(tracker1.hoverImplCount == 3);
473     assert(tracker1.pressHeldCount == 5);
474     assert(tracker1.pressCount == 1);
475     assert(tracker2.hoverImplCount == 0);
476     assert(tracker2.pressHeldCount == 0);
477     assert(tracker2.pressCount == 0);      // The press isn't registered.
478 
479     root.draw();
480     assert(tracker2.hoverImplCount == 1);  // Hover only registers after release
481 
482     // Unrelated input actions cannot trigger press
483     hover.runInputAction!(FluidInputAction.press)(device.pointers[0]);
484     assert(tracker2.pressCount == 1);
485     hover.runInputAction!(FluidInputAction.contextMenu)(device.pointers[0]);
486     assert(tracker2.pressCount == 1);
487     assert(tracker2.hoverImplCount == 1);
488 
489 }
490 
491 @("HoverChain runs hoverImpl if ActionIO is absent")
492 unittest {
493 
494     MyHover device;
495     HoverChain hover;
496     HoverTracker tracker;
497 
498     auto root = hover = hoverChain(
499         .nullTheme,
500         sizeLock!vspace(
501             .sizeLimit(400, 400),
502             device  = myHover(),
503             tracker = hoverTracker(.layout!(1, "fill")),
504         ),
505     );
506 
507     device.pointers = [
508         device.makePointer(0, Vector2(10, 10)),
509     ];
510     root.draw();
511     assert(tracker.hoverImplCount == 1);
512 
513 }
514 
515 @("HoverChain doesn't call input handlers on disabled nodes")
516 unittest {
517 
518     MyHover device;
519     HoverTracker inaccessibleTracker, mainTracker;
520 
521     auto root = hoverChain(
522         .layout!(1, "fill"),
523         vspace(
524             .layout!(1, "fill"),
525             device = myHover(),
526             onionFrame(
527                 .layout!(1, "fill"),
528                 inaccessibleTracker = hoverTracker(),
529                 mainTracker         = hoverTracker(.layout!"fill")
530             ),
531         ),
532     );
533 
534     device.pointers = [
535         device.makePointer(0, Vector2(100, 100)),
536     ];
537     root.draw();
538     assert(mainTracker.isHovered);
539     assert(mainTracker.hoverImplCount == 1);
540     assert(inaccessibleTracker.hoverImplCount == 0);
541 
542     // Disable the tracker; it shouldn't take hoverImpl, but it should continue to block
543     mainTracker.disable();
544     root.draw();
545     assert(mainTracker.hoverImplCount == 1);
546     assert(inaccessibleTracker.hoverImplCount == 0);
547 
548     // Press it
549     root.emitEvent(device.pointers[0], MouseIO.release.left);
550     root.draw();
551     assert(mainTracker.hoverImplCount == 1);
552     assert(mainTracker.pressCount == 0);
553     assert(inaccessibleTracker.hoverImplCount == 0);
554     assert(inaccessibleTracker.pressCount == 0);
555 
556 }
557 
558 @("HoverChain won't call handlers if disability status changes")
559 unittest {
560 
561     MyHover device;
562     HoverTracker tracker;
563 
564     auto root = hoverChain(
565         .layout!"fill",
566         vspace(
567             .layout!"fill",
568             device = myHover(),
569             tracker = hoverTracker(.layout!(1, "fill"))
570         ),
571     );
572 
573     device.pointers = [
574         device.makePointer(0, Vector2(100, 100)),
575     ];
576     root.draw();
577 
578     // Hold the left button now
579     root.runInputAction!(FluidInputAction.press)(device.pointers[0], false);
580     root.draw();
581     assert(tracker.isHovered);
582     assert(tracker.pressHeldCount == 1);
583 
584     // Block the button and press it
585     tracker.disable();
586     root.runInputAction!(FluidInputAction.press)(device.pointers[0], true);
587     root.draw();
588     assert(tracker.isHovered);
589     assert(tracker.pressHeldCount == 1);
590     assert(tracker.pressCount == 0);
591 
592 }
593 
594 @("HoverChain fires scroll events for scrollable nodes")
595 unittest {
596 
597     ScrollTracker tracker;
598     Button btn;
599 
600     auto hover = hoverChain(
601         .layout!"fill",
602         tracker = scrollTracker(
603             .layout!(1, "fill"),
604             btn = button(
605                 .layout!(1, "fill"),
606                 "Do not press me",
607                 delegate {
608                     assert(false);
609                 }
610             ),
611         ),
612     );
613     auto root = hover;
614 
615     hover.point(50, 50).scroll(0, 10)
616         .then((a) {
617             assert(a.currentHover && a.currentHover.opEquals(btn));
618             assert(a.currentScroll && a.currentScroll.opEquals(tracker));
619             assert(tracker.totalScroll == Vector2(0, 10));
620             assert(tracker.lastScroll == Vector2(0, 10));
621 
622             return a.scroll(5, 20);
623         })
624         .then((a) {
625             assert(a.currentHover && a.currentHover.opEquals(btn));
626             assert(a.currentScroll && a.currentScroll.opEquals(tracker));
627             assert(tracker.lastScroll == Vector2(5, 20));
628             assert(tracker.totalScroll == Vector2(5, 30));
629         })
630         .runWhileDrawing(root, 3);
631 
632 }
633 
634 @("HoverChain supports touchscreen scrolling")
635 unittest {
636 
637     // Try the same motion without holding the scroll (like a mouse)
638     // and then while holding (like a touchscreen)
639     foreach (testHold; [false, true]) {
640 
641         ScrollTracker targetTracker, dummyTracker;
642 
643         auto hover = hoverChain(
644             .layout!"fill",
645             sizeLock!vspace(
646                 .sizeLimit(100, 100),
647                 targetTracker = scrollTracker(
648                     .layout!(2, "fill"),
649                 ),
650                 dummyTracker = scrollTracker(
651                     .layout!(1, "fill"),
652                 ),
653             ),
654         );
655         auto root = hover;
656 
657         hover.point(50, 25)
658             .then((a) {
659 
660                 assert(a.currentScroll.opEquals(targetTracker));
661 
662                 // Hold the left mouse button
663                 a.press(false);
664                 return a.move(50, 75).holdScroll(0, -25, testHold);
665 
666             })
667             .then((a) {
668 
669                 if (testHold)
670                     assert(a.currentScroll.opEquals(targetTracker));
671                 else
672                     assert(a.currentScroll.opEquals(dummyTracker));
673 
674                 // Release the mouse button
675                 a.press;
676                 return a.holdScroll(0, -25, testHold);
677 
678             })
679             .runWhileDrawing(root);
680 
681         if (testHold) {
682             assert(targetTracker.totalScroll.y == -50);
683             assert(dummyTracker.totalScroll.y  ==   0);
684         }
685         else {
686             assert(targetTracker.totalScroll.y ==   0);
687             assert(dummyTracker.totalScroll.y  == -50);
688         }
689 
690     }
691 
692 }
693 
694 @("Held scroll counts as a single motion for canScroll() in HoverChain")
695 unittest {
696 
697     // Compare differences between held and hovered scroll
698     foreach (testHold; [false, true]) {
699 
700         ScrollTracker outerTracker, innerTracker;
701 
702         auto hover = hoverChain(
703             sizeLock!vspace(
704                 .sizeLimit(100, 100),
705                 outerTracker = scrollTracker(
706                     .layout!(1, "fill"),
707                     innerTracker = scrollTracker(
708                         .layout!(1, "fill"),
709                     ),
710                 ),
711             ),
712         );
713         auto root = hover;
714 
715         hover.point(50, 50)
716             .then((a) {
717                 assert(a.currentScroll.opEquals(innerTracker));
718                 return a.holdScroll(0, -20, testHold);
719             })
720             // Pretend innerTracker has reached its limit for scrolling
721             .then((a) {
722                 assert(a.currentScroll.opEquals(innerTracker));
723                 innerTracker.disableScroll = true;
724                 return a.holdScroll(0, -10, testHold);
725             })
726             .then((a) {
727                 if (testHold) {
728                     assert(a.currentScroll.opEquals(innerTracker));
729                 }
730                 else {
731                     assert(a.currentScroll.opEquals(outerTracker));
732                 }
733             })
734             .runWhileDrawing(root, 3);
735 
736         // A held scroll counts as a single motion, so it shouldn't test canScroll during the second frame
737         if (testHold) {
738             assert(innerTracker.totalScroll == Vector2(0, -30));
739             assert(outerTracker.totalScroll == Vector2(0,   0));
740         }
741 
742         // If not held, the motions are separate, so outerTracker should get the second part
743         else {
744             assert(innerTracker.totalScroll == Vector2(0, -20));
745             assert(outerTracker.totalScroll == Vector2(0, -10));
746         }
747 
748     }
749 
750 }
751 
752 @("Multiple pointers can be created in HoverChain at once")
753 unittest {
754 
755     auto hover = hoverChain();
756     auto root = hover;
757 
758     // Resize first
759     root.draw();
760 
761     HoverPointer pointer1;
762     pointer1.number = 1;
763     pointer1.position = Vector2(10, 10);
764     hover.loadTo(pointer1);
765 
766     HoverPointer pointer2;
767     pointer2.number = 2;
768     pointer2.position = Vector2(20, 20);
769     hover.loadTo(pointer2);
770     root.draw();
771 
772     assert(hover.fetch(pointer1.id).position == Vector2(10, 10));
773     assert(hover.fetch(pointer2.id).position == Vector2(20, 20));
774 
775 }
776 
777 @("Pressing a node switches focus")
778 unittest {
779 
780     Button button1, button2;
781 
782     auto focus = focusChain();
783     auto hover = hoverChain();
784     auto root = chain(
785         focus,
786         hover,
787         sizeLock!vspace(
788             .sizeLimit(100, 100),
789             .nullTheme,
790             button1 = button(.layout!(1, "fill"), "One", delegate { }),
791             button2 = button(.layout!(1, "fill"), "Two", delegate { }),
792         ),
793     );
794 
795     root.draw();
796 
797     // Hover the first button; focus should stay the same
798     hover.point(50, 25)
799         .then((a) {
800             assert(a.isHovered(button1));
801             assert(focus.currentFocus is null);
802 
803             // Press to change focus (inactive)
804             a.press(false);
805             return a.stayIdle;
806         })
807         .then((a) {
808             assert(a.isHovered(button1));
809             assert(focus.isFocused(button1));
810 
811             // Move onto the other button, focus should stay the same
812             a.press(false);
813             return a.move(50, 75);
814         })
815         .then((a) {
816             assert(a.isHovered(button1));
817             assert(focus.isFocused(button1));
818 
819             a.press();
820 
821         })
822         .runWhileDrawing(root);
823 
824     assert(focus.isFocused(button1));
825 
826 }
827 
828 @("Pressing a non-focusable node clears focus")
829 unittest {
830 
831     Button targetButton;
832     Frame filler;
833 
834     auto focus = focusChain();
835     auto hover = hoverChain();
836     auto root = chain(
837         focus,
838         hover,
839         sizeLock!vspace(
840             .sizeLimit(100, 100),
841             .nullTheme,
842             filler = vframe(.layout!(1, "fill")),
843             targetButton = button(.layout!(1, "fill"), "Target", delegate { }),
844         ),
845     );
846 
847     root.draw();
848     focus.currentFocus = targetButton;
849 
850     // Hover the space; focus should stay the same
851     hover.point(50, 25)
852         .then((a) {
853             assert(!a.isHovered(targetButton));
854             assert(focus.currentFocus.opEquals(targetButton));
855 
856             // Press to change focus (inactive)
857             a.press(false);
858             return a.stayIdle;
859         })
860         .then((a) {
861             assert(!a.isHovered(targetButton));
862             assert(focus.currentFocus is null);
863 
864             // Move onto the button, focus should remain empty
865             a.press(false);
866             return a.move(50, 75);
867         })
868         .then((a) {
869             assert(!a.isHovered(targetButton));
870             assert(focus.currentFocus is null);
871 
872             a.press();
873 
874         })
875         .runWhileDrawing(root);
876 
877     assert(focus.currentFocus is null);
878 
879 }
880 
881 @("Input event handlers receive negative pointer IDs from HoverChain")
882 unittest {
883 
884     auto tracker = sizeLock!hoverTracker(
885         .sizeLimit(100, 100),
886     );
887     auto hover = hoverChain(tracker);
888     auto root = chain(
889         inputMapChain(),
890         hover,
891     );
892 
893     // Try two pointers:
894     // First pointer should be ID 0, armed ID -1,
895     // Second pointer should be ID 1, armed ID -2
896     foreach (int id, armedID; [-1, -2]) {
897 
898         // Setup the pointer and click
899         auto action = hover.point(50, 50);
900         action.runWhileDrawing(root);
901         hover.emitEvent(action.pointer, MouseIO.press.left);
902         root.draw();
903 
904         assert(action.pointer.id == id);
905         assert(tracker.pressCount == 1);
906         assert(tracker.lastPointer.id == armedID);
907 
908         // Try upadting the data
909         action.move(20, 20);
910         assert(hover.fetch(id).position == Vector2(20, 20));
911         assert(hover.fetch(armedID).position == Vector2(50, 50));
912 
913         tracker.pressCount = 0;
914 
915     }
916 
917 }
918 
919 @("HoverChain correctly receives scroll data from node inside")
920 unittest {
921 
922     static class IncrementalScroller : Node, MouseIO {
923 
924         HoverIO hoverIO;
925         HoverPointer pointer;
926         int value;
927 
928         override void resizeImpl(Vector2) {
929             require(hoverIO);
930             pointer.device = this;
931             minSize = Vector2(0, 0);
932         }
933 
934         override void drawImpl(Rectangle, Rectangle) {
935             pointer.position = Vector2(0, 0);
936             pointer.scroll = Vector2(0, ++value);
937             load(hoverIO, pointer);
938         }
939 
940     }
941 
942     alias incrementalScroller = nodeBuilder!IncrementalScroller;
943 
944     auto automaton = incrementalScroller();
945     auto tracker = sizeLock!scrollTracker(
946         .sizeLimit(10, 10),
947     );
948     auto hover = hoverChain(
949         vspace(tracker, automaton),
950     );
951     auto root = testSpace(hover);
952 
953     // One frame to find the node
954     root.draw();
955     assert(tracker.lastScroll.y == 0);
956     assert(tracker.totalScroll.y == 0);
957 
958     root.draw();
959     assert(tracker.lastScroll.y == 1);
960     assert(tracker.totalScroll.y == 1);
961 
962     root.draw();
963     assert(tracker.lastScroll.y == 2);
964     assert(tracker.totalScroll.y == 3);
965 
966 }
967 
968 @("HoverChain exposes all active pointers")
969 unittest {
970 
971     auto hover = hoverChain();
972     auto action1 = hover.point(10, 20);
973     auto action2 = hover.point(20, 10);
974 
975     size_t index;
976     foreach (HoverPointer pointer; hover) {
977         if (index++ == 0) {
978             assert(pointer == action1.pointer);
979             assert(pointer != action2.pointer);
980         }
981         else {
982             assert(pointer != action1.pointer);
983             assert(pointer == action2.pointer);
984         }
985     }
986 
987 }
988 
989 @("HoverChain's HoverPointer iterator uses armed pointers when used while drawing")
990 unittest {
991 
992     auto tracker = hoverTracker();
993     auto hover = hoverChain(tracker);
994     auto action1 = hover.point(10, 20);
995     auto action2 = hover.point(20, 10);
996 
997     hover.draw();
998 
999     assert(tracker.pointers[][0].id == hover.armedPointerID(action1.pointer.id));
1000     assert(tracker.pointers[][1].id == hover.armedPointerID(action2.pointer.id));
1001 
1002     foreach (HoverPointer pointer; hover) {
1003         assert(pointer.id == hover.normalizedPointerID(pointer.id));
1004         assert(pointer.id != hover.armedPointerID(pointer.id));
1005     }
1006 
1007 }