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 }