1 ///
2 module fluid.hover_chain;
3 
4 import std.array;
5 import std.algorithm;
6 
7 import fluid.node;
8 import fluid.types;
9 import fluid.utils;
10 import fluid.node_chain;
11 
12 import fluid.io.hover;
13 import fluid.io.focus;
14 import fluid.io.action;
15 
16 import fluid.future.arena;
17 import fluid.future.action;
18 
19 @safe:
20 
21 alias hoverChain = nodeBuilder!HoverChain;
22 
23 /// A hover chain can be used to separate hover in different areas of the user interface, effectively treating them
24 /// like separate windows. A device node (like a mouse) can be placed to control nodes inside.
25 ///
26 /// `HoverChain` has to be placed inside `FocusIO` to enable switching focus by pressing nodes.
27 ///
28 /// For focus-based nodes like keyboard and gamepad, see `FocusChain`.
29 ///
30 /// `HoverChain` only works with nodes compatible with the new I/O system introduced in Fluid 0.7.2.
31 class HoverChain : NodeChain, ActionHoverIO {
32 
33     mixin controlIO;
34 
35     ActionIO actionIO;
36     FocusIO focusIO;
37 
38     private {
39 
40         struct Pointer {
41 
42             /// The stored pointer. This is the last pointer assigned by `load`, as given by the device node.
43             /// Event handlers are given `armedValue` instead.
44             HoverPointer value;
45 
46             /// Pointer passed to event handlers. Associated with a negative ID, i.e. if the pointer's ID is `0`,
47             /// the ID of `armedValue` is `-1`, if the main ID is `1`, the ID of `armedValue` is `-2` and so on.
48             HoverPointer armedValue;
49 
50             /// Branch action associated with the pointer; finds the associated node.
51             FindHoveredNodeAction action;
52 
53             /// Node last matched to the pointer. "Hovered" node.
54             Node node;
55 
56             /// Node that is being held, placed under the cursor at the time a button has been pressed.
57             /// Input actions won't fire if the hovered node, the one under the cursor, is different from the one
58             /// that is being held.
59             Node heldNode;
60 
61             /// Scrollable hovered by this pointer, if any.
62             HoverScrollable scrollable;
63 
64             /// If true, any button related to the pointer is being held.
65             ///
66             /// `heldNode` will not be updated while this is true, and current focus will be updated to match
67             /// the hovered node.
68             bool isHeld;
69 
70             /// If true, the pointer already handled incoming input events.
71             bool isHandled;
72 
73             bool opEquals(const HoverPointer pointer) const {
74                 return this.value.isSame(pointer);
75             }
76 
77         }
78 
79         ResourceArena!Pointer _pointers;
80 
81     }
82 
83     this() {
84 
85     }
86 
87     this(Node next) {
88         super(next);
89     }
90 
91     /// Each `HoverPointer` loaded into `HoverChain` has two values, under two different ID numbers.
92     /// This function converts either number into the original one.
93     ///
94     /// Since the IDs are assigned in a consistent, deterministic manner,
95     /// the pointer does not need to be loaded for this function to work.
96     ///
97     /// See_Also:
98     ///     `fetch` for information on the difference between the values.
99     ///     `armedPointerID` for a function to get the ID of the armed pointer.
100     /// Params:
101     ///     number = Pointer ID to normalize, negative or not.
102     /// Returns:
103     ///     The normalized, non-negative pointer number.
104     ///     Returns the same ID as given if it was already normalized.
105     int normalizedPointerID(int number) const {
106 
107         if (number < 0) {
108             return -number - 1;
109         }
110         else {
111             return number;
112         }
113 
114     }
115 
116     /// Performs the opposite of `normalizedPointerID`; gets the ID of the armed pointer, the one made available
117     /// to event handling nodes.
118     ///
119     /// Since the IDs are assigned in a consistent, deterministic manner,
120     /// the pointer does not need to be loaded for this function to work.
121     ///
122     /// See_Also:
123     ///     `normalizedPointerID`
124     /// Params:
125     ///     number = ID of the pointer, either negative or not.
126     /// Returns:
127     ///     The ID of the armed pointer.
128     ///     Returns the same ID as given if it was already armed.
129     int armedPointerID(int number) const {
130 
131         if (number >= 0) {
132             return -number - 1;
133         }
134         else {
135             return number;
136         }
137 
138     }
139 
140     /// Get the armed variant of the pointer.
141     /// Params:
142     ///     pointer = A regular, or armed pointer.
143     /// Returns:
144     ///     Armed pointer corresponding to the given pointer.
145     ///     If the pointer is already an armed variant, no conversion is performed.
146     inout(HoverPointer) armedPointer(HoverPointer pointer) inout {
147         const armed = armedPointerID(pointer.id);
148         return fetch(armed);
149     }
150 
151     override int load(HoverPointer pointer)
152     out(r) {
153         import std.format;
154         debug assert(_pointers.allResources.count(pointer) == 1,
155             format!"Duplicate pointers created: %(\n  %s%)"(_pointers.allResources));
156     }
157     do {
158 
159         const index = cast(int) _pointers.allResources.countUntil(pointer);
160 
161         // No such pointer
162         if (index == -1) {
163             Pointer newPointer;
164             newPointer.value = pointer;
165             newPointer.armedValue.isDisabled = true;
166             newPointer.action = new FindHoveredNodeAction;
167             newPointer.action.stop();  // Temporarily mark as inactive
168 
169             const newIndex = _pointers.load(newPointer);
170             _pointers[newIndex].value.load(this, newIndex);
171             return newIndex;
172         }
173 
174         // Found, update the pointer
175         else {
176             auto updatedPointer = _pointers[index];
177             updatedPointer.value.update(pointer);
178             updatedPointer.value.load(this, index);
179             updatedPointer.armedValue.clickCount = pointer.clickCount;
180             _pointers.reload(index, updatedPointer);
181             return index;
182         }
183 
184     }
185 
186     /// Fetch a pointer by the number assigned to it when loading.
187     ///
188     /// Under the hood, `HoverChain` creates two pointers for each load.
189     /// One has a number of zero or more (the original pointer), and one has a negative number (armed pointer).
190     /// The original pointer reflects the changes made when loading and updating exactly,
191     /// while the armed pointer is updated only when a new frame starts.
192     /// This makes it possible to update the pointer, while it is in use by `FindHoveredNodeAction`.
193     /// Otherwise, the values given to the could be out of date by the time the relevant node is found.
194     ///
195     /// See_Also:
196     ///     `normalizedPointerID` and `armedPointerID` for converting between pointer IDs.
197     /// Returns:
198     ///     Pointer associated with the node.
199     override inout(HoverPointer) fetch(int number) inout {
200 
201         // Armed variant
202         if (number < 0) {
203             const trueNumber = normalizedPointerID(number);
204             assert(_pointers.isActive(trueNumber), "Pointer is not active");
205             return _pointers[trueNumber].armedValue;
206         }
207 
208         // Original variant
209         else {
210             assert(_pointers.isActive(number), "Pointer is not active");
211             return _pointers[number].value;
212         }
213 
214     }
215 
216     override void emitEvent(HoverPointer pointer, InputEvent event) {
217 
218         const id = normalizedPointerID(pointer.id);
219 
220         assert(_pointers.isActive(id), "Pointer is not active");
221 
222         // Mark the pointer as held
223         _pointers[id].isHeld = true;
224 
225         // Emit the event
226         if (actionIO) {
227             actionIO.emitEvent(event, id, &runInputAction);
228         }
229 
230     }
231 
232     override bool isHovered(const Hoverable hoverable) const {
233         foreach (pointer; _pointers.activeResources) {
234             if (hoverable.opEquals(pointer.heldNode)) {
235                 return true;
236             }
237         }
238         return false;
239     }
240 
241     /// List all active pointers controlled by this `HoverChain`.
242     ///
243     /// A copy of each pointer is maintained to pass to event handlers. While iterating,
244     /// only one version will be passed of each pointer: if while drawing,
245     /// the "armed" copy is used, otherwise the regular versions will be returned.
246     ///
247     /// The above distinction makes it possible for nodes to process the same pointers
248     /// as they're given in event handlers, while outsiders are given the usual versions.
249     override int opApply(int delegate(HoverPointer) @safe yield) {
250 
251         foreach (pointer; _pointers.activeResources) {
252 
253             auto value = pointer.action.toStop
254                 ? pointer.value
255                 : pointer.armedValue;
256 
257             // List each pointer
258             if (auto result = yield(value)) {
259                 return result;
260             }
261 
262         }
263 
264         return 0;
265 
266     }
267 
268     override int opApply(int delegate(Hoverable) @safe yield) {
269 
270         foreach (pointer; _pointers.activeResources) {
271 
272             // Skip disabled pointers
273             if (pointer.value.isDisabled) continue;
274 
275             // List each hoverable
276             if (auto hoverable = cast(Hoverable) pointer.heldNode) {
277                 if (auto result = yield(hoverable)) {
278                     return result;
279                 }
280             }
281 
282         }
283 
284         return 0;
285 
286     }
287 
288     override void beforeResize(Vector2) {
289         use(actionIO);
290         use(focusIO);
291         _pointers.startCycle();
292         startIO();
293     }
294 
295     override void afterResize(Vector2) {
296         stopIO();
297     }
298 
299     override void beforeDraw(Rectangle outer, Rectangle inner) {
300 
301         foreach (resource; _pointers.activeResources) {
302 
303             const id = resource.value.id;
304             const armedID = armedPointerID(id);
305 
306             // Update the pointer when done
307             scope (exit) _pointers[id] = resource;
308 
309             // Arm the pointer
310             resource.armedValue = resource.value;
311             resource.armedValue.load(this, armedID);
312 
313             if (resource.value.isDisabled) continue;
314 
315             // Start the tree action
316             resource.action.pointer = resource.armedValue;
317             auto frame = controlBranchAction(resource.action);
318             frame.start();
319             frame.release();
320 
321         }
322 
323     }
324 
325     override void afterDraw(Rectangle outer, Rectangle inner) {
326 
327         auto frame = controlBranchAction(_pointers.activeResources.map!"a.action");
328         frame.stop();
329 
330         // Update hover data
331         foreach (resource; _pointers.activeResources) {
332 
333             auto pointer = resource.armedValue;
334 
335             // Ignore disabled pointers
336             if (pointer.isDisabled) continue;
337 
338             const id = resource.value.id;
339             const armedID = pointer.id;
340             assert(armedID < 0);
341 
342             scope (exit) _pointers[id] = resource;
343 
344             // Keep the same hovered node if the pointer is being held,
345             // otherwise switch.
346             resource.node = resource.action.result;
347             if (!resource.isHeld) {
348                 resource.heldNode = resource.node;
349             }
350 
351             // Switch focus to hovered node if holding
352             else if (focusIO) {
353                 if (auto focusable = resource.heldNode.castIfAcceptsInput!Focusable) {
354                     if (!focusable.isFocused) {
355                         focusable.focus();
356                     }
357                 }
358                 else {
359                     focusIO.clearFocus();
360                 }
361             }
362 
363             // Update scroll and send new events
364             if (!pointer.isScrollHeld) {
365                 resource.scrollable = resource.action.scrollable;
366             }
367             if (resource.scrollable) {
368                 resource.scrollable.scrollImpl(pointer);
369             }
370 
371             // Reset state
372             resource.isHeld = false;
373             resource.isHandled = false;
374 
375             // Send a frame event to trigger hoverImpl
376             if (actionIO) {
377                 actionIO.emitEvent(ActionIO.frameEvent, armedID, &runInputAction);
378             }
379             else if (auto hoverable = resource.heldNode.castIfAcceptsInput!Hoverable) {
380                 resource.isHandled = hoverable.hoverImpl(pointer);
381             }
382 
383         }
384 
385     }
386 
387     override inout(Hoverable) hoverOf(HoverPointer pointer) inout {
388         const id = normalizedPointerID(pointer.id);
389         debug assert(_pointers.isActive(id), "Given pointer wasn't loaded");
390         return _pointers[id].heldNode.castIfAcceptsInput!Hoverable;
391     }
392 
393     override inout(HoverScrollable) scrollOf(HoverPointer pointer) inout {
394         const id = normalizedPointerID(pointer.id);
395         debug assert(_pointers.isActive(id), "Given pointer wasn't loaded");
396         return _pointers[id].scrollable;
397     }
398 
399     override bool runInputAction(HoverPointer pointer, immutable InputActionID actionID,
400         bool isActive = true)
401     do {
402 
403         const id = normalizedPointerID(pointer.id);
404         const armedID = -id - 1;
405         const isFrameAction = actionID == inputActionID!(ActionIO.CoreAction.frame);
406 
407         auto hover = hoverOf(pointer);
408         auto meta = _pointers[id];
409 
410         // Active input actions can only fire if `heldNode` is still hovered
411         if (isActive) {
412             if (meta.node is null || !meta.node.opEquals(meta.heldNode)) {
413                 return false;
414             }
415         }
416 
417         // Mark pointer as held
418         if (!isFrameAction) {
419             _pointers[id].isHeld = true;
420         }
421 
422         // Try to handle the action
423         const handled =
424 
425             // Try to run the action
426             (hover && hover.actionImpl(this, armedID, actionID, isActive))
427 
428             // Run local input actions as fallback
429             || runLocalInputActions(pointer, actionID, isActive)
430 
431             // Run hoverImpl as a last resort
432             || (isFrameAction && hover && hover.hoverImpl(pointer));
433 
434         // Mark as handled, if so
435         _pointers[id].isHandled = meta.isHandled || handled;
436 
437         return handled;
438 
439     }
440 
441     /// ditto
442     bool runInputAction(alias action)(HoverPointer pointer, bool isActive = true) {
443         const id = inputActionID!action;
444         return runInputAction(pointer, id, isActive);
445     }
446 
447     /// ditto
448     protected final bool runInputAction(InputActionID actionID, bool isActive, int number) {
449 
450         auto pointer = fetch(number);
451         return runInputAction(pointer, actionID, isActive);
452 
453     }
454 
455     /// Run an input action implemented by this node. `HoverChain` does not implement any by default.
456     /// Params:
457     ///     pointer  = Pointer associated with the event.
458     ///     actionID = ID of the input action to perform.
459     ///     isActive = If true, the action has been activated during this frame.
460     /// Returns:
461     ///     True if the action was handled, false if not.
462     protected bool runLocalInputActions(HoverPointer pointer, InputActionID actionID,
463         bool isActive = true)
464     do {
465 
466         return false;
467 
468     }
469 
470 }