1 ///
2 module fluid.focus_chain;
3 
4 import optional;
5 import std.array;
6 
7 import fluid.node;
8 import fluid.types;
9 import fluid.style;
10 import fluid.utils;
11 import fluid.actions;
12 import fluid.node_chain;
13 
14 import fluid.io.focus;
15 import fluid.io.action;
16 
17 import fluid.future.action;
18 
19 @safe:
20 
21 alias focusChain = nodeBuilder!FocusChain;
22 
23 /// A focus chain can be used to separate focus in different areas of the user interface. A device node
24 /// (focus-based, like a keyboard or gamepad) node can be placed to control nodes inside.
25 ///
26 /// For hover-based nodes like mouse, see `HoverChain`.
27 ///
28 /// `FocusChain` only works with nodes compatible with the new I/O system introduced in Fluid 0.7.2.
29 class FocusChain : NodeChain, FocusIO, WithOrderedFocus, WithPositionalFocus {
30 
31     mixin controlIO;
32 
33     ActionIO actionIO;
34 
35     protected {
36 
37         /// Focus box tracking action.
38         FindFocusBoxAction findFocusBoxAction;
39 
40     }
41 
42     private {
43 
44         Focusable _focus;
45         bool _wasInputHandled;
46         Appender!(char[]) _buffer;
47         PositionalFocusAction _positionalFocusAction;
48         OrderedFocusAction _orderedFocusAction;
49         Optional!Rectangle _lastFocusBox;
50 
51     }
52 
53     this() {
54         this(null);
55     }
56 
57     this(Node next) {
58 
59         super(next);
60         findFocusBoxAction     = new FindFocusBoxAction(this);
61         _orderedFocusAction    = new OrderedFocusAction;
62         _positionalFocusAction = new PositionalFocusAction;
63 
64         // Track the current focus box
65         findFocusBoxAction
66             .then((Optional!Rectangle rect) => _lastFocusBox = rect);
67 
68     }
69 
70     /// If a node inside `FocusChain` triggers an input event (for example a keyboard node,
71     /// like a keyboard automaton), another node inside may handle the event. This property
72     /// will be set to true after that happens.
73     ///
74     /// This status is reset the moment this frame is updated again.
75     ///
76     /// Returns:
77     ///     True, if an input action launched during the last frame was passed to a focused node and handled.
78     bool wasInputHandled() const {
79 
80         return _wasInputHandled;
81 
82     }
83 
84     override protected Optional!Rectangle lastFocusBox() const {
85         return _lastFocusBox;
86     }
87 
88     override protected inout(OrderedFocusAction) orderedFocusAction() inout {
89         return _orderedFocusAction;
90     }
91 
92     override protected inout(PositionalFocusAction) positionalFocusAction() inout {
93         return _positionalFocusAction;
94     }
95 
96     override inout(Focusable) currentFocus() inout {
97         return _focus;
98     }
99 
100     override Focusable currentFocus(Focusable newFocus) {
101         return _focus = newFocus;
102     }
103 
104     override void beforeResize(Vector2) {
105         use(actionIO);
106         startIO();
107     }
108 
109     override void afterResize(Vector2) {
110         stopIO();
111     }
112 
113     override void beforeDraw(Rectangle, Rectangle) {
114 
115         controlBranchAction(findFocusBoxAction)
116             .startAndRelease();
117 
118         _wasInputHandled = false;
119 
120     }
121 
122     override void afterDraw(Rectangle outer, Rectangle inner) {
123 
124         controlBranchAction(findFocusBoxAction)
125             .stop();
126 
127         // Send a frame event to trigger focusImpl
128         if (actionIO) {
129             actionIO.emitEvent(ActionIO.frameEvent, 0, &runInputAction);
130         }
131         else if (isFocusActionable) {
132             _wasInputHandled = currentFocus.focusImpl();
133             _buffer.clear();
134         }
135 
136     }
137 
138     /// Handle an input action using the currently focused node.
139     ///
140     /// Does nothing if no node has focus.
141     ///
142     /// Params:
143     ///     actionID = Input action for the node to handle.
144     ///     isActive = If true (default) the action is active, on top of being simply emitted.
145     ///         Most handlers only react to active actions.
146     /// Returns:
147     ///     True if the action was handled.
148     ///     Consequently, `wasInputAction` will be set to true.
149     bool runInputAction(InputActionID actionID, bool isActive = true) {
150 
151         const isFrameAction = actionID == inputActionID!(ActionIO.CoreAction.frame);
152 
153         // Try to handle the input action
154         const handled =
155 
156             // Run the action, and mark input as handled
157             (isFocusActionable && currentFocus.actionImpl(this, 0, actionID, isActive))
158 
159             // Run local input actions
160             || (runLocalInputActions(actionID, isActive))
161 
162             // Run focusImpl as a fallback
163             || (isFrameAction && isFocusActionable && currentFocus.focusImpl());
164 
165         // Mark as handled, if so
166         if (handled) {
167             _wasInputHandled = true;
168 
169             // Cancel action events
170             if (actionIO) {
171                 actionIO.emitEvent(ActionIO.noopEvent, 0, &runInputAction);
172             }
173         }
174 
175         // Clear the input buffer after frame action
176         if (isFrameAction) {
177             _buffer.clear();
178         }
179 
180         return handled;
181 
182     }
183 
184     /// ditto
185     bool runInputAction(alias action)(bool isActive = true) {
186 
187         const id = inputActionID!action;
188 
189         return runInputAction(id, isActive);
190 
191     }
192 
193     /// ditto
194     protected final bool runInputAction(InputActionID actionID, bool isActive, int) {
195 
196         return runInputAction(actionID, isActive);
197 
198     }
199 
200     /// Run an input action implemented by this node. These usually perform focus switching
201     /// Params:
202     ///     actionID = ID of the input action to perform.
203     ///     isActive = If true, the action has been activated during this frame.
204     /// Returns:
205     ///     True if the action was handled, false if not.
206     protected bool runLocalInputActions(InputActionID actionID, bool isActive = true) {
207 
208         return runInputActionHandler(this, actionID, isActive);
209 
210     }
211 
212     // Disable default focus switching
213     override protected void focusPreviousOrNext(FluidInputAction actionType) { }
214     override protected void focusInDirection(FluidInputAction actionType) { }
215 
216     /// Type text to read during the next frame.
217     ///
218     /// This text will then become available for reading through `readText`.
219     ///
220     /// Params:
221     ///     text = Text to write into the buffer.
222     override void typeText(scope const char[] text) {
223         _buffer ~= text;
224     }
225 
226     override char[] readText(return scope char[] buffer, ref int offset) nothrow {
227 
228         import std.algorithm : min;
229 
230         // Read the entire text, nothing remains to be read
231         if (offset >= _buffer[].length) return null;
232 
233         // Get remaining text
234         const text = _buffer[][offset .. $];
235         const length = min(text.length, buffer.length);
236 
237         offset += length;
238         return buffer[0 .. length] = text[0 .. length];
239 
240     }
241 
242     override void emitEvent(InputEvent event) {
243 
244         if (actionIO) {
245             actionIO.emitEvent(event, 0, &runInputAction);
246         }
247 
248     }
249 
250 }