1 module actions.find_hovered_node_action;
2 
3 import fluid;
4 
5 @safe:
6 
7 alias rectangleBox = nodeBuilder!RectangleBox;
8 
9 class RectangleBox : Node {
10 
11     Rectangle self;
12 
13     this(Rectangle self) {
14         this.self = self;
15     }
16 
17     override void resizeImpl(Vector2) {
18         minSize = Vector2(0, 0);
19     }
20 
21     override void drawImpl(Rectangle, Rectangle) {
22 
23     }
24 
25     override IsOpaque inBoundsImpl(Rectangle, Rectangle, Vector2 position) {
26         return self.contains(position)
27             ? IsOpaque.yes
28             : IsOpaque.no;
29     }
30 
31 }
32 
33 alias circleBox = nodeBuilder!CircleBox;
34 
35 class CircleBox : Node {
36 
37     Vector2 center;
38     float radius;
39 
40     this(Vector2 center, float radius) {
41         this.center = center;
42         this.radius = radius;
43     }
44 
45     override void resizeImpl(Vector2) {
46         minSize = Vector2(0, 0);
47     }
48 
49     override void drawImpl(Rectangle, Rectangle) {
50 
51     }
52 
53     override IsOpaque inBoundsImpl(Rectangle, Rectangle, Vector2 position) {
54         return (center.x - position.x)^^2 + (center.y - position.y)^^2 <= radius^^2
55             ? IsOpaque.yes
56             : IsOpaque.no;
57     }
58 
59 }
60 
61 alias myScrollable = nodeBuilder!MyScrollable;
62 
63 class MyScrollable : Frame, HoverScrollable {
64 
65     bool disableScroll;
66 
67     this(Node[] nodes...) {
68         super(nodes);
69     }
70 
71     alias opEquals = Space.opEquals;
72     override bool opEquals(const Object other) const {
73         return super.opEquals(other);
74     }
75 
76     override bool canScroll(const HoverPointer) const {
77         return !disableScroll;
78     }
79 
80     override bool scrollImpl(HoverPointer) {
81         return true;
82     }
83 
84     override Rectangle shallowScrollTo(const Node, Rectangle, Rectangle childBox) {
85         return childBox;
86     }
87 
88 }
89 
90 @("FindHoveredNodeAction can find any node by screen position")
91 unittest {
92 
93     Node rect1, rect2, circle;
94 
95     auto root = onionFrame(
96         rect1  = rectangleBox(Rectangle(0, 0, 50, 50)),
97         rect2  = rectangleBox(Rectangle(50, 50, 50, 50)),
98         circle = circleBox(Vector2(50, 50), 10),
99     );
100     auto action = new FindHoveredNodeAction;
101 
102     action.pointer.position = Vector2(0, 0);
103     root.startAction(action);
104     root.draw();
105     assert(action.result.opEquals(rect1));
106 
107     action.pointer.position = Vector2(60, 0);
108     root.startAction(action);
109     root.draw();
110     assert(action.result is null);
111 
112     action.pointer.position = Vector2(60, 50);
113     root.startAction(action);
114     root.draw();
115     assert(action.result.opEquals(circle));
116 
117     action.pointer.position = Vector2(75, 75);
118     root.startAction(action);
119     root.draw();
120     assert(action.result.opEquals(rect2));
121 
122 }
123 
124 @("FindHoveredNodeAction can find scrollable ancestors of any node")
125 unittest {
126 
127     Button buttonIn, buttonOut;
128     ScrollFrame scroll;
129 
130     auto root = sizeLock!hspace(
131         .sizeLimit(100, 100),
132         .nullTheme,
133         scroll = vscrollFrame(
134             .layout!(1, "fill"),
135             buttonIn = sizeLock!button(
136                 .sizeLimit(50, 100),
137                 "In scrollable 1",
138                 delegate { }
139             ),
140             sizeLock!button(
141                 .sizeLimit(50, 100),
142                 "In scrollable 2",
143                 delegate { }
144             ),
145         ),
146         buttonOut = button(
147             .layout!(1, "fill"),
148             "Outside",
149             delegate { }
150         ),
151     );
152     auto action = new FindHoveredNodeAction;
153     assert(root.isHorizontal);
154 
155     action.pointer.position = Vector2(25, 50);
156     action.pointer.scroll = Vector2(0, 10);
157     root.startAction(action);
158     root.draw();
159     assert(action.result.opEquals(buttonIn));
160     assert(action.scrollable.opEquals(scroll));
161 
162     action.pointer.position = Vector2(75, 50);
163     root.startAction(action);
164     root.draw();
165     assert(action.result.opEquals(buttonOut));
166     assert(action.scrollable is null);
167 
168 }
169 
170 @("FindHoveredNodeAction skips over Scrollables where scroll will have no effect")
171 unittest {
172 
173     MyScrollable outer, inner;
174 
175     auto root = sizeLock!vframe(
176         .sizeLimit(100, 100),
177         outer = myScrollable(
178             .layout!(1, "fill"),
179             inner = myScrollable(
180                 .layout!(1, "fill"),
181             ),
182         ),
183     );
184     auto action = new FindHoveredNodeAction;
185 
186     action.pointer.position = Vector2(50, 50);
187     action.pointer.scroll = Vector2(0, 10);
188     root.startAction(action);
189     root.draw();
190     assert(action.result.opEquals(inner));
191     assert(action.scrollable.opEquals(inner));
192 
193     inner.disableScroll = true;
194     root.startAction(action);
195     root.draw();
196     assert(action.result.opEquals(inner));
197     assert(action.scrollable.opEquals(outer));
198 
199 }
200 
201 @("FindHoveredNodeAction respects Node.isOpaque")
202 unittest {
203 
204     import std.conv;
205 
206     Frame container;
207     RectangleBox[3] boxes;
208 
209     auto root = sizeLock!vspace(
210         .sizeLimit(50, 150),
211         container = vframe(
212             .layout!(1, "fill"),
213             boxes[0] = rectangleBox(                   Rectangle(0,   0, 50, 50)),
214             boxes[1] = rectangleBox(IsOpaque.no,       Rectangle(0,  50, 50, 50)),
215             boxes[2] = rectangleBox(IsOpaque.onlySelf, Rectangle(0, 100, 50, 50)),
216         ),
217     );
218 
219     auto action = new FindHoveredNodeAction;
220 
221     void testSearch(IsOpaque opacity, Vector2 position, Node result) {
222         container.isOpaque = opacity;
223         action.pointer.position = position;
224         root.startAction(action);
225         root.draw();
226         if (result is null) {
227             assert(action.result is null, action.result.text);
228         }
229         else {
230             assert(result.opEquals(action.result), action.result.text);
231         }
232     }
233 
234     // Children should be selectable for both `yes` and `no` options
235     testSearch(IsOpaque.yes, Vector2(25,  25), boxes[0]);
236     testSearch(IsOpaque.yes, Vector2(25, 125), boxes[2]);
237 
238     // `no` on boxes[1] prevents from selecting
239     testSearch(IsOpaque.yes, Vector2(25, 75), container);
240     testSearch(IsOpaque.no,  Vector2(25, 75), null);
241 
242     // The remaining options disable children access
243     testSearch(IsOpaque.notInBranch, Vector2(25, 25), null);
244     testSearch(IsOpaque.onlySelf,    Vector2(25, 25), container);
245 
246 }