1 /// Enables transforming the mouse position from one space to another by translating and scaling.
2 ///
3 /// Requires the new I/O system introduced in Fluid 0.7.2 to be used.
4 ///
5 /// History:
6 ///     * Introduced in Fluid 0.7.2.
7 module fluid.hover_transform;
8 
9 import std.array;
10 import std.string;
11 import std.algorithm;
12 
13 import fluid.node;
14 import fluid.types;
15 import fluid.utils;
16 import fluid.structs;
17 import fluid.node_chain;
18 
19 import fluid.future.arena;
20 import fluid.future.context;
21 
22 import fluid.io.hover;
23 import fluid.io.focus;
24 import fluid.io.action;
25 
26 @safe:
27 
28 ///
29 alias hoverTransform = nodeBuilder!HoverTransform;
30 
31 /// Implements `HoverIO` by transforming inputs from a "host" Hover I/O system. `HoverTransform`
32 /// must be placed as a child of a host I/O system to function.
33 ///
34 /// This node is most useful when Fluid's graphical output is transformed in post-processing.
35 /// For example, Raylib users may render the user interface inside a render texture. In such
36 /// situation, the mouse input would not match what is seen by the user.
37 ///
38 /// `HoverTransform` creates a barrier between inside — its own children — and nodes outside.
39 /// Inputs received from the host are transformed for all of its children, but remain unmodified
40 /// outside. On the other hand, inputs created inside are untouched for other transformed nodes,
41 /// but are inversely transformed for nodes outside.
42 ///
43 /// ---
44 /// hoverChain(
45 ///     vspace(
46 ///         .layout!"fill",
47 ///         button("I receive unmodified inputs", delegate { }),
48 ///         hoverTransform(
49 ///             .layout!(1, "fill"),
50 ///             Rectangle(0, 0, 100, 100),
51 ///             button("I'm transformed", delegate { })
52 ///         ),
53 ///     ),
54 /// ),
55 /// ---
56 ///
57 /// Instead of managing its own set of `HoverPointer` instances, `HoverTransform` uses the host
58 /// Hover I/O system for this task. For every pointer inside the host, a transformed version
59 /// exists in this system. Conversely, a pointer created inside `HoverTransform` will have
60 /// an inversely transformed version in the host system.
61 ///
62 /// Note that the host system will not know that the transform has taken place. If a transformed
63 /// node is hovered, the host will report that the *hover transform* node itself is hovered.
64 /// To see which node is hovered, use `HoverTransform`'s `hoverOf`.
65 class HoverTransform : NodeChain, HoverIO, Focusable, Hoverable, HoverScrollable {
66 
67     // This node is WAY too complex right now
68 
69     mixin controlIO;
70 
71     HoverIO hoverIO;
72     FocusIO focusIO;
73 
74     public {
75 
76         /// By default, the destination rectangle is automatically updated to match the padding
77         /// box of the transform's child node. If toggled on, it is instead static, and can be
78         /// manually updated.
79         bool isDestinationManual;
80 
81     }
82 
83     private {
84 
85         Rectangle _sourceRectangle;
86         Rectangle _destinationRectangle;
87 
88         /// Pointers received from the host.
89         ResourceArena!Pointer _pointers;
90 
91         /// Pool of actions that are used to find matching nodes.
92         FindHoveredNodeAction[] _actions;
93 
94     }
95 
96     /// Params:
97     ///     sourceRectangle      = Rectangle the input is expected to fit in.
98     ///     destinationRectangle = Rectangle to map the input to. If omitted, chosen automatically
99     ///         so that input is remapped to the content of this node.
100     ///     next                 = Node to be affected by the transform.
101     this(Rectangle sourceRectangle, Node next = null) {
102         this._sourceRectangle = sourceRectangle;
103         this.isDestinationManual = false;
104         super(next);
105     }
106 
107     /// ditto
108     this(Rectangle sourceRectangle, Rectangle destinationRectangle, Node next = null) {
109         this._sourceRectangle = sourceRectangle;
110         this._destinationRectangle = destinationRectangle;
111         this.isDestinationManual = true;
112         super(next);
113     }
114 
115     /// Returns:
116     ///     Rectangle for hover input. This input will be transformed to match
117     ///     `destinationRectangle`.
118     Rectangle sourceRectangle() const {
119         return _sourceRectangle;
120     }
121 
122     /// Change the source rectangle.
123     /// Params:
124     ///     newValue = New value for the rectangle.
125     /// Returns:
126     ///     Same value as passed.
127     Rectangle sourceRectangle(Rectangle newValue) {
128         return _sourceRectangle = newValue;
129     }
130 
131     /// Returns:
132     ///     Rectangle for output. By default, this should match the padding box of this node,
133     ///     unless explicitly changed to something else.
134     Rectangle destinationRectangle() const {
135         if (tree)
136             return _destinationRectangle;
137         else
138             return sourceRectangle;
139     }
140 
141     /// Change the destination rectangle, disabling automatic destination selection.
142     ///
143     /// Changing destination rectangle sets `isDestinationManual` to `true`. Set it to false if
144     /// you want the destination rectangle to match the node's padding box instead.
145     ///
146     /// See_Also:
147     ///     `isDestinationManual`
148     Rectangle destinationRectangle(Rectangle newValue) {
149         isDestinationManual = true;
150         return _destinationRectangle = newValue;
151     }
152 
153     /// Transform a point in `sourceRectangle` onto `destinationRectangle`.
154     /// See_Also:
155     ///     `pointToHost` for the reverse transformation.
156     /// Params:
157     ///     point = Point, in host space, to transform.
158     /// Returns:
159     ///     Point transformed into local space.
160     Vector2 pointToLocal(Vector2 point) const {
161         return point.viewportTransform(sourceRectangle, destinationRectangle);
162     }
163 
164     /// Transform a point in `destinationRectangle` onto `sourceRectangle`.
165     /// See_Also:
166     ///     `pointToLocal` for the reverse transformation.
167     /// Params:
168     ///     point = Point, in local space, to transform.
169     /// Returns:
170     ///     Point transformed into host space.
171     Vector2 pointToHost(Vector2 point) const {
172         return point.viewportTransform(destinationRectangle, sourceRectangle);
173     }
174 
175     /// Transform a pointer into a new position.
176     ///
177     /// This will convert the pointer into a pointer within this node. The pointer *must*
178     /// be loaded in the host `HoverIO`.
179     ///
180     /// Params:
181     ///     pointer = Pointer to transform.
182     /// Returns:
183     ///     Transformed pointer.
184     inout(HoverPointer) pointerToLocal(inout HoverPointer pointer) inout @trusted {
185         HoverPointer result;
186         result.update(pointer);
187         result.device = cast() pointer.device;
188         result.number = pointer.number;
189         result.position = pointToLocal(pointer.position);
190         return cast(inout) result.loadCopy(this, pointer.id);
191     }
192 
193     /// Reverse pointer transform. Transform pointers from the local, transformed space, into the
194     /// space of the host.
195     ///
196     /// This is used when loading pointers through `HoverTransform.load`. This way, devices placed
197     /// inside the transform exist within the transformed space.
198     ///
199     /// Params:
200     ///     pointer = Pointer to transform.
201     /// Returns:
202     ///     Transformed pointer.
203     inout(HoverPointer) pointerToHost(inout HoverPointer pointer) inout @trusted {
204         HoverPointer result;
205         result.update(pointer);
206         result.device = cast() pointer.device;
207         result.number = pointer.number;
208         result.position = pointToHost(pointer.position);
209         return cast(inout) result.loadCopy(hoverIO, pointer.id);
210     }
211 
212     override void beforeResize(Vector2) {
213         require(hoverIO);
214         use(focusIO);
215         startIO();
216     }
217 
218     override void afterResize(Vector2) {
219         stopIO();
220     }
221 
222     /// `HoverTransform` saves all the pointers it receives from the host `HoverIO`
223     /// and creates local copies. It then transforms those, and checks its children for
224     /// matching nodes.
225     override void beforeDraw(Rectangle outer, Rectangle inner) {
226 
227         if (!isDestinationManual && next) {
228             _destinationRectangle = next.paddingBoxForSpace(inner);
229         }
230 
231         size_t actionIndex;
232 
233         foreach (HoverPointer pointer; hoverIO) {
234             if (pointer.isDisabled) continue;
235 
236             auto transformed = pointerToLocal(pointer);
237             const localID = cast(int) _pointers.allResources.countUntil(pointer.id);
238 
239             // Allocate a branch action for each pointer
240             if (actionIndex >= _actions.length) {
241                 _actions.length = actionIndex + 1;
242                 _actions[actionIndex] = new FindHoveredNodeAction;
243             }
244 
245             auto action = _actions[actionIndex++];
246             action.pointer = transformed;
247             controlBranchAction(action).startAndRelease();
248 
249             // Create or update pointer entries
250             if (localID == -1) {
251                 const newLocalID = _pointers.load(Pointer(pointer.id, 0, action));
252                 _pointers[newLocalID].localID = newLocalID;
253             }
254             else {
255                 auto resource = _pointers[localID];
256                 resource.action = action;
257                 resource.localID = localID;
258                 _pointers.reload(localID, resource);
259             }
260         }
261 
262     }
263 
264     override void afterDraw(Rectangle outer, Rectangle inner) {
265         foreach (pointer; _pointers.activeResources) {
266             controlBranchAction(pointer.action).stop();
267 
268             // Read the result of each action into the local pointer
269             pointer.hoveredNode = pointer.action.result;
270 
271             if (!pointer.isHeld) {
272                 pointer.heldNode = pointer.hoveredNode;
273             }
274 
275             // Switch focus if holding
276             else if (focusIO) {
277                 if (auto focusable = pointer.heldNode.castIfAcceptsInput!Focusable) {
278                     if (!focusable.isFocused) {
279                         focusable.focus();
280                     }
281                 }
282                 else {
283                     focusIO.clearFocus();
284                 }
285             }
286             if (!pointer.action.pointer.isScrollHeld) {
287                 pointer.scrollable = pointer.action.scrollable;
288             }
289 
290             pointer.isHeld = false;
291             _pointers[pointer.localID] = pointer;
292         }
293     }
294 
295     override int load(HoverPointer pointer) {
296         auto hostPointer = pointerToHost(pointer);
297         return hoverIO.load(hostPointer);
298     }
299 
300     override inout(HoverPointer) fetch(int number) inout {
301         auto pointer = hoverIO.fetch(number);
302         return pointerToLocal(pointer);
303     }
304 
305     override void emitEvent(HoverPointer pointer, InputEvent event) {
306         auto hostPointer = pointerToHost(pointer);
307         hoverIO.emitEvent(hostPointer, event);
308     }
309 
310     private int hostToLocalID(int id) const {
311         const localID = cast(int) _pointers.allResources.countUntil(id);
312         assert(localID >= 0, format!"Pointer %s isn't loaded"(id));
313         return localID;
314     }
315 
316     override inout(Hoverable) hoverOf(HoverPointer pointer) inout {
317         return hoverOf(pointer.id);
318     }
319 
320     inout(Hoverable) hoverOf(int pointerID) inout {
321         const localID = hostToLocalID(pointerID);
322         return _pointers[localID].heldNode.castIfAcceptsInput!Hoverable;
323     }
324 
325     override inout(HoverScrollable) scrollOf(const HoverPointer pointer) inout {
326         return scrollOf(pointer.id);
327     }
328 
329     inout(HoverScrollable) scrollOf(int pointerID) inout {
330         const localID = hostToLocalID(pointerID);
331         return _pointers[localID].scrollable;
332     }
333 
334     override bool isHovered(const Hoverable hoverable) const {
335         foreach (pointer; _pointers.activeResources) {
336             if (hoverable.opEquals(pointer.heldNode)) {
337                 return true;
338             }
339         }
340         return false;
341     }
342 
343     override int opApply(int delegate(HoverPointer) @safe yield) {
344         foreach (HoverPointer pointer; hoverIO) {
345 
346             auto transformed = pointerToLocal(pointer);
347             if (auto result = yield(transformed)) {
348                 return result;
349             }
350 
351         }
352         return 0;
353     }
354 
355     override int opApply(int delegate(Hoverable) @safe yield) {
356         foreach (pointer; _pointers.activeResources) {
357             if (auto hoverable = cast(Hoverable) pointer.heldNode) {
358                 if (auto result = yield(hoverable)) {
359                     return result;
360                 }
361             }
362         }
363         return 0;
364     }
365 
366     override bool blocksInput() const {
367         return isDisabled || isDisabledInherited;
368     }
369 
370     override bool actionImpl(IO io, int hostID, immutable InputActionID actionID, bool isActive) {
371 
372         const localID = hostToLocalID(hostID);
373         auto resource = _pointers[localID];
374         const isFrameAction = actionID == inputActionID!(ActionIO.CoreAction.frame);
375 
376         // Active input actions can only fire if `heldNode` is still hovered
377         if (isActive) {
378             const isNotHovered = resource.hoveredNode is null
379                 || !resource.hoveredNode.opEquals(resource.heldNode);
380 
381             if (isNotHovered) {
382                 return false;
383             }
384         }
385 
386         // Mark pointer as held
387         if (!isFrameAction) {
388             _pointers[localID].isHeld = true;
389         }
390 
391         // Dispatch the event
392         if (auto target = resource.heldNode.castIfAcceptsInput!Hoverable) {
393             return target.actionImpl(this, hostID, actionID, isActive);
394         }
395         return false;
396 
397     }
398 
399     override bool hoverImpl(HoverPointer pointer) {
400         if (auto target = hoverOf(pointer)) {
401             auto transformed = pointerToLocal(pointer);
402             return target.hoverImpl(transformed);
403         }
404         return false;
405     }
406 
407     override IsOpaque inBoundsImpl(Rectangle outer, Rectangle inner, Vector2 position) {
408         if (super.inBoundsImpl(outer, inner, position).inSelf) {
409             return IsOpaque.onlySelf;
410         }
411         return IsOpaque.no;
412     }
413 
414     override bool isHovered() const {
415         return hoverIO.isHovered(this);
416     }
417 
418     override bool canScroll(const HoverPointer pointer) const {
419         if (auto scroll = scrollOf(pointer)) {
420             auto transformed = pointerToLocal(pointer);
421             return scroll.canScroll(transformed);
422         }
423         return false;
424     }
425 
426     override bool scrollImpl(HoverPointer pointer) {
427         if (auto scroll = scrollOf(pointer)) {
428             auto transformed = pointerToLocal(pointer);
429             return scroll.scrollImpl(transformed);
430         }
431         else return false;
432     }
433 
434     override Rectangle shallowScrollTo(const(Node) child, Rectangle parentBox, Rectangle childBox) {
435         return childBox;
436     }
437 
438     override bool focusImpl() {
439         return false;
440     }
441 
442     override void focus() {
443         // NOOP, can't focus
444     }
445 
446     bool isFocused() const {
447         return false;
448     }
449 
450     alias opEquals = typeof(super).opEquals;
451     override bool opEquals(const Object other) const {
452         return super.opEquals(other);
453     }
454 
455 }
456 
457 private struct Pointer {
458     int hostID;
459     int localID;
460     FindHoveredNodeAction action;
461     Node heldNode;
462     Node hoveredNode;
463     HoverScrollable scrollable;
464     bool isHeld;
465 
466     /// Find a pointer by its host ID
467     bool opEquals(int id) const {
468         return this.hostID == id;
469     }
470 }