1 /// This module contains interfaces for handling focus and connecting focusable nodes with input devices.
2 module fluid.io.focus;
3 
4 import optional;
5 import fluid.types;
6 
7 import fluid.future.pipe;
8 import fluid.future.context;
9 import fluid.future.branch_action;
10 
11 import fluid.io.action;
12 
13 public import fluid.io.action : InputEvent, InputEventCode;
14 
15 @safe:
16 
17 /// `FocusIO` is an input handler system that reads events off devices like keyboards or gamepads, which do not
18 /// map directly to screen coordinates.
19 ///
20 /// Most of the time, `FocusIO` systems will pass the events they receive to an `ActionIO` system, and then send
21 /// these actions to a focused node.
22 ///
23 /// Multiple different `FocusIO` instances can coexist in the same tree, allowing for multiple different nodes
24 /// to be focused at the same time, as long as they belong in different branches of the node tree. That means
25 /// two different nodes can be focused by two different `FocusIO` systems, but a single `FocusIO` system can only
26 /// focus a single node.
27 interface FocusIO : IO, WithFocus {
28 
29     /// Read an input event from an input device. Input devices will call this function every frame
30     /// if an input event occurs.
31     ///
32     /// `FocusIO` will usually pass these down to an `ActionIO` system. It is up to `FocusIO` to decide how
33     /// the input and the resulting input actions are handled, though they will most often be passed
34     /// to the focused node.
35     ///
36     /// Params:
37     ///     event = Input event the system should save.
38     void emitEvent(InputEvent event);
39 
40     /// Write text received from the system. Input devices should call this function every frame to transmit text
41     /// that the user wrote on the keyboard, which other nodes can then read through `readText`.
42     /// Params:
43     ///     text = Text written by the user.
44     void typeText(scope const char[] text);
45 
46     /// Read text inserted by the user into a buffer.
47     ///
48     /// Reads a UTF-8 sequence of characters from the system that was typed in by the user during the last frame.
49     /// This will be keyboard input as interpreted by the system, using the system's input method, providing support
50     /// for internationalization.
51     ///
52     /// All the text will be written by reference into the provided buffer, overwriting previously stored text.
53     /// The returned value will be a slice of this buffer, representing the entire value:
54     ///
55     /// ---
56     /// char[1024] buffer;
57     /// int offset;
58     /// auto text = focusIO.readText(buffer, offset);
59     /// writeln(text);
60     /// assert(text is buffer[0 .. text.length] || text is null);
61     /// ---
62     ///
63     /// The buffer may not fit the entire text. Because of this, the function should be called repeatedly until the
64     /// returned value is `null`.
65     ///
66     /// ---
67     /// char[1024] buffer;
68     /// int offset;
69     /// while (true) {
70     ///     if (auto text = focusIO.readText(buffer, offset)) {
71     ///         writeln(text);
72     ///     }
73     ///     else {
74     ///         break;
75     ///     }
76     /// }
77     /// ---
78     ///
79     /// This function may not throw: In the instance the offset extends beyond text boundaries, the buffer is empty
80     /// or text cannot be read, this function should return `null`, as if no text should remain to read.
81     ///
82     /// Params:
83     ///     buffer = Buffer to write the text to.
84     ///     offset = Number of leading bytes to skip when writing into the buffer. Updated to point to the end
85     ///         of the buffer. This makes it possible to keep track of position in the text if it doesn't fit
86     ///         in a single buffer.
87     /// Returns:
88     ///     A slice of the given buffer with text that was read. `null` if no text remains to read.
89     char[] readText(return scope char[] buffer, ref int offset) nothrow
90     out(text; text is buffer[0 .. text.length] || text is null,
91         "Returned value must be a slice of the buffer, or be null")
92     out(text; text is null || text.length > 0,
93         "Returned value must be null if it is empty");
94 
95 }
96 
97 /// Nodes implementing this interface can be focused by a `FocusIO` system.
98 interface Focusable : Actionable {
99 
100     /// Handle input. Called each frame when focused.
101     ///
102     /// This method should not be called if `blocksInput` is true.
103     ///
104     /// Returns:
105     ///     True if focus input was handled, false if it was ignored.
106     bool focusImpl()
107     in (!blocksInput, "This node currently doesn't accept input.");
108 
109     /// Set focus to this node.
110     ///
111     /// Implementation would usually check `blocksInput` and call `focusIO.focus` on self for this to take effect.
112     /// A node may override this method to redirect the focus to another node (by calling its `focus()` method),
113     /// or ignore the request.
114     ///
115     /// Focus should do nothing if the node `isDisabled` is true or if
116     void focus();
117 
118     /// Returns:
119     ///     True if this node has focus. Recommended implementation: `return this == focusIO.focus`.
120     ///     Proxy nodes, such as `FieldSlot` might choose to return the value of the node they hold.
121     bool isFocused() const;
122 
123 }
124 
125 /// Find the focus box using a `FindFocusAction`.
126 /// Params:
127 ///     focusIO = FocusIO node owning the focus.
128 FindFocusBoxAction findFocusBox(FocusIO focusIO) {
129 
130     import fluid.node;
131 
132     auto node = cast(Node) focusIO;
133     assert(node, "Given FocusIO is not a node");
134 
135     auto action = new FindFocusBoxAction(focusIO);
136     node.startAction(action);
137     return action;
138 
139 }
140 
141 /// This branch action tracks and reports position of the current focus box.
142 class FindFocusBoxAction : BranchAction, Publisher!(Optional!Rectangle) {
143 
144     import fluid.node;
145 
146     public {
147 
148         /// System holding the focused node in question.
149         FocusIO focusIO;
150 
151         /// Focus box reported by the node, if any. Use `.then((Rectangle) { ... })` to get the focus box the moment
152         /// it is found.
153         Optional!Rectangle focusBox;
154 
155     }
156 
157     private {
158 
159         Subscriber!(Optional!Rectangle) _onFinishRectangle;
160 
161     }
162 
163     /// Prepare the action. To work, it needs to know the `FocusIO` it will search in.
164     /// At this point it can be omitted, but it has to be set before the action launches.
165     this(FocusIO focusIO = null) {
166 
167         this.focusIO = focusIO;
168 
169     }
170 
171     alias then = typeof(super).then;
172     alias then = Publisher!(Optional!Rectangle).then;
173 
174     alias subscribe = typeof(super).subscribe;
175 
176     override void subscribe(Subscriber!(Optional!Rectangle) subscriber) {
177 
178         assert(_onFinishRectangle is null, "Subscriber already connected.");
179         _onFinishRectangle = subscriber;
180 
181     }
182 
183     override void started() {
184 
185         assert(focusIO !is null, "FindFocusBoxAction launched without assigning focusIO");
186 
187         this.focusBox = Optional!Rectangle();
188 
189     }
190 
191     override void beforeDraw(Node node, Rectangle, Rectangle, Rectangle inner) {
192 
193         auto focus = focusIO.currentFocus;
194 
195         // Only the focused node matters
196         if (focus is null || !focus.opEquals(node)) return;
197 
198         this.focusBox = node.focusBox(inner);
199         stop;
200 
201     }
202 
203     override void stopped() {
204 
205         super.stopped();
206 
207         if (_onFinishRectangle) {
208             _onFinishRectangle(focusBox);
209         }
210 
211     }
212 
213 }
214 
215 /// Using FindFocusBoxAction.
216 @("FindFocusBoxAction setup example")
217 unittest {
218 
219     import fluid.node;
220     import fluid.space;
221 
222     class MyNode : Space {
223 
224         FocusIO focusIO;
225         FindFocusBoxAction findFocusBoxAction;
226 
227         this(Node[] nodes...) {
228             super(nodes);
229             this.findFocusBoxAction = new FindFocusBoxAction;
230         }
231 
232         override void resizeImpl(Vector2 space) {
233 
234             require(focusIO);
235             findFocusBoxAction.focusIO = focusIO;
236 
237             super.resizeImpl(space);
238 
239         }
240 
241         override void drawImpl(Rectangle outer, Rectangle inner) {
242 
243             // Start the action before drawing nodes
244             auto frame = startBranchAction(findFocusBoxAction);
245             super.drawImpl(outer, inner);
246 
247             // Inspect the result
248             auto result = findFocusBoxAction.focusBox;
249 
250         }
251 
252     }
253 
254 }
255 
256 /// Base interface for `FocusIO`, providing access and control over the current focusable.
257 /// Used to create additional interfaces like `WithPositionalFocus` without defining a new I/O
258 /// set.
259 interface WithFocus {
260 
261     /// Note:
262     ///     Currently focused node may have `blocksInput` set to true; take care to check it before calling input
263     ///     handling methods.
264     /// Returns:
265     ///     The currently focused node, or `null` if no node has focus at the moment.
266     inout(Focusable) currentFocus() inout nothrow;
267 
268     /// Change the currently focused node to another.
269     ///
270     /// This function may frequently be passed `null` with the intent of clearing the focused node.
271     ///
272     /// Params:
273     ///     newValue = Node to assign focus to.
274     /// Returns:
275     ///     Node that was focused, to allow chaining assignments.
276     Focusable currentFocus(Focusable newValue) nothrow;
277 
278     /// Returns:
279     ///     True, if a node focused (`currentFocus` is not null) and if it accepts input (`currentFocus.blocksInput`
280     ///     is false).
281     final bool isFocusActionable() const {
282 
283         auto focus = currentFocus;
284 
285         return focus !is null
286             && !focus.blocksInput;
287 
288     }
289 
290     /// Returns:
291     ///     True if the focusable is currently focused.
292     ///     Always returns `false` if the parameter is `null`.
293     /// Params:
294     ///     focusable = Focusable to check.
295     final bool isFocused(const Focusable focusable) const nothrow {
296 
297         import std.exception : assumeWontThrow;
298 
299         auto focus = currentFocus;
300 
301         return focus !is null
302             && focus.opEquals(cast(const Object) focusable).assumeWontThrow;
303 
304     }
305 
306     /// Clear current focus (set it to null).
307     final void clearFocus() {
308         currentFocus = null;
309     }
310 
311 }
312 
313 /// A ready-made implementation of tabbing for `FocusIO` using `orderedFocusAction`,
314 /// provided as an interface to subclass from.
315 ///
316 /// Tabbing can be performed using the `focusNext` and `focusPrevious` methods. They are bound to
317 /// the corresponding `FluidInputAction` actions and should be automatically picked up by
318 /// `enableInputActions`. A complete implementation will thus provide the ability to navigate
319 /// between nodes using the "tab" key.
320 ///
321 /// To make `WithOrderedFocus` work, it is currently necessary to override two methods:
322 ///
323 /// ---
324 /// override protected inout(OrederedFocusAction) orderedFocusAction() inout;
325 /// override protected void focusPreviousOrNext(FluidInputAction actionType) { }
326 /// ---
327 ///
328 /// The latter, `focusPreviousOrNext` must be overridden so that it does nothing if `FocusIO`
329 /// is in use, as it only applies to the old backend. It will be removed in Fluid 0.8.0.
330 interface WithOrderedFocus : WithFocus {
331 
332     import fluid.node;
333     import fluid.style : Style;
334     import fluid.actions;
335     import fluid.future.action;
336 
337     /// Returns:
338     ///     An instance of OrderedFocusAction.
339     protected inout(OrderedFocusAction) orderedFocusAction() inout;
340 
341     /// `focusNext` focuses the next, and `focusPrevious` focuses the previous node, relative
342     /// to the one that is currently focused.
343     ///
344     /// Params:
345     ///     isReverse = Reverse direction; if true, focuses the previous node.
346     /// Returns:
347     ///     Tree action that switches focus to the previous, or next node.
348     ///     If no node is currently focused, returns a tree action to focus the first
349     ///     or the last node, equivalent to `focusFirst` or `focusLast`.
350     ///
351     ///     You can use `.then` on the returned action to run a callback the moment
352     ///     the focus switches.
353     final FocusSearchAction focusNext(bool isReverse = false) {
354 
355         auto focus = cast(Node) currentFocus;
356         auto self = cast(Node) this;
357 
358         if (focus is null) {
359             if (isReverse)
360                 return focusLast();
361             else
362                 return focusFirst();
363         }
364 
365         // Switch focus
366         orderedFocusAction.reset(focus, isReverse);
367         self.startAction(orderedFocusAction);
368 
369         return orderedFocusAction;
370 
371     }
372 
373     /// ditto
374     final FocusSearchAction focusPrevious() {
375         return focusNext(true);
376     }
377 
378     /// Focus the first (`focusFirst`), or the last node (`focusLast`) that exists inside the
379     /// focus space.
380     /// Returns:
381     ///     Tree action that switches focus to the first, or the last node.
382     ///     You can use `.then` on the returned action to run a callback the moment the focus
383     ///     switches.
384     final FocusSearchAction focusFirst() {
385         // TODO cache this, or integrate into OrderedFocusAction?
386         return focusRecurseChildren(cast(Node) this);
387     }
388 
389     /// ditto
390     final FocusSearchAction focusLast() {
391         auto action = focusRecurseChildren(cast(Node) this);
392         action.isReverse = true;
393         return action;
394     }
395 
396     @(FluidInputAction.focusNext)
397     final bool focusNext(FluidInputAction) {
398         focusNext();
399         return true;
400     }
401 
402     @(FluidInputAction.focusPrevious)
403     final bool focusPrevious(FluidInputAction) {
404         focusPrevious();
405         return true;
406     }
407 
408 }
409 
410 /// A ready implementation of positional focus for `FocusIO`, enabling switching between nodes
411 /// using (usually) arrow keys. Used by subclassing in the focus I/O system.
412 ///
413 /// This interface expects to be provided `positionalFocusAction`, which will be used to locate
414 /// the target. `lastFocusBox` should be updated with the current focus box every frame; this can
415 /// be achieved using the `FindFocusBox` branch action.
416 ///
417 /// This interface exposes a few input actions, which if enabled using `mixin enableInputActions`,
418 /// will enable navigation using standard Fluid input actions.
419 ///
420 /// Implementing positional focus using this class requires three overrides in total:
421 ///
422 /// ---
423 /// override protected Optional!Rectangle lastFocusBox() const;
424 /// override protected inout(PositionalFocusAction) positionalFocusAction() inout;
425 /// override protected void focusInDirection(FluidInputAction actionType) { }
426 /// ---
427 ///
428 /// The last overload is necessary to avoid conflicts with the old backend system. It will stop
429 /// being available in Fluid 0.8.0.
430 interface WithPositionalFocus : WithFocus {
431 
432     import fluid.node;
433     import fluid.style : Style;
434     import fluid.actions;
435     import fluid.future.action;
436 
437     /// To provide a reference for positional focus, the bounding box of the focused node.
438     /// Returns:
439     ///     Last known focus box of the focused node. May be out of date if the focused node
440     ///     has changed since last fetched.
441     protected Optional!Rectangle lastFocusBox() const;
442 
443     /// Returns:
444     ///     An instance of PositionalFocusAction.
445     protected inout(PositionalFocusAction) positionalFocusAction() inout;
446 
447     /// Positional focus: Switch focus from the currently focused node to another based on screen
448     /// position.
449     ///
450     /// This launches a tree action that will find a candidate node and switch focus to it during
451     /// the next frame. Nodes that are the closest semantically (are in the same container node,
452     /// or overall close in the tree) will be chosen first; screen distance will be used when two
453     /// nodes have the same weight.
454     ///
455     /// Returns:
456     ///     The launched tree action. You can use `.then` to attach a callback that will run as
457     ///     soon as the node is found.
458     final FocusSearchAction focusAbove() {
459         return focusDirection(Style.Side.top);
460     }
461 
462     /// ditto
463     final FocusSearchAction focusBelow() {
464         return focusDirection(Style.Side.bottom);
465     }
466 
467     /// ditto
468     final FocusSearchAction focusToLeft() {
469         return focusDirection(Style.Side.left);
470     }
471 
472     /// ditto
473     final FocusSearchAction focusToRight() {
474         return focusDirection(Style.Side.right);
475     }
476 
477     /// ditto
478     final FocusSearchAction focusDirection(Style.Side side) {
479         return lastFocusBox.match!(
480             (Rectangle focusBox) {
481 
482                 auto reference = cast(Node) currentFocus;
483 
484                 // No focus, no action to launch
485                 if (reference is null) return null;
486 
487                 auto self = cast(Node) this;
488 
489                 positionalFocusAction.reset(reference, focusBox, side);
490                 self.startAction(positionalFocusAction);
491 
492                 return positionalFocusAction;
493 
494             },
495             () => PositionalFocusAction.init,
496         );
497     }
498 
499     @(FluidInputAction.focusUp)
500     final bool focusUp() {
501         focusAbove();
502         return true;
503     }
504 
505     @(FluidInputAction.focusDown)
506     final bool focusDown() {
507         focusBelow();
508         return true;
509     }
510 
511     @(FluidInputAction.focusLeft)
512     final bool focusLeft() {
513         focusToLeft();
514         return true;
515     }
516 
517     @(FluidInputAction.focusRight)
518     final bool focusRight() {
519         focusToRight();
520         return true;
521     }
522 
523 }