1 /// This module implements actions that require the new I/O system to work correctly.
2 module fluid.future.action;
3 
4 import fluid.node;
5 import fluid.tree;
6 import fluid.types;
7 import fluid.style;
8 import fluid.actions;
9 
10 import fluid.io.focus;
11 import fluid.io.action;
12 
13 import fluid.future.pipe;
14 import fluid.future.branch_action;
15 
16 @safe:
17 
18 
19 /// Focus next or previous focusable node relative to the point of reference.
20 /// This function only works with nodes compatible with the new I/O system introduced in Fluid 0.7.2.
21 ///
22 /// Params:
23 ///     node   = Node to use for reference.
24 ///     branch = Branch to search. Nodes that are not children of this node will not be matched.
25 ///         Default to the whole tree.
26 ///     wrap   = If true, if no node remains to focus, focus the first or last node found.
27 OrderedFocusAction focusNext(Node node, bool wrap = true) {
28     auto action = new OrderedFocusAction(node, false, wrap);
29     node.tree.queueAction(action);
30     return action;
31 }
32 
33 /// ditto
34 OrderedFocusAction focusPrevious(Node node, bool wrap = true) {
35     auto action = new OrderedFocusAction(node, true, wrap);
36     node.tree.queueAction(action);
37     return action;
38 }
39 
40 /// ditto
41 OrderedFocusAction focusNext(Node node, Node branch, bool wrap = true) {
42     auto action = new OrderedFocusAction(node, false, wrap);
43     branch.queueAction(action);
44     return action;
45 }
46 
47 /// ditto
48 OrderedFocusAction focusPrevious(Node node, Node branch, bool wrap = true) {
49     auto action = new OrderedFocusAction(node, true, wrap);
50     branch.queueAction(action);
51     return action;
52 }
53 
54 final class OrderedFocusAction : FocusSearchAction {
55 
56     public {
57 
58         /// Node to use as reference. The action will either select the next node that follows, or the previous.
59         Node target;
60 
61         /// If true, the action finds the previous node. If false, the action finds the next one.
62         bool isReverse;
63 
64         /// If true, does nothing if the target node is the last (going forward) or the first (going backwards).
65         /// Otherwise goes back to the top or bottom respectively.
66         bool isWrapDisabled;
67 
68     }
69 
70     private {
71 
72         /// Last focusable node in the branch, first focusable node in the branch. Updates as the node iterates.
73         Node _last, _first;
74 
75         /// Previous and next focusable relative to the target.
76         Node _previous, _next;
77 
78     }
79 
80     this() {
81 
82     }
83 
84     this(Node target, bool isReverse = false, bool wrap = true) {
85         reset(target, isReverse, wrap);
86     }
87 
88     /// Re-arm the action.
89     void reset(Node target, bool isReverse = false, bool wrap = true) {
90         this.target = target;
91         this.isReverse = isReverse;
92         this.isWrapDisabled = !wrap;
93         clearSubscribers();
94     }
95 
96     override void beforeTree(Node node, Rectangle rect) {
97 
98         super.beforeTree(node, rect);
99         this._last = null;
100         this._first = null;
101         this._previous = null;
102         this._next = null;
103 
104     }
105 
106     override void beforeDraw(Node node, Rectangle) {
107 
108         // The start node is not a valid subject
109         if (startNode && node.opEquals(startNode)) return;
110 
111         // Found the target
112         if (node.opEquals(target)) {
113 
114             // Going backwards: Mark the last focusable as the previous node
115             if (isReverse) {
116                 _previous = _last;
117             }
118 
119             // Going forwards: Clear the next focusable so it can be overriden by a correct value
120             else {
121                 _next = null;
122             }
123 
124             return;
125 
126         }
127 
128         // Ignore nodes that are not focusable
129         if (node.castIfAcceptsInput!Focusable is null) return;
130 
131         // Set first and next node to this node
132         if (_first is null) {
133             _first = node;
134         }
135         if (_next is null) {
136             _next = node;
137         }
138 
139         // Mark as the last found focusable
140         _last = node;
141 
142     }
143 
144     override void afterTree() {
145 
146         // Selecting previous or next node
147         result = isReverse
148             ? _previous
149             : _next;
150 
151         // No such node, try first/last
152         if (!isWrapDisabled && result is null) {
153             result = isReverse
154                 ? _last
155                 : _first;
156         }
157 
158         // Found a result!
159         if (auto focusable = cast(Focusable) result) {
160             focusable.focus();
161         }
162 
163         stop;
164 
165     }
166 
167 }
168 
169 
170 /// Find and focus a focusable node based on its visual position; above, below, to the left or to the right
171 /// of a chosen node.
172 ///
173 /// Using this function requires knowing the last position of the node, which isn't usually stored. Depending on
174 /// the usecase, you may need to use `FindFocusBoxAction` earlier.
175 ///
176 /// Nodes are chosen based on semantical weight — nodes within the same container will be prioritized over
177 /// nodes in another. Only if the weight is the same, they will be compared based on their visual distance.
178 ///
179 /// Params:
180 ///     node      = Node to use as reference.
181 ///     focusBox  = Last known `focusBox` of the node.
182 ///     direction = Direction to switch to, if calling `focusDirection`.
183 /// Returns:
184 ///     A tree action which will run during the next frame. You can attach a callback using its `then` method
185 ///     to process the found node.
186 PositionalFocusAction focusAbove(Node node, Rectangle focusBox) {
187     return focusDirection(node, focusBox, Style.Side.top);
188 }
189 
190 /// ditto
191 PositionalFocusAction focusBelow(Node node, Rectangle focusBox) {
192     return focusDirection(node, focusBox, Style.Side.bottom);
193 }
194 
195 /// ditto
196 PositionalFocusAction focusToLeft(Node node, Rectangle focusBox) {
197     return focusDirection(node, focusBox, Style.Side.left);
198 }
199 
200 /// ditto
201 PositionalFocusAction focusToRight(Node node, Rectangle focusBox) {
202     return focusDirection(node, focusBox, Style.Side.right);
203 }
204 
205 /// ditto
206 PositionalFocusAction focusDirection(Node node, Rectangle focusBox, Style.Side direction) {
207 
208     auto action = new PositionalFocusAction(node, focusBox, direction);
209     node.startAction(action);
210     return action;
211 
212 }
213 
214 final class PositionalFocusAction : FocusSearchAction {
215 
216     public {
217 
218         /// Node to use as reference. The action will either select the next node that follows, or the previous.
219         Node target;
220 
221         /// Focus box of the target node.
222         Rectangle focusBox;
223 
224         /// Direction of search.
225         Style.Side direction;
226 
227         /// Focus box of the located node.
228         Rectangle resultFocusBox;
229 
230     }
231 
232     private {
233 
234         // Properties for the match
235         int   resultPriority;   /// Priority assigned to the match.
236         float resultDistance2;  /// Distance
237 
238         /// Priority assigned to the next node, based on the current tree position.
239         int priority;
240 
241         /// Multiplier for changes to priority; +1 when moving towards the target, -1 when moving away from it.
242         /// This assigns higher priority for nodes that are semantically closer to the match.
243         ///
244         /// Priority changes only when depth changes; if two nodes are drawn and they're siblings, priority
245         /// won't change. Priority will only change if the relation is different, e.g. child, cousin, etc.
246         int priorityDirection = 1;
247 
248         /// Current depth.
249         int depth;
250 
251         /// Depth of the last node drawn.
252         int lastDepth;
253 
254     }
255 
256     this() {
257 
258     }
259 
260     this(Node target, Rectangle focusBox, Style.Side direction) {
261         reset(target, focusBox, direction);
262     }
263 
264     /// Re-arm the action.
265     void reset(Node target, Rectangle focusBox, Style.Side direction) {
266         this.result = null;
267         this.target = target;
268         this.focusBox = focusBox;
269         this.direction = direction;
270         this.resultFocusBox = focusBox;
271         clearSubscribers();
272     }
273 
274     override void beforeTree(Node node, Rectangle rectangle) {
275         this.result = null;
276         this.priority = 0;
277         this.priorityDirection = 1;
278         this.depth = 0;
279         this.lastDepth = 0;
280     }
281 
282     override void beforeDraw(Node node, Rectangle) {
283 
284         depth++;
285 
286     }
287 
288     override void afterDraw(Node node, Rectangle, Rectangle, Rectangle inner) {
289 
290         import std.math : abs;
291 
292         depth--;
293 
294         auto focusable = node.castIfAcceptsInput!Focusable;
295 
296         // Set priority
297         priority += priorityDirection * abs(depth - lastDepth);
298         lastDepth = depth;
299 
300         // Ignore nodes that don't accept focus
301         if (!focusable) return;
302 
303         // Found the target, reverse priority direction
304         if (node.opEquals(target)) {
305             priorityDirection = -1;
306             return;
307         }
308 
309         const box = node.focusBox(inner);
310         const dist = distance2(box);
311 
312 
313         // Check if this node matches the direction
314         if (!box.isBeyond(focusBox, direction)) return;
315 
316         // Compare against previous best match
317         if (result) {
318 
319             // Ignore if the other match has higher priority
320             if (resultPriority > priority) return;
321 
322             // If priorities are equal, compare distance
323             if (resultPriority == priority
324                 && resultDistance2 < dist) return;
325 
326         }
327 
328         // Replace the node
329         result = node;
330         resultPriority  = priority;
331         resultDistance2 = dist;
332         resultFocusBox  = box;
333 
334     }
335 
336     override void stopped() {
337 
338         if (auto focusable = cast(Focusable) result) {
339             focusable.focus();
340         }
341 
342         super.stopped();
343 
344     }
345 
346     /// Get the square of the distance between given box and the target's `focusBox`.
347     private float distance2(Rectangle box) {
348 
349         /// Get the center of given rectangle on the axis opposite to the results of getSide.
350         float center(Rectangle rect) {
351 
352             return direction == Style.Side.left || direction == Style.Side.right
353                 ? rect.y + rect.height
354                 : rect.x + rect.width;
355 
356         }
357 
358         // Distance between box sides facing each other, see `checkDirection`
359         const distanceExternal = focusBox.getSide(direction) - box.getSide(direction.reverse);
360 
361         /// Distance between centers of the boxes on the other axis
362         const distanceOpposite = center(box) - center(focusBox);
363 
364         return distanceExternal^^2 + distanceOpposite^^2;
365 
366     }
367 
368 }