1 /// This module contains interfaces for handling focus and connecting focusable nodes with input devices.2 modulefluid.io.focus;
3 4 importoptional;
5 importfluid.types;
6 7 importfluid.future.pipe;
8 importfluid.future.context;
9 importfluid.future.branch_action;
10 11 importfluid.io.action;
12 13 publicimportfluid.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 not18 /// 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 send21 /// these actions to a focused node.22 ///23 /// Multiple different `FocusIO` instances can coexist in the same tree, allowing for multiple different nodes24 /// to be focused at the same time, as long as they belong in different branches of the node tree. That means25 /// two different nodes can be focused by two different `FocusIO` systems, but a single `FocusIO` system can only26 /// focus a single node.27 interfaceFocusIO : IO, WithFocus {
28 29 /// Read an input event from an input device. Input devices will call this function every frame30 /// if an input event occurs.31 ///32 /// `FocusIO` will usually pass these down to an `ActionIO` system. It is up to `FocusIO` to decide how33 /// the input and the resulting input actions are handled, though they will most often be passed34 /// to the focused node.35 ///36 /// Params:37 /// event = Input event the system should save.38 voidemitEvent(InputEventevent);
39 40 /// Write text received from the system. Input devices should call this function every frame to transmit text41 /// 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 voidtypeText(scopeconstchar[] 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 support50 /// 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 the64 /// 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 empty80 /// 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 end85 /// of the buffer. This makes it possible to keep track of position in the text if it doesn't fit86 /// 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(returnscopechar[] buffer, refintoffset) nothrow90 out(text; textisbuffer[0 .. text.length] || textisnull,
91 "Returned value must be a slice of the buffer, or be null")
92 out(text; textisnull || 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 interfaceFocusable : 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 boolfocusImpl()
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 if116 voidfocus();
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 boolisFocused() const;
122 123 }
124 125 /// Find the focus box using a `FindFocusAction`.126 /// Params:127 /// focusIO = FocusIO node owning the focus.128 FindFocusBoxActionfindFocusBox(FocusIOfocusIO) {
129 130 importfluid.node;
131 132 autonode = cast(Node) focusIO;
133 assert(node, "Given FocusIO is not a node");
134 135 autoaction = newFindFocusBoxAction(focusIO);
136 node.startAction(action);
137 returnaction;
138 139 }
140 141 /// This branch action tracks and reports position of the current focus box.142 classFindFocusBoxAction : BranchAction, Publisher!(Optional!Rectangle) {
143 144 importfluid.node;
145 146 public {
147 148 /// System holding the focused node in question.149 FocusIOfocusIO;
150 151 /// Focus box reported by the node, if any. Use `.then((Rectangle) { ... })` to get the focus box the moment152 /// it is found.153 Optional!RectanglefocusBox;
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(FocusIOfocusIO = null) {
166 167 this.focusIO = focusIO;
168 169 }
170 171 aliasthen = typeof(super).then;
172 aliasthen = Publisher!(Optional!Rectangle).then;
173 174 aliassubscribe = typeof(super).subscribe;
175 176 overridevoidsubscribe(Subscriber!(Optional!Rectangle) subscriber) {
177 178 assert(_onFinishRectangleisnull, "Subscriber already connected.");
179 _onFinishRectangle = subscriber;
180 181 }
182 183 overridevoidstarted() {
184 185 assert(focusIO !isnull, "FindFocusBoxAction launched without assigning focusIO");
186 187 this.focusBox = Optional!Rectangle();
188 189 }
190 191 overridevoidbeforeDraw(Nodenode, Rectangle, Rectangle, Rectangleinner) {
192 193 autofocus = focusIO.currentFocus;
194 195 // Only the focused node matters196 if (focusisnull || !focus.opEquals(node)) return;
197 198 this.focusBox = node.focusBox(inner);
199 stop;
200 201 }
202 203 overridevoidstopped() {
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 importfluid.node;
220 importfluid.space;
221 222 classMyNode : Space {
223 224 FocusIOfocusIO;
225 FindFocusBoxActionfindFocusBoxAction;
226 227 this(Node[] nodes...) {
228 super(nodes);
229 this.findFocusBoxAction = newFindFocusBoxAction;
230 }
231 232 overridevoidresizeImpl(Vector2space) {
233 234 require(focusIO);
235 findFocusBoxAction.focusIO = focusIO;
236 237 super.resizeImpl(space);
238 239 }
240 241 overridevoiddrawImpl(Rectangleouter, Rectangleinner) {
242 243 // Start the action before drawing nodes244 autoframe = startBranchAction(findFocusBoxAction);
245 super.drawImpl(outer, inner);
246 247 // Inspect the result248 autoresult = 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/O258 /// set.259 interfaceWithFocus {
260 261 /// Note:262 /// Currently focused node may have `blocksInput` set to true; take care to check it before calling input263 /// handling methods.264 /// Returns:265 /// The currently focused node, or `null` if no node has focus at the moment.266 inout(Focusable) currentFocus() inoutnothrow;
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 FocusablecurrentFocus(FocusablenewValue) 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 finalboolisFocusActionable() const {
282 283 autofocus = currentFocus;
284 285 returnfocus !isnull286 && !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 finalboolisFocused(constFocusablefocusable) constnothrow {
296 297 importstd.exception : assumeWontThrow;
298 299 autofocus = currentFocus;
300 301 returnfocus !isnull302 && focus.opEquals(cast(constObject) focusable).assumeWontThrow;
303 304 }
305 306 /// Clear current focus (set it to null).307 finalvoidclearFocus() {
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 to317 /// the corresponding `FluidInputAction` actions and should be automatically picked up by318 /// `enableInputActions`. A complete implementation will thus provide the ability to navigate319 /// 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 interfaceWithOrderedFocus : WithFocus {
331 332 importfluid.node;
333 importfluid.style : Style;
334 importfluid.actions;
335 importfluid.future.action;
336 337 /// Returns:338 /// An instance of OrderedFocusAction.339 protectedinout(OrderedFocusAction) orderedFocusAction() inout;
340 341 /// `focusNext` focuses the next, and `focusPrevious` focuses the previous node, relative342 /// 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 first349 /// or the last node, equivalent to `focusFirst` or `focusLast`.350 ///351 /// You can use `.then` on the returned action to run a callback the moment352 /// the focus switches.353 finalFocusSearchActionfocusNext(boolisReverse = false) {
354 355 autofocus = cast(Node) currentFocus;
356 autoself = cast(Node) this;
357 358 if (focusisnull) {
359 if (isReverse)
360 returnfocusLast();
361 else362 returnfocusFirst();
363 }
364 365 // Switch focus366 orderedFocusAction.reset(focus, isReverse);
367 self.startAction(orderedFocusAction);
368 369 returnorderedFocusAction;
370 371 }
372 373 /// ditto374 finalFocusSearchActionfocusPrevious() {
375 returnfocusNext(true);
376 }
377 378 /// Focus the first (`focusFirst`), or the last node (`focusLast`) that exists inside the379 /// 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 focus383 /// switches.384 finalFocusSearchActionfocusFirst() {
385 // TODO cache this, or integrate into OrderedFocusAction?386 returnfocusRecurseChildren(cast(Node) this);
387 }
388 389 /// ditto390 finalFocusSearchActionfocusLast() {
391 autoaction = focusRecurseChildren(cast(Node) this);
392 action.isReverse = true;
393 returnaction;
394 }
395 396 @(FluidInputAction.focusNext)
397 finalboolfocusNext(FluidInputAction) {
398 focusNext();
399 returntrue;
400 }
401 402 @(FluidInputAction.focusPrevious)
403 finalboolfocusPrevious(FluidInputAction) {
404 focusPrevious();
405 returntrue;
406 }
407 408 }
409 410 /// A ready implementation of positional focus for `FocusIO`, enabling switching between nodes411 /// 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 locate414 /// the target. `lastFocusBox` should be updated with the current focus box every frame; this can415 /// 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 stop429 /// being available in Fluid 0.8.0.430 interfaceWithPositionalFocus : WithFocus {
431 432 importfluid.node;
433 importfluid.style : Style;
434 importfluid.actions;
435 importfluid.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 node440 /// has changed since last fetched.441 protectedOptional!RectanglelastFocusBox() const;
442 443 /// Returns:444 /// An instance of PositionalFocusAction.445 protectedinout(PositionalFocusAction) positionalFocusAction() inout;
446 447 /// Positional focus: Switch focus from the currently focused node to another based on screen448 /// position.449 ///450 /// This launches a tree action that will find a candidate node and switch focus to it during451 /// 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 two453 /// nodes have the same weight.454 ///455 /// Returns:456 /// The launched tree action. You can use `.then` to attach a callback that will run as457 /// soon as the node is found.458 finalFocusSearchActionfocusAbove() {
459 returnfocusDirection(Style.Side.top);
460 }
461 462 /// ditto463 finalFocusSearchActionfocusBelow() {
464 returnfocusDirection(Style.Side.bottom);
465 }
466 467 /// ditto468 finalFocusSearchActionfocusToLeft() {
469 returnfocusDirection(Style.Side.left);
470 }
471 472 /// ditto473 finalFocusSearchActionfocusToRight() {
474 returnfocusDirection(Style.Side.right);
475 }
476 477 /// ditto478 finalFocusSearchActionfocusDirection(Style.Sideside) {
479 returnlastFocusBox.match!(
480 (RectanglefocusBox) {
481 482 autoreference = cast(Node) currentFocus;
483 484 // No focus, no action to launch485 if (referenceisnull) returnnull;
486 487 autoself = cast(Node) this;
488 489 positionalFocusAction.reset(reference, focusBox, side);
490 self.startAction(positionalFocusAction);
491 492 returnpositionalFocusAction;
493 494 },
495 () => PositionalFocusAction.init,
496 );
497 }
498 499 @(FluidInputAction.focusUp)
500 finalboolfocusUp() {
501 focusAbove();
502 returntrue;
503 }
504 505 @(FluidInputAction.focusDown)
506 finalboolfocusDown() {
507 focusBelow();
508 returntrue;
509 }
510 511 @(FluidInputAction.focusLeft)
512 finalboolfocusLeft() {
513 focusToLeft();
514 returntrue;
515 }
516 517 @(FluidInputAction.focusRight)
518 finalboolfocusRight() {
519 focusToRight();
520 returntrue;
521 }
522 523 }