1 /// This module implements interfaces for handling hover and connecting hoverable nodes with input devices. 2 module fluid.io.hover; 3 4 import optional; 5 6 import std.range; 7 8 import fluid.tree; 9 import fluid.types; 10 11 import fluid.future.pipe; 12 import fluid.future.context; 13 import fluid.future.branch_action; 14 15 import fluid.io.action; 16 17 public import fluid.io.action : InputEvent, InputEventCode; 18 19 @safe: 20 21 /// `HoverIO` is an input handler system that reads events off devices with the ability to point at the screen, 22 /// like mouses, touchpads or pens. 23 /// 24 /// Most of the time, `HoverIO` systems will pass the events they receive to an `ActionIO` system, and then send 25 /// these actions to a hovered node. 26 /// 27 /// Multiple different `HoverIO` instances can coexist in the same tree, allowing for multiple different nodes 28 /// to be hovered at the same time, as long as they belong in different branches of the node tree. That means 29 /// two different nodes can be hovered by two different `HoverIO` systems, but a single `HoverIO` system can only 30 /// hover a single node. 31 interface HoverIO : IO { 32 33 /// Load a hover pointer (mouse cursor, finger) and place it at the position currently 34 /// indicated in the struct. Update the pointer's position if already loaded. 35 /// 36 /// It is expected `load` will be called after every pointer motion in order to keep its 37 /// position up to date. 38 /// 39 /// A pointer is considered loaded until the next resize. If the `load` call for a pointer 40 /// isn't repeated during a resize, the pointer is invalidated. Pointers do not have to be 41 /// loaded while resizing; they can also be loaded while drawing. 42 /// 43 /// An example implementation of a pointer device, from inside of a node, could look like: 44 /// 45 /// --- 46 /// HoverIO hoverIO; 47 /// HoverPointer pointer; 48 /// 49 /// override void resizeImpl(Vector2) { 50 /// require(hoverIO); 51 /// minSize = Vector2(); 52 /// } 53 /// 54 /// override void drawImpl(Rectangle, Rectangle) { 55 /// pointer.device = this; 56 /// pointer.number = 0; 57 /// pointer.position = mousePosition(); 58 /// load(mouseIO, pointer); 59 /// if (clicked) { 60 /// mouseIO.emitEvent(pointer, MouseIO.createEvent(MouseIO.Button.left, true)); 61 /// } 62 /// } 63 /// --- 64 /// 65 /// Params: 66 /// pointer = Pointer to prepare. 67 /// The pointer's `device` field should be set to whatever node represents this device, 68 /// and the `number` field should be set to whatever number the device can associate with the pointer, 69 /// if multiple pointers are to be used. 70 /// Returns: 71 /// An ID the `HoverIO` system will use to recognize the pointer. 72 int load(HoverPointer pointer); 73 74 /// Fetch a pointer from a number assigned to it by this I/O. This is used by `Actionable` 75 /// nodes to find `HoverPointer` data corresponding to fired input action events. 76 /// 77 /// The pointer, and the matching number, must be valid. 78 /// 79 /// Params: 80 /// number = Number assigned to the pointer by this I/O. 81 /// Returns: 82 /// The pointer. 83 inout(HoverPointer) fetch(int number) inout; 84 85 /// Read an input event from an input device. Input devices will call this function every 86 /// frame if an input event (such as a button press) occurs. Moving a mouse does not qualify 87 /// as an input event. 88 /// 89 /// The hover pointer emitting the event must have been loaded earlier (using `load`) during 90 /// the same frame for the action to work. 91 /// 92 /// `HoverIO` will usually pass these down to an `ActionIO` system. It is up to `HoverIO` to 93 /// decide how the input and the resulting input actions is handled, though the node hovered 94 /// by the pointer will most often receive them. 95 /// 96 /// Params: 97 /// pointer = Pointer that emitted the event. 98 /// event = Input event the system should emit. 99 /// The event is usually considered "active" during the frame the action is 100 /// "released". For example, user stops holding a mouse button, or a finger stops 101 /// touching the screen. 102 void emitEvent(HoverPointer pointer, InputEvent event); 103 104 /// Params: 105 /// pointer = Pointer to query. The pointer must be loaded. 106 /// Returns: 107 /// Node hovered by the hover pointer. 108 /// See_Also: 109 /// `scrollOf` to get the current scrollable node. 110 inout(Hoverable) hoverOf(HoverPointer pointer) inout; 111 112 /// Params: 113 /// pointer = Pointer to query. The pointer must be loaded. 114 /// Returns: 115 /// Scrollable ancestor for the currently hovered node. 116 /// See_Also: 117 /// `hoverOf` to get the currently hovered node. 118 inout(HoverScrollable) scrollOf(HoverPointer pointer) inout; 119 120 /// Returns: 121 /// True if the node is hovered. 122 /// Params: 123 /// hoverable = True if this node is hovered. 124 bool isHovered(const Hoverable hoverable) const; 125 126 /// List all active hover pointer, namely all pointers that have been loaded since the last 127 /// resize. 128 /// 129 /// Pointers do not need to be sorted. 130 /// 131 /// Params: 132 /// yield = A delegate to be called for every active node. 133 /// Disabled nodes should be included. 134 /// If the delegate returns a non-zero value, it should immediately break out 135 /// of the loop and return this value. 136 /// Returns: 137 /// If `yield` returned a non-zero value, it should be returned; 138 /// if `yield` wasn't called, or has only returned zeroes, a zero is returned. 139 int opApply(int delegate(HoverPointer) @safe yield); 140 141 /// List all currently hovered nodes. 142 /// 143 /// Nodes do not need to be sorted. 144 /// 145 /// Params: 146 /// yield = A delegate to be called for every hovered node. 147 /// This should include nodes that block input, but are hovered. 148 /// If the delegate returns a non-zero value, the value should be immediately returned. 149 /// Returns: 150 /// If `yield` returned a non-zero value, this is the value it returned; 151 /// if `yield` wasn't called, or has only returned zeroes, a zero is returned. 152 int opApply(int delegate(Hoverable) @safe yield); 153 154 } 155 156 /// An extension of `HoverIO` that enables support for dispatching and running input actions. 157 interface ActionHoverIO : HoverIO { 158 159 /// Handle an input action associated with a pointer. 160 /// Params: 161 /// pointer = Pointer to send the input action. It must be loaded. 162 /// The input action will be loaded by the node the pointer points at. 163 /// actionID = ID of the input action. 164 /// isActive = If true, the action has been activated during this frame. 165 /// Returns: 166 /// True if the input action was handled. 167 bool runInputAction(HoverPointer pointer, immutable InputActionID actionID, 168 bool isActive = true); 169 170 /// ditto 171 bool runInputAction(alias action)(HoverPointer pointer, bool isActive = true) { 172 const id = inputActionID!action; 173 return runInputAction(pointer, id, isActive); 174 } 175 176 } 177 178 /// Returns: 179 /// True if the `hoverIO` is hovering some node. 180 /// Params: 181 /// hoverIO = HoverIO to test. 182 /// hoverable = Node that HoverIO is expected to hover. 183 bool hovers(HoverIO hoverIO) { 184 185 foreach (Hoverable _; hoverIO) { 186 return true; 187 } 188 return false; 189 190 } 191 192 /// ditto 193 bool hovers(HoverIO hoverIO, const Hoverable hoverable) { 194 195 foreach (Hoverable hovered; hoverIO) { 196 if (hovered.opEquals(cast(const Object) hoverable)) return true; 197 } 198 199 return false; 200 201 } 202 203 /// Test if the hover I/O system hovers all of the nodes and none other. 204 /// Params: 205 /// hoverIO = Hover I/O system to test. Hoverables reported by this system will be checked. 206 /// hoverables = A forward range of hoverables. 207 /// Returns: 208 /// True if the system considers all of the given ranges as hovered, 209 /// and it does not find any other nodes hovered. 210 bool hoversOnly(Range)(HoverIO hoverIO, Range hoverables) 211 if (isForwardRange!Range && is(ElementType!Range : const Hoverable)) 212 do { 213 214 import std.algorithm : canFind; 215 216 foreach (Hoverable hovered; hoverIO) { 217 218 // A node is hovered, but not in the known list 219 if (!hoverables.canFind!((a, b) => a.opEquals(cast(const Object) b))(hovered)) { 220 return false; 221 } 222 223 } 224 225 foreach (hoverable; hoverables) { 226 227 // A hoverable is not hovered 228 if (!hoverIO.hovers(hoverable)) { 229 return false; 230 } 231 232 } 233 234 return true; 235 236 } 237 238 /// A pointer is a position on the screen chosen by the user using a mouse, touchpad, touchscreen or other device 239 /// capable of communicating some position. 240 /// 241 /// While in a typical desktop application there will usually be a single pointer at a time, there can be cases 242 /// where there may be none (no mouse connected) or more (multitouch-enabled screen, multiple mouses connected, etc.) 243 /// 244 /// A pointer is associated with an I/O system that represents the device that invoked the pointer. 245 /// This may be a dedicated mouse node, but it may also be a generic system that abstracts the device away; 246 /// for example, Raylib provides a singular function for getting the mouse position without distinguishing 247 /// between multiple devices or touchscreens. 248 /// 249 /// For a pointer to work, it has to be loaded into a `HoverIO` system using its `load` method. This has to be done 250 /// once a frame for as long as the pointer is active. This will be every frame for a mouse (if one is connected), 251 /// or only the frames a finger is touching the screen for a touchscreen. 252 /// 253 /// See_Also: 254 /// `HoverIO`, `HoverIO.load` 255 struct HoverPointer { 256 257 // As a workaround for a DMD codegen bug on Windows, HoverPointer must not be zero initialized. 258 // See https://git.samerion.com/Samerion/Fluid/pulls/357#issuecomment-3309 for more details. 259 static assert(!__traits(isZeroInit, HoverPointer)); 260 261 /// I/O system that represents the device controlling the pointer. 262 IO device; 263 264 /// If the device can control multiple pointers (like a touchscreen), this number should uniquely identify 265 /// a pointer. 266 int number; 267 268 /// Position in the window the pointer is pointing at. 269 Vector2 position; 270 271 /// Current scroll value. For a mouse, this indicates mouse wheel movement, for other devices 272 /// like touchpad or touchscreen, this will be translated from its movement. 273 /// 274 /// This value indicates the distance and direction in window space that the scroll should 275 /// result in covering. This means that on the X axis negative values move left and positive 276 /// values move right, while on the Y axis negative values go upwards and positive values go 277 /// downwards. For example, a scroll value of `(0, 20)` scrolls 20 pixels down vertically, 278 /// while `(0, -10)` scrolls 10 pixels up. 279 /// 280 /// While it is possible to read scroll of the `HoverPointer` data received in an input action 281 /// handler, it is recommended to implement scroll through `Scrollable.scrollImpl`. 282 /// 283 /// Scroll is exposed for both the horizontal and vertical axis. While a basic mouse wheel 284 /// only supports vertical movement, touchscreens, touchpads, trackpads or more advanced 285 /// mouses do support horizontal movement. It is also possible for a device to perform both 286 /// horizontal and vertical movement at once. 287 Vector2 scroll; 288 289 /// True if the pointer is not currently pointing, like a finger that stopped touching the screen. 290 bool isDisabled; 291 292 /// Consecutive click counter. A value of 1 represents a single click, 2 is a double click, 3 is a triple click, 293 /// and so on. The counter should reset after a small delay, or if a distance threshold is crossed. 294 /// 295 /// This value is usually provided by the system. If unavailable, you can use 296 /// `fluid.io.preference.MultipleClickCounter` to generate this value from data available to Fluid. 297 int clickCount; 298 299 /// If true, the scroll control is held, like a finger swiping through the screen. This does not apply to mouse 300 /// wheels. 301 /// 302 /// If scroll is "held," the scrolling motion should detach from pointer position. Whatever scrollable was 303 /// selected at the time scroll was pressed should continue to be selected while held even if the pointer moves 304 /// away from it. This makes it possible to comfortably scroll with a touchscreen without having to mind node 305 /// boundaries, or to implement features such as [autoscroll]. 306 /// 307 /// [autoscroll]: (https://chromewebstore.google.com/detail/autoscroll/occjjkgifpmdgodlplnacmkejpdionan) 308 bool isScrollHeld; 309 310 /// `HoverIO` system controlling the pointer. 311 private HoverIO _hoverIO; 312 313 /// ID of the pointer assigned by the `HoverIO` system. 314 private int _id = -1; 315 316 /// If the given system is a Hover I/O system, fetch a hover pointer. 317 /// 318 /// Given data must be valid; the I/O must be a `HoverIO` instance and the number must be a 319 /// valid pointer number. 320 /// 321 /// Params: 322 /// io = I/O system to use. 323 /// number = Valid pointer number assigned by the I/O system. 324 /// Returns: 325 /// Hover pointer under given number. 326 static Optional!HoverPointer fetch(IO io, int number) { 327 328 import std.format; 329 330 if (auto hoverIO = cast(HoverIO) io) { 331 return typeof(return)( 332 hoverIO.fetch(number)); 333 } 334 335 return typeof(return).init; 336 337 } 338 339 /// Compare two pointers. All publicly exposed fields (`device`, `number`, `position`, 340 /// `isDisabled`) must be equal. To check if the two pointers have the same origin (device and 341 /// number), use `isSame`. 342 /// Params: 343 /// other = Pointer to compare against. 344 /// Returns: 345 /// True if the pointer is the same as the other pointer and has the same state. 346 bool opEquals(const HoverPointer other) const { 347 348 // Do not compare I/O metadata last two fields 349 // Do not compare device (for old compilers), use isSame instead 350 return this.tupleof[1..$-2] == other.tupleof[1..$-2] 351 && isSame(other); 352 353 } 354 355 /// Test if the two pointers have the same origin — same device and pointer number. 356 /// Params: 357 /// other = Pointer to compare against. 358 /// Returns: 359 /// True if the pointers have the same device and pointer number. 360 bool isSame(const HoverPointer other) const { 361 362 if (device is null) { 363 return other.device is null 364 && number == other.number; 365 } 366 367 return device.opEquals(cast(const Object) other.device) 368 && number == other.number; 369 370 } 371 372 /// Returns: The ID/index assigned by `HoverIO` when this pointer was loaded. 373 int id() const nothrow { 374 return this._id; 375 } 376 377 /// Returns: The I/O system owning the pointer. 378 inout(HoverIO) system() inout nothrow { 379 return this._hoverIO; 380 } 381 382 /// Load the pointer into the system. 383 void load(HoverIO hoverIO, int id) nothrow { 384 this._hoverIO = hoverIO; 385 this._id = id; 386 } 387 388 inout(HoverPointer) loadCopy(inout HoverIO hoverIO, int id) inout { 389 return inout HoverPointer( 390 this.device, 391 this.number, 392 this.position, 393 this.scroll, 394 this.isDisabled, 395 this.clickCount, 396 this.isScrollHeld, 397 hoverIO, 398 id, 399 ); 400 } 401 402 /// Update a pointer in place using data of another pointer. 403 /// Params: 404 /// other = Pointer to copy data from. 405 void update(const HoverPointer other) { 406 this.position = other.position; 407 this.scroll = other.scroll; 408 this.isScrollHeld = other.isScrollHeld; 409 this.isDisabled = other.isDisabled; 410 this.clickCount = other.clickCount; 411 } 412 413 /// Emit an event through the pointer. 414 /// 415 /// The device should call this every frame an input event associated with the pointer occurs. This will be 416 /// when a mouse button is pressed, every frame a finger touches the screen, or when a gesture recognized 417 /// by the device or system is performed. 418 /// 419 /// Params: 420 /// event = Event to emit. 421 /// The event is usually considered "active" during the frame the action is "released". For example, 422 /// user stops holding a mouse button, or a finger stops touching the screen. 423 /// See_Also: 424 /// `HoverIO.emitEvent` 425 void emitEvent(InputEvent event) { 426 427 _hoverIO.emitEvent(this, event); 428 429 } 430 431 } 432 433 /// Nodes implementing this interface can be selected by a `HoverIO` system. 434 interface Hoverable : Actionable { 435 436 /// Handle input. Called each frame when focused. 437 /// 438 /// Do not call this method if the `blocksInput` is true. 439 /// 440 /// Params: 441 /// pointer = Pointer to handle this input. 442 /// Returns: 443 /// True if hover was handled, false if it was ignored. 444 bool hoverImpl(HoverPointer pointer) 445 in (!blocksInput, "This node currently doesn't accept input."); 446 447 /// Returns: 448 /// True if this node is hovered. 449 /// This will most of the time be equivalent to `hoverIO.isHovered(this)`, 450 /// but a node wrapping another hoverable may choose to instead redirect this to the other node. 451 bool isHovered() const; 452 453 } 454 455 /// Nodes implementing this interface can react to scroll motion if selected by a `HoverIO` system. 456 /// 457 /// Temporarily called `HoverScrollable`, this node will be renamed to `Scrollable` in a future release. 458 /// https://git.samerion.com/Samerion/Fluid/issues/278 459 interface HoverScrollable { 460 461 import fluid.node; 462 463 /// Determines whether this node can accept scroll input and if the input can have visible 464 /// effect. This is usually determined by the node's position; for example a container node 465 /// already scrolled to the bottom cannot accept further vertical movement down. 466 /// 467 /// This property is used to determine which node should be used to accept scroll. If there's 468 /// a scrollable container nested in another scrollable node, it will be chosen for scrolling 469 /// only if the scroll motion can still be performed. On the other hand, if the intent is 470 /// specifically to block scroll motion (like in a modal window), this method should always 471 /// return true. 472 /// 473 /// Note that a node "can scroll" even if it can only accept part of the motion. If the scroll 474 /// would have the node scroll beyond its maximum value, but the node is not already at its 475 /// maximum, it should accept the input and clamp the value. 476 /// 477 /// Params: 478 /// pointer = `HoverPointer` used to perform the action. Its most important property 479 /// is the `scroll` field, but some nodes may also filter based on the pointer's 480 /// state. 481 /// Returns: 482 /// True if the node can accept the scroll value in part or in whole, 483 /// false if the motion would have no effect. 484 bool canScroll(const HoverPointer pointer) const; 485 486 /// Perform a scroll motion, moving the node's contents by the specified distance. 487 /// 488 /// Params: 489 /// pointer = Pointer to use to perform the scroll. 490 /// Returns: 491 /// True if the motion was handled, or false if not. 492 bool scrollImpl(HoverPointer pointer); 493 494 /// Scroll towards a specified child node, trying to get it into view. 495 /// 496 /// Params: 497 /// child = Target node, a child of this node. Ideally, this node should appear on 498 /// screen as a consequence of this action. 499 /// parentBox = Padding box of this node, the node performing the scroll. 500 /// childBox = Known padding box of the target child node. 501 /// Returns: 502 /// A new padding box for the child node after applying scroll. 503 Rectangle shallowScrollTo(const Node child, Rectangle parentBox, Rectangle childBox); 504 505 /// Memory safe and `const` object comparison. 506 /// Returns: 507 /// True if this, and the other object, are the same object. 508 /// Params: 509 /// other = Object to compare to. 510 bool opEquals(const Object other) const; 511 512 } 513 514 /// Cast the node to given type if it accepts scroll. 515 /// 516 /// In addition to performing a dynamic cast, this checks if the node can handle a specified 517 /// scroll value according to its `HoverScrollable.canScroll` method. If it doesn't, it will fail 518 /// the cast. 519 /// 520 /// Params: 521 /// node = Node to cast. 522 /// pointer = Pointer used for scrolling. Different values may be accepted depending on the 523 /// scroll value or position. 524 /// Returns: 525 /// Node casted to `Scrollable`, or null if the node can't be casted, or the motion would 526 /// not have effect. 527 inout(HoverScrollable) castIfAcceptsScroll(inout Object node, HoverPointer pointer) { 528 529 // Perform the cast 530 if (auto scrollable = cast(inout HoverScrollable) node) { 531 532 // Node must accept scroll 533 if (scrollable.canScroll(pointer)) { 534 return scrollable; 535 } 536 537 } 538 539 return null; 540 541 } 542 543 /// Find the topmost node that occupies the given position on the screen, along with its scrollable. 544 /// 545 /// The result may change while the search runs; the final result is available once the action stops. 546 /// 547 /// On top of finding the node at specified position, a scroll value can be passed through the 548 /// pointer so this action will also find any `Scrollable` ancestor present in the branch, if one 549 /// can handle the motion. The hovered node and the scrollable node can be the same. 550 /// 551 /// For backwards compatibility, this node is not currently registered as a `NodeSearchAction` and 552 /// does not emit a node when done. 553 final class FindHoveredNodeAction : BranchAction { 554 555 import fluid.node; 556 557 public { 558 559 /// If a node was found, this is the result. 560 Node result; 561 562 /// Topmost scrollable ancestor of `result` (the chosen node). 563 HoverScrollable scrollable; 564 565 /// Position to use for the lookup. The pointer determines the current position and scroll 566 /// value of interest. 567 HoverPointer pointer; 568 569 } 570 571 private { 572 int _transparentDepth; 573 574 /// Incremented for every beforeDraw, decremented in afterDraw; never negative. Reset to 575 /// zero as soon as a new `result` is found, so that all ancestors have a zero value. 576 /// Non-zero for every sibling node of `result` or siblings of its ancestors. 577 /// 578 /// Used to find `scrollable`: scrollable nodes are only checked if _siblingDepth is 579 /// zero. 580 int _siblingDepth; 581 invariant(_siblingDepth >= 0); 582 } 583 584 this(HoverPointer pointer = HoverPointer.init) { 585 this.pointer = pointer; 586 } 587 588 override void started() { 589 super.started(); 590 this.result = null; 591 this.scrollable = null; 592 this._transparentDepth = 0; 593 } 594 595 /// Test if the searched position is within the bounds of this node, and set it as the result 596 /// if so. Any previously found result is overridden. 597 /// 598 /// If a node is found, `scrollable` is cleared. A new one will be found in `afterDraw`. 599 /// 600 /// Because of how layering works in Fluid, the last node in bounds will be the result. This 601 /// action cannot quit early as any node can override the current hover. 602 override void beforeDraw(Node node, Rectangle, Rectangle outer, Rectangle inner) { 603 604 _siblingDepth++; 605 606 if (_transparentDepth) { 607 _transparentDepth++; 608 return; 609 } 610 611 const inBounds = node.inBounds(outer, inner, pointer.position); 612 613 // Children cannot be hovered 614 if (!inBounds.inChildren) { 615 _transparentDepth++; 616 } 617 618 // Check if the position is in bounds of the node 619 if (!inBounds.inSelf) return; 620 621 // Save the result 622 result = node; 623 624 // Clear scrollable 625 scrollable = null; 626 _siblingDepth = 0; 627 628 // Do not stop; the result may be overridden 629 630 } 631 632 /// Find a matching scrollable for the node. The topmost ancestor of `result` (the chosen 633 /// node) will be used. 634 override void afterDraw(Node node, Rectangle) { 635 636 scope (exit) { 637 if (_siblingDepth != 0) { 638 _siblingDepth--; 639 } 640 } 641 642 // A result is required and no scrollable could have matched already 643 if (result is null) return; 644 if (scrollable) return; 645 646 // Reduce _transparentDepth, skip unless the node that started it 647 if (_transparentDepth != 0) { 648 _transparentDepth--; 649 if (_transparentDepth != 0) return; 650 } 651 652 // Ignore if in a sibling node 653 if (_siblingDepth != 0) return; 654 655 // Try to match this node 656 scrollable = node.castIfAcceptsScroll(pointer); 657 658 } 659 660 } 661 662 /// Create a virtual Hover I/O pointer for testing, and place it at the given position. 663 /// Interactions on the pointer are asynchronous and should be performed by `then` chains, see 664 /// `fluid.future.pipe`. 665 /// 666 /// The pointer is disabled after every interaction, but it will be automatically re-enabled after 667 /// every movement. 668 /// 669 /// See_Also: 670 /// `pointAndClick` 671 /// Params: 672 /// hoverIO = Hover I/O system to target. 673 /// position = Position to place the pointer at. 674 /// x = X position to place the pointer at. 675 /// y = Y position to place the pointer at. 676 /// Returns: 677 /// An instance of `HoverPointerAction`. 678 HoverPointerAction point(HoverIO hoverIO, Vector2 position) { 679 auto action = new HoverPointerAction(hoverIO); 680 action.move(position); 681 return action; 682 } 683 684 /// ditto 685 HoverPointerAction point(HoverIO hoverIO, float x, float y) { 686 return point(hoverIO, Vector2(x, y)); 687 } 688 689 /// Create a virtual Hover I/O pointer and use it to click a given position. This is a helper 690 /// wrapping `point`. 691 /// 692 /// "Clicking" is equivalent to sending a `FluidInputAction.press` event. 693 /// 694 /// See_Also: 695 /// `point` 696 /// Params; 697 /// hoverIO = Hover I/O system to target. 698 /// position = Position to click. 699 /// x = X position to click. 700 /// y = Y position to click. 701 /// isActive = If true (default), sends an active event. 702 /// clickCount = If set to 2, imitate a double click, if 3, a triple click and so on. 703 /// Returns: 704 /// A `Publisher` that produces `HoverPointerAction`. 705 Publisher!HoverPointerAction pointAndClick(HoverIO hoverIO, Vector2 position, 706 bool isActive = true, int clickCount = 1) 707 do { 708 return hoverIO.point(position) 709 .then((a) { 710 a.click(isActive, clickCount); 711 return a; 712 }); 713 } 714 715 /// ditto 716 Publisher!HoverPointerAction pointAndClick(HoverIO hoverIO, float x, float y, 717 bool isActive = true, int clickCount = 1) 718 do { 719 return pointAndClick(hoverIO, Vector2(x, y), isActive, clickCount); 720 } 721 722 /// Virtual Hover I/O pointer, for testing. 723 class HoverPointerAction : TreeAction, Publisher!HoverPointerAction, IO { 724 725 import fluid.node; 726 727 public { 728 729 /// Pointer this action operates on. 730 HoverPointer pointer; 731 732 } 733 734 private { 735 736 /// Hover I/O interface the action interacts with. 737 HoverIO hoverIO; 738 739 /// Hover I/O casted to a node 740 Node _node; 741 742 Event!HoverPointerAction _onInteraction; 743 744 } 745 746 alias then = typeof(super).then; 747 alias then = Publisher!HoverPointerAction.then; 748 749 this(HoverIO hoverIO) { 750 751 this.hoverIO = hoverIO; 752 this._node = cast(Node) hoverIO; 753 this.pointer.device = this; 754 assert(_node, "Given Hover I/O is not a valid node"); 755 756 } 757 758 override bool opEquals(const Object other) const { 759 return this is other; 760 } 761 762 override inout(TreeContext) treeContext() inout { 763 return hoverIO.treeContext; 764 } 765 766 void subscribe(Subscriber!HoverPointerAction subscriber) { 767 _onInteraction ~= subscriber; 768 } 769 770 override void clearSubscribers() { 771 super.clearSubscribers(); 772 _onInteraction.clearSubscribers(); 773 } 774 775 /// Returns: 776 /// Currently hovered node, if any. 777 Hoverable currentHover() { 778 779 hoverIO.loadTo(pointer); 780 return hoverIO.hoverOf(pointer); 781 782 } 783 784 /// Returns: 785 /// Chosen scrollable, if any. 786 HoverScrollable currentScroll() { 787 788 hoverIO.loadTo(pointer); 789 return hoverIO.scrollOf(pointer); 790 791 } 792 793 /// Returns: True if the given node is hovered. 794 bool isHovered(const Hoverable hoverable) { 795 796 if (hoverable is null) 797 return currentHover is null; 798 else 799 return currentHover && currentHover.opEquals(cast(const Object) hoverable); 800 801 } 802 803 /// Don't move the pointer, but keep it active. 804 /// Returns: This action, for chaining. 805 HoverPointerAction stayIdle() return { 806 807 clearSubscribers(); 808 _node.startAction(this); 809 810 // Place the pointer 811 pointer.isDisabled = false; 812 hoverIO.loadTo(pointer); 813 814 return this; 815 816 } 817 818 /// Move the pointer to given position. 819 /// Params: 820 /// position = Position to move the pointer to. 821 /// Returns: 822 /// This action, for chaining. 823 HoverPointerAction move(Vector2 position) return { 824 825 clearSubscribers(); 826 _node.startAction(this); 827 828 // Place the pointer 829 pointer.isDisabled = false; 830 pointer.position = position; 831 hoverIO.loadTo(pointer); 832 833 return this; 834 835 } 836 837 /// ditto 838 HoverPointerAction move(float x, float y) return { 839 840 return move(Vector2(x, y)); 841 842 } 843 844 /// Set a scroll value for the action. 845 /// 846 /// Once the motion is completed, the scroll value will be reset and will not apply for future actions. 847 /// 848 /// Params: 849 /// motion = Distance to scroll. 850 /// Returns: 851 /// This action, for chaining. 852 HoverPointerAction scroll(Vector2 motion) return { 853 854 clearSubscribers(); 855 _node.startAction(this); 856 857 // Place the pointer 858 pointer.isDisabled = false; 859 pointer.scroll = motion; 860 hoverIO.loadTo(pointer); 861 862 return this; 863 864 } 865 866 /// ditto 867 HoverPointerAction scroll(float x, float y) return { 868 869 return scroll(Vector2(x, y)); 870 871 } 872 873 /// Hold the scroll control in place. This makes it possible to continue scrolling a single node while 874 /// moving the cursor, which is commonly the scrolling behavior of touchscreens. 875 /// 876 /// The hold status will be reset after a frame. 877 HoverPointerAction holdScroll(Vector2 motion, bool value = true) return { 878 pointer.isScrollHeld = value; 879 return scroll(motion); 880 } 881 882 /// ditto 883 HoverPointerAction holdScroll(float x, float y, bool value = true) return { 884 return holdScroll(Vector2(x, y), value); 885 } 886 887 /// Run an input action on the currently hovered node, if any. 888 /// 889 /// For this to work, `HoverIO` this pointer operates on must also support `ActionHoverIO`. 890 /// 891 /// Params: 892 /// actionID = ID of the action to run. 893 /// isActive = "Active" status of the action. 894 /// Returns: 895 /// True if the action was handled, false if not. 896 bool runInputAction(immutable InputActionID actionID, bool isActive = true) { 897 898 hoverIO.loadTo(pointer); 899 auto hoverable = hoverIO.hoverOf(pointer); 900 901 auto actionHoverIO = cast(ActionHoverIO) hoverIO; 902 assert(actionHoverIO, "This HoverIO does not support dispatching input actions."); 903 904 // Emit a matching, fake hover event 905 // If HoverIO uses ActionIO, ActionIO should recognize and prioritize this event 906 const event = ActionIO.noopEvent(isActive); 907 hoverIO.emitEvent(pointer, event); 908 909 return actionHoverIO.runInputAction(pointer, actionID, isActive); 910 911 } 912 913 /// ditto 914 bool runInputAction(alias action)(bool isActive = true) { 915 916 alias actionID = inputActionID!action; 917 918 return runInputAction(actionID, isActive); 919 920 } 921 922 /// Perform a left click. 923 /// Params: 924 /// isActive = Trigger input actions (like a mouse release event) if true, emulate holding if false. 925 /// clickCount = Set to 2 to emulate a double click, 3 to emulate a triple click, etc. 926 /// Returns: 927 /// True if the action was handled, false if not. 928 bool click(bool isActive = true, int clickCount = 1) { 929 930 pointer.clickCount = clickCount; 931 return runInputAction!(FluidInputAction.press)(isActive); 932 933 } 934 935 /// Perform a double (`doubleClick`) or triple click (`tripleClick`) using the primary press action. 936 /// Params: 937 /// isActive = Trigger input actions (like a mouse release event) if true, emulate holding if false. 938 /// Returns: 939 /// True if the action was handled, false if not. 940 bool doubleClick(bool isActive = true) { 941 return click(isActive, 2); 942 } 943 944 /// ditto 945 bool tripleClick(bool isActive = true) { 946 return click(isActive, 3); 947 } 948 949 alias press = click; 950 951 override void beforeDraw(Node node, Rectangle) { 952 953 // Make sure the pointer is loaded and up to date 954 // If the action was scheduled before a resize, the pointer would die during it 955 if (hoverIO.opEquals(node)) { 956 hoverIO.loadTo(pointer); 957 } 958 959 } 960 961 override void stopped() { 962 963 super.stopped(); 964 965 // Disable the pointer and clear scroll 966 pointer.isDisabled = true; 967 pointer.scroll = Vector2(); 968 pointer.isScrollHeld = false; 969 pointer.clickCount = 0; 970 hoverIO.loadTo(pointer); 971 972 _onInteraction(this); 973 974 } 975 976 }