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 }