1 /// This module implements interfaces for handling hover and connecting hoverable nodes with input devices.
2 module fluid.io.hover;
3 
4 import optional;
5 
6 import std.range;
7 
8 import fluid.tree;
9 import fluid.types;
10 
11 import fluid.future.pipe;
12 import fluid.future.context;
13 import fluid.future.branch_action;
14 
15 import fluid.io.action;
16 
17 public import fluid.io.action : InputEvent, InputEventCode;
18 
19 @safe:
20 
21 /// `HoverIO` is an input handler system that reads events off devices with the ability to point at the screen,
22 /// like mouses, touchpads or pens.
23 ///
24 /// Most of the time, `HoverIO` systems will pass the events they receive to an `ActionIO` system, and then send
25 /// these actions to a hovered node.
26 ///
27 /// Multiple different `HoverIO` instances can coexist in the same tree, allowing for multiple different nodes
28 /// to be hovered at the same time, as long as they belong in different branches of the node tree. That means
29 /// two different nodes can be hovered by two different `HoverIO` systems, but a single `HoverIO` system can only
30 /// hover a single node.
31 interface HoverIO : IO {
32 
33     /// Load a hover pointer (mouse cursor, finger) and place it at the position currently
34     /// indicated in the struct. Update the pointer's position if already loaded.
35     ///
36     /// It is expected `load` will be called after every pointer motion in order to keep its
37     /// position up to date.
38     ///
39     /// A pointer is considered loaded until the next resize. If the `load` call for a pointer
40     /// isn't repeated during a resize, the pointer is invalidated. Pointers do not have to be
41     /// loaded while resizing; they can also be loaded while drawing.
42     ///
43     /// An example implementation of a pointer device, from inside of a node, could look like:
44     ///
45     /// ---
46     /// HoverIO hoverIO;
47     /// HoverPointer pointer;
48     ///
49     /// override void resizeImpl(Vector2) {
50     ///     require(hoverIO);
51     ///     minSize = Vector2();
52     /// }
53     ///
54     /// override void drawImpl(Rectangle, Rectangle) {
55     ///     pointer.device = this;
56     ///     pointer.number = 0;
57     ///     pointer.position = mousePosition();
58     ///     load(mouseIO, pointer);
59     ///     if (clicked) {
60     ///         mouseIO.emitEvent(pointer, MouseIO.createEvent(MouseIO.Button.left, true));
61     ///     }
62     /// }
63     /// ---
64     ///
65     /// Params:
66     ///     pointer = Pointer to prepare.
67     ///         The pointer's `device` field should be set to whatever node represents this device,
68     ///         and the `number` field should be set to whatever number the device can associate with the pointer,
69     ///         if multiple pointers are to be used.
70     /// Returns:
71     ///     An ID the `HoverIO` system will use to recognize the pointer.
72     int load(HoverPointer pointer);
73 
74     /// Fetch a pointer from a number assigned to it by this I/O. This is used by `Actionable`
75     /// nodes to find `HoverPointer` data corresponding to fired input action events.
76     ///
77     /// The pointer, and the matching number, must be valid.
78     ///
79     /// Params:
80     ///     number = Number assigned to the pointer by this I/O.
81     /// Returns:
82     ///     The pointer.
83     inout(HoverPointer) fetch(int number) inout;
84 
85     /// Read an input event from an input device. Input devices will call this function every
86     /// frame if an input event (such as a button press) occurs. Moving a mouse does not qualify
87     /// as an input event.
88     ///
89     /// The hover pointer emitting the event must have been loaded earlier (using `load`) during
90     /// the same frame for the action to work.
91     ///
92     /// `HoverIO` will usually pass these down to an `ActionIO` system. It is up to `HoverIO` to
93     /// decide how the input and the resulting input actions is handled, though the node hovered
94     /// by the pointer will most often receive them.
95     ///
96     /// Params:
97     ///     pointer = Pointer that emitted the event.
98     ///     event   = Input event the system should emit.
99     ///         The event is usually considered "active" during the frame the action is
100     ///         "released". For example, user stops holding a mouse button, or a finger stops
101     ///         touching the screen.
102     void emitEvent(HoverPointer pointer, InputEvent event);
103 
104     /// Params:
105     ///     pointer = Pointer to query. The pointer must be loaded.
106     /// Returns:
107     ///     Node hovered by the hover pointer.
108     /// See_Also:
109     ///     `scrollOf` to get the current scrollable node.
110     inout(Hoverable) hoverOf(HoverPointer pointer) inout;
111 
112     /// Params:
113     ///     pointer = Pointer to query. The pointer must be loaded.
114     /// Returns:
115     ///     Scrollable ancestor for the currently hovered node.
116     /// See_Also:
117     ///     `hoverOf` to get the currently hovered node.
118     inout(HoverScrollable) scrollOf(HoverPointer pointer) inout;
119 
120     /// Returns:
121     ///     True if the node is hovered.
122     /// Params:
123     ///     hoverable = True if this node is hovered.
124     bool isHovered(const Hoverable hoverable) const;
125 
126     /// List all active hover pointer, namely all pointers that have been loaded since the last
127     /// resize.
128     ///
129     /// Pointers do not need to be sorted.
130     ///
131     /// Params:
132     ///     yield = A delegate to be called for every active node.
133     ///         Disabled nodes should be included.
134     ///         If the delegate returns a non-zero value, it should immediately break out
135     ///         of the loop and return this value.
136     /// Returns:
137     ///     If `yield` returned a non-zero value, it should be returned;
138     ///     if `yield` wasn't called, or has only returned zeroes, a zero is returned.
139     int opApply(int delegate(HoverPointer) @safe yield);
140 
141     /// List all currently hovered nodes.
142     ///
143     /// Nodes do not need to be sorted.
144     ///
145     /// Params:
146     ///     yield = A delegate to be called for every hovered node.
147     ///         This should include nodes that block input, but are hovered.
148     ///         If the delegate returns a non-zero value, the value should be immediately returned.
149     /// Returns:
150     ///     If `yield` returned a non-zero value, this is the value it returned;
151     ///     if `yield` wasn't called, or has only returned zeroes, a zero is returned.
152     int opApply(int delegate(Hoverable) @safe yield);
153 
154 }
155 
156 /// An extension of `HoverIO` that enables support for dispatching and running input actions.
157 interface ActionHoverIO : HoverIO {
158 
159     /// Handle an input action associated with a pointer.
160     /// Params:
161     ///     pointer  = Pointer to send the input action. It must be loaded.
162     ///         The input action will be loaded by the node the pointer points at.
163     ///     actionID = ID of the input action.
164     ///     isActive = If true, the action has been activated during this frame.
165     /// Returns:
166     ///     True if the input action was handled.
167     bool runInputAction(HoverPointer pointer, immutable InputActionID actionID,
168         bool isActive = true);
169 
170     /// ditto
171     bool runInputAction(alias action)(HoverPointer pointer, bool isActive = true) {
172         const id = inputActionID!action;
173         return runInputAction(pointer, id, isActive);
174     }
175 
176 }
177 
178 /// Returns:
179 ///     True if the `hoverIO` is hovering some node.
180 /// Params:
181 ///     hoverIO   = HoverIO to test.
182 ///     hoverable = Node that HoverIO is expected to hover.
183 bool hovers(HoverIO hoverIO) {
184 
185     foreach (Hoverable _; hoverIO) {
186         return true;
187     }
188     return false;
189 
190 }
191 
192 /// ditto
193 bool hovers(HoverIO hoverIO, const Hoverable hoverable) {
194 
195     foreach (Hoverable hovered; hoverIO) {
196         if (hovered.opEquals(cast(const Object) hoverable)) return true;
197     }
198 
199     return false;
200 
201 }
202 
203 /// Test if the hover I/O system hovers all of the nodes and none other.
204 /// Params:
205 ///     hoverIO    = Hover I/O system to test. Hoverables reported by this system will be checked.
206 ///     hoverables = A forward range of hoverables.
207 /// Returns:
208 ///     True if the system considers all of the given ranges as hovered,
209 ///     and it does not find any other nodes hovered.
210 bool hoversOnly(Range)(HoverIO hoverIO, Range hoverables)
211 if (isForwardRange!Range && is(ElementType!Range : const Hoverable))
212 do {
213 
214     import std.algorithm : canFind;
215 
216     foreach (Hoverable hovered; hoverIO) {
217 
218         // A node is hovered, but not in the known list
219         if (!hoverables.canFind!((a, b) => a.opEquals(cast(const Object) b))(hovered)) {
220             return false;
221         }
222 
223     }
224 
225     foreach (hoverable; hoverables) {
226 
227         // A hoverable is not hovered
228         if (!hoverIO.hovers(hoverable)) {
229             return false;
230         }
231 
232     }
233 
234     return true;
235 
236 }
237 
238 /// A pointer is a position on the screen chosen by the user using a mouse, touchpad, touchscreen or other device
239 /// capable of communicating some position.
240 ///
241 /// While in a typical desktop application there will usually be a single pointer at a time, there can be cases
242 /// where there may be none (no mouse connected) or more (multitouch-enabled screen, multiple mouses connected, etc.)
243 ///
244 /// A pointer is associated with an I/O system that represents the device that invoked the pointer.
245 /// This may be a dedicated mouse node, but it may also be a generic system that abstracts the device away;
246 /// for example, Raylib provides a singular function for getting the mouse position without distinguishing
247 /// between multiple devices or touchscreens.
248 ///
249 /// For a pointer to work, it has to be loaded into a `HoverIO` system using its `load` method. This has to be done
250 /// once a frame for as long as the pointer is active. This will be every frame for a mouse (if one is connected),
251 /// or only the frames a finger is touching the screen for a touchscreen.
252 ///
253 /// See_Also:
254 ///     `HoverIO`, `HoverIO.load`
255 struct HoverPointer {
256 
257     // As a workaround for a DMD codegen bug on Windows, HoverPointer must not be zero initialized.
258     // See https://git.samerion.com/Samerion/Fluid/pulls/357#issuecomment-3309 for more details.
259     static assert(!__traits(isZeroInit, HoverPointer));
260 
261     /// I/O system that represents the device controlling the pointer.
262     IO device;
263 
264     /// If the device can control multiple pointers (like a touchscreen), this number should uniquely identify
265     /// a pointer.
266     int number;
267 
268     /// Position in the window the pointer is pointing at.
269     Vector2 position;
270 
271     /// Current scroll value. For a mouse, this indicates mouse wheel movement, for other devices
272     /// like touchpad or touchscreen, this will be translated from its movement.
273     ///
274     /// This value indicates the distance and direction in window space that the scroll should
275     /// result in covering. This means that on the X axis negative values move left and positive
276     /// values move right, while on the Y axis negative values go upwards and positive values go
277     /// downwards. For example, a scroll value of `(0, 20)` scrolls 20 pixels down vertically,
278     /// while `(0, -10)` scrolls 10 pixels up.
279     ///
280     /// While it is possible to read scroll of the `HoverPointer` data received in an input action
281     /// handler, it is recommended to implement scroll through `Scrollable.scrollImpl`.
282     ///
283     /// Scroll is exposed for both the horizontal and vertical axis. While a basic mouse wheel
284     /// only supports vertical movement, touchscreens, touchpads, trackpads or more advanced
285     /// mouses do support horizontal movement. It is also possible for a device to perform both
286     /// horizontal and vertical movement at once.
287     Vector2 scroll;
288 
289     /// True if the pointer is not currently pointing, like a finger that stopped touching the screen.
290     bool isDisabled;
291 
292     /// Consecutive click counter. A value of 1 represents a single click, 2 is a double click, 3 is a triple click,
293     /// and so on. The counter should reset after a small delay, or if a distance threshold is crossed.
294     ///
295     /// This value is usually provided by the system. If unavailable, you can use
296     /// `fluid.io.preference.MultipleClickCounter` to generate this value from data available to Fluid.
297     int clickCount;
298 
299     /// If true, the scroll control is held, like a finger swiping through the screen. This does not apply to mouse
300     /// wheels.
301     ///
302     /// If scroll is "held," the scrolling motion should detach from pointer position. Whatever scrollable was
303     /// selected at the time scroll was pressed should continue to be selected while held even if the pointer moves
304     /// away from it. This makes it possible to comfortably scroll with a touchscreen without having to mind node
305     /// boundaries, or to implement features such as [autoscroll].
306     ///
307     /// [autoscroll]: (https://chromewebstore.google.com/detail/autoscroll/occjjkgifpmdgodlplnacmkejpdionan)
308     bool isScrollHeld;
309 
310     /// `HoverIO` system controlling the pointer.
311     private HoverIO _hoverIO;
312 
313     /// ID of the pointer assigned by the `HoverIO` system.
314     private int _id = -1;
315 
316     /// If the given system is a Hover I/O system, fetch a hover pointer.
317     ///
318     /// Given data must be valid; the I/O must be a `HoverIO` instance and the number must be a
319     /// valid pointer number.
320     ///
321     /// Params:
322     ///     io     = I/O system to use.
323     ///     number = Valid pointer number assigned by the I/O system.
324     /// Returns:
325     ///     Hover pointer under given number.
326     static Optional!HoverPointer fetch(IO io, int number) {
327 
328         import std.format;
329 
330         if (auto hoverIO = cast(HoverIO) io) {
331             return typeof(return)(
332                 hoverIO.fetch(number));
333         }
334 
335         return typeof(return).init;
336 
337     }
338 
339     /// Compare two pointers. All publicly exposed fields (`device`, `number`, `position`,
340     /// `isDisabled`) must be equal. To check if the two pointers have the same origin (device and
341     /// number), use `isSame`.
342     /// Params:
343     ///     other = Pointer to compare against.
344     /// Returns:
345     ///     True if the pointer is the same as the other pointer and has the same state.
346     bool opEquals(const HoverPointer other) const {
347 
348         // Do not compare I/O metadata last two fields
349         // Do not compare device (for old compilers), use isSame instead
350         return this.tupleof[1..$-2] == other.tupleof[1..$-2]
351             && isSame(other);
352 
353     }
354 
355     /// Test if the two pointers have the same origin — same device and pointer number.
356     /// Params:
357     ///     other = Pointer to compare against.
358     /// Returns:
359     ///     True if the pointers have the same device and pointer number.
360     bool isSame(const HoverPointer other) const {
361 
362         if (device is null) {
363             return other.device is null
364                 && number == other.number;
365         }
366 
367         return device.opEquals(cast(const Object) other.device)
368             && number == other.number;
369 
370     }
371 
372     /// Returns: The ID/index assigned by `HoverIO` when this pointer was loaded.
373     int id() const nothrow {
374         return this._id;
375     }
376 
377     /// Returns: The I/O system owning the pointer.
378     inout(HoverIO) system() inout nothrow {
379         return this._hoverIO;
380     }
381 
382     /// Load the pointer into the system.
383     void load(HoverIO hoverIO, int id) nothrow {
384         this._hoverIO = hoverIO;
385         this._id = id;
386     }
387 
388     inout(HoverPointer) loadCopy(inout HoverIO hoverIO, int id) inout {
389         return inout HoverPointer(
390             this.device,
391             this.number,
392             this.position,
393             this.scroll,
394             this.isDisabled,
395             this.clickCount,
396             this.isScrollHeld,
397             hoverIO,
398             id,
399         );
400     }
401 
402     /// Update a pointer in place using data of another pointer.
403     /// Params:
404     ///     other = Pointer to copy data from.
405     void update(const HoverPointer other) {
406         this.position     = other.position;
407         this.scroll       = other.scroll;
408         this.isScrollHeld = other.isScrollHeld;
409         this.isDisabled   = other.isDisabled;
410         this.clickCount   = other.clickCount;
411     }
412 
413     /// Emit an event through the pointer.
414     ///
415     /// The device should call this every frame an input event associated with the pointer occurs. This will be
416     /// when a mouse button is pressed, every frame a finger touches the screen, or when a gesture recognized
417     /// by the device or system is performed.
418     ///
419     /// Params:
420     ///     event = Event to emit.
421     ///         The event is usually considered "active" during the frame the action is "released". For example,
422     ///         user stops holding a mouse button, or a finger stops touching the screen.
423     /// See_Also:
424     ///     `HoverIO.emitEvent`
425     void emitEvent(InputEvent event) {
426 
427         _hoverIO.emitEvent(this, event);
428 
429     }
430 
431 }
432 
433 /// Nodes implementing this interface can be selected by a `HoverIO` system.
434 interface Hoverable : Actionable {
435 
436     /// Handle input. Called each frame when focused.
437     ///
438     /// Do not call this method if the `blocksInput` is true.
439     ///
440     /// Params:
441     ///     pointer = Pointer to handle this input.
442     /// Returns:
443     ///     True if hover was handled, false if it was ignored.
444     bool hoverImpl(HoverPointer pointer)
445     in (!blocksInput, "This node currently doesn't accept input.");
446 
447     /// Returns:
448     ///     True if this node is hovered.
449     ///     This will most of the time be equivalent to `hoverIO.isHovered(this)`,
450     ///     but a node wrapping another hoverable may choose to instead redirect this to the other node.
451     bool isHovered() const;
452 
453 }
454 
455 /// Nodes implementing this interface can react to scroll motion if selected by a `HoverIO` system.
456 ///
457 /// Temporarily called `HoverScrollable`, this node will be renamed to `Scrollable` in a future release.
458 /// https://git.samerion.com/Samerion/Fluid/issues/278
459 interface HoverScrollable {
460 
461     import fluid.node;
462 
463     /// Determines whether this node can accept scroll input and if the input can have visible
464     /// effect. This is usually determined by the node's position; for example a container node
465     /// already scrolled to the bottom cannot accept further vertical movement down.
466     ///
467     /// This property is used to determine which node should be used to accept scroll. If there's
468     /// a scrollable container nested in another scrollable node, it will be chosen for scrolling
469     /// only if the scroll motion can still be performed. On the other hand, if the intent is
470     /// specifically to block scroll motion (like in a modal window), this method should always
471     /// return true.
472     ///
473     /// Note that a node "can scroll" even if it can only accept part of the motion. If the scroll
474     /// would have the node scroll beyond its maximum value, but the node is not already at its
475     /// maximum, it should accept the input and clamp the value.
476     ///
477     /// Params:
478     ///     pointer = `HoverPointer` used to perform the action. Its most important property
479     ///         is the `scroll` field, but some nodes may also filter based on the pointer's
480     ///         state.
481     /// Returns:
482     ///     True if the node can accept the scroll value in part or in whole,
483     ///     false if the motion would have no effect.
484     bool canScroll(const HoverPointer pointer) const;
485 
486     /// Perform a scroll motion, moving the node's contents by the specified distance.
487     ///
488     /// Params:
489     ///     pointer = Pointer to use to perform the scroll.
490     /// Returns:
491     ///     True if the motion was handled, or false if not.
492     bool scrollImpl(HoverPointer pointer);
493 
494     /// Scroll towards a specified child node, trying to get it into view.
495     ///
496     /// Params:
497     ///     child     = Target node, a child of this node. Ideally, this node should appear on
498     ///         screen as a consequence of this action.
499     ///     parentBox = Padding box of this node, the node performing the scroll.
500     ///     childBox  = Known padding box of the target child node.
501     /// Returns:
502     ///     A new padding box for the child node after applying scroll.
503     Rectangle shallowScrollTo(const Node child, Rectangle parentBox, Rectangle childBox);
504 
505     /// Memory safe and `const` object comparison.
506     /// Returns:
507     ///     True if this, and the other object, are the same object.
508     /// Params:
509     ///     other = Object to compare to.
510     bool opEquals(const Object other) const;
511 
512 }
513 
514 /// Cast the node to given type if it accepts scroll.
515 ///
516 /// In addition to performing a dynamic cast, this checks if the node can handle a specified
517 /// scroll value according to its `HoverScrollable.canScroll` method. If it doesn't, it will fail
518 /// the cast.
519 ///
520 /// Params:
521 ///     node    = Node to cast.
522 ///     pointer = Pointer used for scrolling. Different values may be accepted depending on the
523 ///         scroll value or position.
524 /// Returns:
525 ///     Node casted to `Scrollable`, or null if the node can't be casted, or the motion would
526 ///     not have effect.
527 inout(HoverScrollable) castIfAcceptsScroll(inout Object node, HoverPointer pointer) {
528 
529     // Perform the cast
530     if (auto scrollable = cast(inout HoverScrollable) node) {
531 
532         // Node must accept scroll
533         if (scrollable.canScroll(pointer)) {
534             return scrollable;
535         }
536 
537     }
538 
539     return null;
540 
541 }
542 
543 /// Find the topmost node that occupies the given position on the screen, along with its scrollable.
544 ///
545 /// The result may change while the search runs; the final result is available once the action stops.
546 ///
547 /// On top of finding the node at specified position, a scroll value can be passed through the
548 /// pointer so this action will also find any `Scrollable` ancestor present in the branch, if one
549 /// can handle the motion. The hovered node and the scrollable node can be the same.
550 ///
551 /// For backwards compatibility, this node is not currently registered as a `NodeSearchAction` and
552 /// does not emit a node when done.
553 final class FindHoveredNodeAction : BranchAction {
554 
555     import fluid.node;
556 
557     public {
558 
559         /// If a node was found, this is the result.
560         Node result;
561 
562         /// Topmost scrollable ancestor of `result` (the chosen node).
563         HoverScrollable scrollable;
564 
565         /// Position to use for the lookup. The pointer determines the current position and scroll
566         /// value of interest.
567         HoverPointer pointer;
568 
569     }
570 
571     private {
572         int _transparentDepth;
573 
574         /// Incremented for every beforeDraw, decremented in afterDraw; never negative. Reset to
575         /// zero as soon as a new `result` is found, so that all ancestors have a zero value.
576         /// Non-zero for every sibling node of `result` or siblings of its ancestors.
577         ///
578         /// Used to find `scrollable`: scrollable nodes are only checked if _siblingDepth is
579         /// zero.
580         int _siblingDepth;
581         invariant(_siblingDepth >= 0);
582     }
583 
584     this(HoverPointer pointer = HoverPointer.init) {
585         this.pointer = pointer;
586     }
587 
588     override void started() {
589         super.started();
590         this.result = null;
591         this.scrollable = null;
592         this._transparentDepth = 0;
593     }
594 
595     /// Test if the searched position is within the bounds of this node, and set it as the result
596     /// if so. Any previously found result is overridden.
597     ///
598     /// If a node is found, `scrollable` is cleared. A new one will be found in `afterDraw`.
599     ///
600     /// Because of how layering works in Fluid, the last node in bounds will be the result. This
601     /// action cannot quit early as any node can override the current hover.
602     override void beforeDraw(Node node, Rectangle, Rectangle outer, Rectangle inner) {
603 
604         _siblingDepth++;
605 
606         if (_transparentDepth) {
607             _transparentDepth++;
608             return;
609         }
610 
611         const inBounds = node.inBounds(outer, inner, pointer.position);
612 
613         // Children cannot be hovered
614         if (!inBounds.inChildren) {
615             _transparentDepth++;
616         }
617 
618         // Check if the position is in bounds of the node
619         if (!inBounds.inSelf) return;
620 
621         // Save the result
622         result = node;
623 
624         // Clear scrollable
625         scrollable = null;
626         _siblingDepth = 0;
627 
628         // Do not stop; the result may be overridden
629 
630     }
631 
632     /// Find a matching scrollable for the node. The topmost ancestor of `result` (the chosen
633     /// node) will be used.
634     override void afterDraw(Node node, Rectangle) {
635 
636         scope (exit) {
637             if (_siblingDepth != 0) {
638                 _siblingDepth--;
639             }
640         }
641 
642         // A result is required and no scrollable could have matched already
643         if (result is null) return;
644         if (scrollable) return;
645 
646         // Reduce _transparentDepth, skip unless the node that started it
647         if (_transparentDepth != 0) {
648             _transparentDepth--;
649             if (_transparentDepth != 0) return;
650         }
651 
652         // Ignore if in a sibling node
653         if (_siblingDepth != 0) return;
654 
655         // Try to match this node
656         scrollable = node.castIfAcceptsScroll(pointer);
657 
658     }
659 
660 }
661 
662 /// Create a virtual Hover I/O pointer for testing, and place it at the given position.
663 /// Interactions on the pointer are asynchronous and should be performed by `then` chains, see
664 /// `fluid.future.pipe`.
665 ///
666 /// The pointer is disabled after every interaction, but it will be automatically re-enabled after
667 /// every movement.
668 ///
669 /// See_Also:
670 ///     `pointAndClick`
671 /// Params:
672 ///     hoverIO  = Hover I/O system to target.
673 ///     position = Position to place the pointer at.
674 ///     x        = X position to place the pointer at.
675 ///     y        = Y position to place the pointer at.
676 /// Returns:
677 ///     An instance of `HoverPointerAction`.
678 HoverPointerAction point(HoverIO hoverIO, Vector2 position) {
679     auto action = new HoverPointerAction(hoverIO);
680     action.move(position);
681     return action;
682 }
683 
684 /// ditto
685 HoverPointerAction point(HoverIO hoverIO, float x, float y) {
686     return point(hoverIO, Vector2(x, y));
687 }
688 
689 /// Create a virtual Hover I/O pointer and use it to click a given position. This is a helper
690 /// wrapping `point`.
691 ///
692 /// "Clicking" is equivalent to sending a `FluidInputAction.press` event.
693 ///
694 /// See_Also:
695 ///     `point`
696 /// Params;
697 ///     hoverIO    = Hover I/O system to target.
698 ///     position   = Position to click.
699 ///     x          = X position to click.
700 ///     y          = Y position to click.
701 ///     isActive   = If true (default), sends an active event.
702 ///     clickCount = If set to 2, imitate a double click, if 3, a triple click and so on.
703 /// Returns:
704 ///     A `Publisher` that produces `HoverPointerAction`.
705 Publisher!HoverPointerAction pointAndClick(HoverIO hoverIO, Vector2 position,
706     bool isActive = true, int clickCount = 1)
707 do {
708     return hoverIO.point(position)
709         .then((a) {
710             a.click(isActive, clickCount);
711             return a;
712         });
713 }
714 
715 /// ditto
716 Publisher!HoverPointerAction pointAndClick(HoverIO hoverIO, float x, float y,
717     bool isActive = true, int clickCount = 1)
718 do {
719     return pointAndClick(hoverIO, Vector2(x, y), isActive, clickCount);
720 }
721 
722 /// Virtual Hover I/O pointer, for testing.
723 class HoverPointerAction : TreeAction, Publisher!HoverPointerAction, IO {
724 
725     import fluid.node;
726 
727     public {
728 
729         /// Pointer this action operates on.
730         HoverPointer pointer;
731 
732     }
733 
734     private {
735 
736         /// Hover I/O interface the action interacts with.
737         HoverIO hoverIO;
738 
739         /// Hover I/O casted to a node
740         Node _node;
741 
742         Event!HoverPointerAction _onInteraction;
743 
744     }
745 
746     alias then = typeof(super).then;
747     alias then = Publisher!HoverPointerAction.then;
748 
749     this(HoverIO hoverIO) {
750 
751         this.hoverIO = hoverIO;
752         this._node = cast(Node) hoverIO;
753         this.pointer.device = this;
754         assert(_node, "Given Hover I/O is not a valid node");
755 
756     }
757 
758     override bool opEquals(const Object other) const {
759         return this is other;
760     }
761 
762     override inout(TreeContext) treeContext() inout {
763         return hoverIO.treeContext;
764     }
765 
766     void subscribe(Subscriber!HoverPointerAction subscriber) {
767         _onInteraction ~= subscriber;
768     }
769 
770     override void clearSubscribers() {
771         super.clearSubscribers();
772         _onInteraction.clearSubscribers();
773     }
774 
775     /// Returns:
776     ///     Currently hovered node, if any.
777     Hoverable currentHover() {
778 
779         hoverIO.loadTo(pointer);
780         return hoverIO.hoverOf(pointer);
781 
782     }
783 
784     /// Returns:
785     ///     Chosen scrollable, if any.
786     HoverScrollable currentScroll() {
787 
788         hoverIO.loadTo(pointer);
789         return hoverIO.scrollOf(pointer);
790 
791     }
792 
793     /// Returns: True if the given node is hovered.
794     bool isHovered(const Hoverable hoverable) {
795 
796         if (hoverable is null)
797             return currentHover is null;
798         else
799             return currentHover && currentHover.opEquals(cast(const Object) hoverable);
800 
801     }
802 
803     /// Don't move the pointer, but keep it active.
804     /// Returns: This action, for chaining.
805     HoverPointerAction stayIdle() return {
806 
807         clearSubscribers();
808         _node.startAction(this);
809 
810         // Place the pointer
811         pointer.isDisabled = false;
812         hoverIO.loadTo(pointer);
813 
814         return this;
815 
816     }
817 
818     /// Move the pointer to given position.
819     /// Params:
820     ///     position = Position to move the pointer to.
821     /// Returns:
822     ///     This action, for chaining.
823     HoverPointerAction move(Vector2 position) return {
824 
825         clearSubscribers();
826         _node.startAction(this);
827 
828         // Place the pointer
829         pointer.isDisabled = false;
830         pointer.position = position;
831         hoverIO.loadTo(pointer);
832 
833         return this;
834 
835     }
836 
837     /// ditto
838     HoverPointerAction move(float x, float y) return {
839 
840         return move(Vector2(x, y));
841 
842     }
843 
844     /// Set a scroll value for the action.
845     ///
846     /// Once the motion is completed, the scroll value will be reset and will not apply for future actions.
847     ///
848     /// Params:
849     ///     motion = Distance to scroll.
850     /// Returns:
851     ///     This action, for chaining.
852     HoverPointerAction scroll(Vector2 motion) return {
853 
854         clearSubscribers();
855         _node.startAction(this);
856 
857         // Place the pointer
858         pointer.isDisabled = false;
859         pointer.scroll = motion;
860         hoverIO.loadTo(pointer);
861 
862         return this;
863 
864     }
865 
866     /// ditto
867     HoverPointerAction scroll(float x, float y) return {
868 
869         return scroll(Vector2(x, y));
870 
871     }
872 
873     /// Hold the scroll control in place. This makes it possible to continue scrolling a single node while
874     /// moving the cursor, which is commonly the scrolling behavior of touchscreens.
875     ///
876     /// The hold status will be reset after a frame.
877     HoverPointerAction holdScroll(Vector2 motion, bool value = true) return {
878         pointer.isScrollHeld = value;
879         return scroll(motion);
880     }
881 
882     /// ditto
883     HoverPointerAction holdScroll(float x, float y, bool value = true) return {
884         return holdScroll(Vector2(x, y), value);
885     }
886 
887     /// Run an input action on the currently hovered node, if any.
888     ///
889     /// For this to work, `HoverIO` this pointer operates on must also support `ActionHoverIO`.
890     ///
891     /// Params:
892     ///     actionID   = ID of the action to run.
893     ///     isActive   = "Active" status of the action.
894     /// Returns:
895     ///     True if the action was handled, false if not.
896     bool runInputAction(immutable InputActionID actionID, bool isActive = true) {
897 
898         hoverIO.loadTo(pointer);
899         auto hoverable = hoverIO.hoverOf(pointer);
900 
901         auto actionHoverIO = cast(ActionHoverIO) hoverIO;
902         assert(actionHoverIO, "This HoverIO does not support dispatching input actions.");
903 
904         // Emit a matching, fake hover event
905         // If HoverIO uses ActionIO, ActionIO should recognize and prioritize this event
906         const event = ActionIO.noopEvent(isActive);
907         hoverIO.emitEvent(pointer, event);
908 
909         return actionHoverIO.runInputAction(pointer, actionID, isActive);
910 
911     }
912 
913     /// ditto
914     bool runInputAction(alias action)(bool isActive = true) {
915 
916         alias actionID = inputActionID!action;
917 
918         return runInputAction(actionID, isActive);
919 
920     }
921 
922     /// Perform a left click.
923     /// Params:
924     ///     isActive   = Trigger input actions (like a mouse release event) if true, emulate holding if false.
925     ///     clickCount = Set to 2 to emulate a double click, 3 to emulate a triple click, etc.
926     /// Returns:
927     ///     True if the action was handled, false if not.
928     bool click(bool isActive = true, int clickCount = 1) {
929 
930         pointer.clickCount = clickCount;
931         return runInputAction!(FluidInputAction.press)(isActive);
932 
933     }
934 
935     /// Perform a double (`doubleClick`) or triple click (`tripleClick`) using the primary press action.
936     /// Params:
937     ///     isActive = Trigger input actions (like a mouse release event) if true, emulate holding if false.
938     /// Returns:
939     ///     True if the action was handled, false if not.
940     bool doubleClick(bool isActive = true) {
941         return click(isActive, 2);
942     }
943 
944     /// ditto
945     bool tripleClick(bool isActive = true) {
946         return click(isActive, 3);
947     }
948 
949     alias press = click;
950 
951     override void beforeDraw(Node node, Rectangle) {
952 
953         // Make sure the pointer is loaded and up to date
954         // If the action was scheduled before a resize, the pointer would die during it
955         if (hoverIO.opEquals(node)) {
956             hoverIO.loadTo(pointer);
957         }
958 
959     }
960 
961     override void stopped() {
962 
963         super.stopped();
964 
965         // Disable the pointer and clear scroll
966         pointer.isDisabled = true;
967         pointer.scroll = Vector2();
968         pointer.isScrollHeld = false;
969         pointer.clickCount = 0;
970         hoverIO.loadTo(pointer);
971 
972         _onInteraction(this);
973 
974     }
975 
976 }