From 8a01c0a7abea38be2cab3f22d848cc13ca355dce Mon Sep 17 00:00:00 2001 From: Artha Date: Thu, 16 Jan 2025 15:36:41 +0100 Subject: [PATCH 1/6] Implement MultipleClickSensor --- source/fluid/io/preference.d | 229 ++++++++++++++++++++++++++++++++ source/fluid/preference_chain.d | 8 +- 2 files changed, 235 insertions(+), 2 deletions(-) diff --git a/source/fluid/io/preference.d b/source/fluid/io/preference.d index 04f0189..5e64e1d 100644 --- a/source/fluid/io/preference.d +++ b/source/fluid/io/preference.d @@ -3,6 +3,7 @@ module fluid.io.preference; import core.time; +import fluid.types; import fluid.future.context; @safe: @@ -31,4 +32,232 @@ interface PreferenceIO : IO { /// The double click interval. Duration doubleClickInterval() const nothrow; + /// Get the maximum distance allowed between two clicks for them to count as a double click. + /// + /// Many systems do not provide this value, so it may be necessary to make a guess. + /// This is typically a small value around 5 pixels. + /// + /// Returns: + /// Maximum distance a pointer can travel before dismissing a double click. + float maximumDoubleClickDistance() const nothrow; + +} + +/// Helper struct to detect double clicks, triple clicks, or overall repeated clicks. +/// +/// To make use of the sensor, you need to call its two methods — `hold` and `activate` — whenever the relevant +/// event occurs. `hold` should be called for every instance, whereas `activate` should only be called if the +/// event is active. For example, to implement double clicks via mouse using input actions, you'd need to implement +/// two input handlers: +/// +/// --- +/// mixin enableInputActions; +/// TimeIO timeIO; +/// PreferenceIO preferenceIO; +/// DoubleClickSensor sensor; +/// +/// override void resizeImpl(Vector2) { +/// require(timeIO); +/// require(preferenceIO); +/// } +/// +/// @(FluidInputAction.press, WhileHeld) +/// override bool hold(Pointer pointer) { +/// sensor.hold(timeIO, preferenceIO, pointer); +/// } +/// +/// @(FluidInputAction.press) +/// override bool press(Pointer) { +/// sensor.activate(); +/// } +/// --- +struct MultipleClickSensor { + + import fluid.io.time; + import fluid.io.action; + import fluid.io.hover; + + /// Number of registered clicks. + int clicks; + + /// Time the event has last triggered. Following an activated click event, this is only updated once. + private MonoTime _lastClickTime; + private Vector2 _lastPosition; + private bool _down; + + /// Call this function every time the desired click event is emitted. + /// + /// This overload accepts `TimeIO` and `ActionIO` systems and reads their properties to determine + /// the right values. If for some reason you cannot use these systems, use the other overload instead. + /// + /// Params: + /// timeIO = Time I/O system. + /// preferenceIO = User preferences I/O system. + /// pointer = Pointer emitting the event. + void hold(TimeIO timeIO, PreferenceIO preferenceIO, Pointer pointer) { + return hold( + timeIO.now, + preferenceIO.doubleClickInterval, + preferenceIO.maximumDoubleClickDistance, + pointer.position + ); + + } + + /// Call this function every time the desired click event is emitted. + /// + /// This overload accepts raw values for settings. You should use Fluid's I/O systems where possible, + /// so the other overload is preferable over this one. + /// + /// Params: + /// currentTime = Current time in the system. + /// doubleClickInterval = Maximum time allowed between two clicks. + /// maximumDistance = Maximum distance the pointer can travel before. + /// pointerPosition = Position of the pointer emitting the event. + void hold(MonoTime currentTime, Duration doubleClickInterval, float maximumDistance, Vector2 pointerPosition) { + + import fluid.utils : distance2; + + if (_down) return; + + const shouldReset = currentTime - _lastClickTime > doubleClickInterval + || distance2(pointerPosition, _lastPosition) > maximumDistance^^2; + + // Reset clicks if enough time has passed, or if the cursor has gone too far + if (shouldReset) { + clicks = 1; + } + else { + clicks++; + } + + // Update values + _down = true; + _lastClickTime = currentTime; + _lastPosition = pointerPosition; + + } + + /// Call this function every time the desired click event is active. + void activate() { + _down = false; + } + +} + +@("MultipleClickSensor can detect double clicks") +unittest { + + MultipleClickSensor sensor; + MonoTime start; + const interval = 500.msecs; + const maxDistance = 5; + const position = Vector2(); + + sensor.hold(start + 0.msecs, interval, maxDistance, position); + assert(sensor.clicks == 1); + sensor.activate(); + assert(sensor.clicks == 1); + + sensor.hold(start + 140.msecs, interval, maxDistance, position); + assert(sensor.clicks == 2); + sensor.activate(); + assert(sensor.clicks == 2); + +} + +@("MultipleClickSensor can detect triple clicks") +unittest { + + MultipleClickSensor sensor; + MonoTime start; + const interval = 500.msecs; + const maxDistance = 5; + const position = Vector2(); + + sensor.hold(start + 0.msecs, interval, maxDistance, position); + assert(sensor.clicks == 1); + sensor.activate(); + assert(sensor.clicks == 1); + + sensor.hold(start + 300.msecs, interval, maxDistance, position); + assert(sensor.clicks == 2); + sensor.activate(); + assert(sensor.clicks == 2); + + sensor.hold(start + 600.msecs, interval, maxDistance, position); + assert(sensor.clicks == 3); + sensor.activate(); + assert(sensor.clicks == 3); + +} + +@("MultipleClickSensor checks doubleClickInterval") +unittest { + + MultipleClickSensor sensor; + MonoTime start; + const interval = 500.msecs; + const maxDistance = 5; + const position = Vector2(); + + sensor.hold(start + 0.msecs, interval, maxDistance, position); + assert(sensor.clicks == 1); + sensor.activate(); + assert(sensor.clicks == 1); + + sensor.hold(start + 600.msecs, interval, maxDistance, position); + assert(sensor.clicks == 1); + sensor.activate(); + assert(sensor.clicks == 1); + +} + +@("MultipleClickSensor checks maxDistance") +unittest { + + MultipleClickSensor sensor; + MonoTime start; + const interval = 500.msecs; + const maxDistance = 5; + const position1 = Vector2(0, 0); + const position2 = Vector2(5, 5); + + sensor.hold(start + 0.msecs, interval, maxDistance, position1); + assert(sensor.clicks == 1); + sensor.activate(); + assert(sensor.clicks == 1); + + sensor.hold(start + 200.msecs, interval, maxDistance, position2); + assert(sensor.clicks == 1); + sensor.activate(); + assert(sensor.clicks == 1); + +} + +@("MultipleClickSensor allows for dragging") +unittest { + + MultipleClickSensor sensor; + MonoTime start; + const interval = 500.msecs; + const maxDistance = 5; + const position1 = Vector2(0, 0); + const position2 = Vector2(3, 0); + const position3 = Vector2(5, 5); + const position4 = Vector2(10, 11); + + sensor.hold(start + 0.msecs, interval, maxDistance, position1); + assert(sensor.clicks == 1); + sensor.activate(); + assert(sensor.clicks == 1); + + sensor.hold(start + 200.msecs, interval, maxDistance, position2); + assert(sensor.clicks == 2); + sensor.hold(start + 250.msecs, interval, maxDistance, position3); + assert(sensor.clicks == 2); + sensor.hold(start + 300.msecs, interval, maxDistance, position4); + assert(sensor.clicks == 2); + sensor.activate(); + } diff --git a/source/fluid/preference_chain.d b/source/fluid/preference_chain.d index 6500a9e..ee53f01 100644 --- a/source/fluid/preference_chain.d +++ b/source/fluid/preference_chain.d @@ -18,8 +18,8 @@ alias preferenceChain = nodeBuilder!PreferenceChain; /// of Fluid programs. /// /// Currently, `PreferenceChain` does *not* communicate with the system, and instead assumes a default value of 400 -/// milliseconds for the double-click interval. Communicating with the system will be enabled in a future update -/// through a `version` flag. +/// milliseconds for the double-click interval, and 6 pixels for the maximum double click distance. Communicating +/// with the system will be enabled in a future update through a `version` flag. class PreferenceChain : NodeChain, PreferenceIO { this(Node next = null) { @@ -41,4 +41,8 @@ class PreferenceChain : NodeChain, PreferenceIO { return 400.msecs; } + override float maximumDoubleClickDistance() const nothrow { + return 6; + } + } From d001aa5657bab88d9a1f299e43b1e788c3509ec0 Mon Sep 17 00:00:00 2001 From: Artha Date: Thu, 16 Jan 2025 15:54:51 +0100 Subject: [PATCH 2/6] Add Pointer.clickCount --- source/fluid/io/hover.d | 40 +++++++++++++++--------------------- source/fluid/io/preference.d | 9 +++++++- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/source/fluid/io/hover.d b/source/fluid/io/hover.d index 2909430..5b77180 100644 --- a/source/fluid/io/hover.d +++ b/source/fluid/io/hover.d @@ -226,6 +226,12 @@ struct Pointer { /// Current scroll value. For a mouse, this indicates mouse wheel movement, for other devices like touchpad or /// touchscreen, this will be translated from its movement. /// + /// This value indicates the distance and direction in window space that the scroll should result in covering. + /// This means that on the X axis negative values move left and positive values move right, while on the Y axis + /// negative values go upwards and positive values go downwards. + /// For example, a scroll value of `(0, 20)` scrolls 20 pixels down vertically, while `(0, -10)` scrolls 10 pixels + /// up. + /// /// While it is possible to read scroll of the `Pointer` data received in an input action handler, /// it is recommended to implement scroll through `Scrollable.scrollImpl`. /// @@ -234,6 +240,16 @@ struct Pointer { /// It is also possible for a device to perform both horizontal and vertical movement at once. Vector2 scroll; + /// True if the pointer is not currently pointing, like a finger that stopped touching the screen. + bool isDisabled; + + /// Consecutive click counter. A value of 1 represents a single click, 2 is a double click, 3 is a triple click, + /// and so on. The counter should reset after a small delay, or if a distance threshold is crossed. + /// + /// This value is usually provided by the system. If unavailable, you can use + /// `fluid.io.preference.MultipleClickCounter` to generate this value from data available to Fluid. + int clickCount; + /// If true, the scroll control is held, like a finger swiping through the screen. This does not apply to mouse /// wheels. /// @@ -245,36 +261,12 @@ struct Pointer { /// [autoscroll]: (https://chromewebstore.google.com/detail/autoscroll/occjjkgifpmdgodlplnacmkejpdionan) bool isScrollHeld; - /// True if the pointer is not currently pointing, like a finger that stopped touching the screen. - bool isDisabled; - /// `HoverIO` system controlling the pointer. private HoverIO _hoverIO; /// ID of the pointer assigned by the `HoverIO` system. private int _id; - /// Create a new pointer. - /// Params: - /// device = I/O system representing the device that created the pointer. - /// number = Pointer number as assigned by the device. - /// position = Position of the pointer. - /// isDisabled = If true, disable the node. - this(inout IO device, int number, Vector2 position, Vector2 scroll = Vector2.init, bool isDisabled = false) inout { - this.device = device; - this.number = number; - this.position = position; - this.scroll = scroll; - this.isDisabled = isDisabled; - } - - /// Create a new pointer. - /// Params: - /// position = Position of the pointer. - this(Vector2 position) { - this.position = position; - } - /// If the given system is a Hover I/O system, fetch a pointer. /// /// Given data must be valid; the I/O must be a `HoverIO` instance and the number must be a valid pointer number. diff --git a/source/fluid/io/preference.d b/source/fluid/io/preference.d index 5e64e1d..abdc0eb 100644 --- a/source/fluid/io/preference.d +++ b/source/fluid/io/preference.d @@ -85,6 +85,12 @@ struct MultipleClickSensor { private Vector2 _lastPosition; private bool _down; + /// Clear the counter, resetting click count to 0. + void clear() { + clicks = 0; + _down = false; + } + /// Call this function every time the desired click event is emitted. /// /// This overload accepts `TimeIO` and `ActionIO` systems and reads their properties to determine @@ -121,7 +127,8 @@ struct MultipleClickSensor { if (_down) return; const shouldReset = currentTime - _lastClickTime > doubleClickInterval - || distance2(pointerPosition, _lastPosition) > maximumDistance^^2; + || distance2(pointerPosition, _lastPosition) > maximumDistance^^2 + || clicks == 0; // Reset clicks if enough time has passed, or if the cursor has gone too far if (shouldReset) { From bb2e9873ca6a8635424e74dfc81e0ae6b0d14f3b Mon Sep 17 00:00:00 2001 From: Artha Date: Thu, 16 Jan 2025 16:36:15 +0100 Subject: [PATCH 3/6] Implement multiclicking in RaylibView --- source/fluid/io/hover.d | 1 + source/fluid/io/preference.d | 16 ++++++++--- source/fluid/raylib_view.d | 52 ++++++++++++++++++++++++++---------- source/fluid/text_input.d | 6 ++++- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/source/fluid/io/hover.d b/source/fluid/io/hover.d index 5b77180..9c144f0 100644 --- a/source/fluid/io/hover.d +++ b/source/fluid/io/hover.d @@ -340,6 +340,7 @@ struct Pointer { this.scroll = other.scroll; this.isScrollHeld = other.isScrollHeld; this.isDisabled = other.isDisabled; + this.clickCount = other.clickCount; } /// Emit an event through the pointer. diff --git a/source/fluid/io/preference.d b/source/fluid/io/preference.d index abdc0eb..caae26f 100644 --- a/source/fluid/io/preference.d +++ b/source/fluid/io/preference.d @@ -97,9 +97,10 @@ struct MultipleClickSensor { /// the right values. If for some reason you cannot use these systems, use the other overload instead. /// /// Params: - /// timeIO = Time I/O system. - /// preferenceIO = User preferences I/O system. - /// pointer = Pointer emitting the event. + /// timeIO = Time I/O system. + /// preferenceIO = User preferences I/O system. + /// pointer = Pointer emitting the event. + /// pointerPosition = Alternatively to `pointer`, just the pointer's position. void hold(TimeIO timeIO, PreferenceIO preferenceIO, Pointer pointer) { return hold( timeIO.now, @@ -107,7 +108,16 @@ struct MultipleClickSensor { preferenceIO.maximumDoubleClickDistance, pointer.position ); + } + /// ditto + void hold(TimeIO timeIO, PreferenceIO preferenceIO, Vector2 pointerPosition) { + return hold( + timeIO.now, + preferenceIO.doubleClickInterval, + preferenceIO.maximumDoubleClickDistance, + pointerPosition + ); } /// Call this function every time the desired click event is emitted. diff --git a/source/fluid/raylib_view.d b/source/fluid/raylib_view.d index b0bd2a6..7bfbdc1 100644 --- a/source/fluid/raylib_view.d +++ b/source/fluid/raylib_view.d @@ -55,6 +55,7 @@ import fluid.io.focus; import fluid.io.keyboard; import fluid.io.clipboard; import fluid.io.image_load; +import fluid.io.preference; static if (!__traits(compiles, IsShaderReady)) private alias IsShaderReady = IsShaderValid; @@ -66,9 +67,9 @@ static if (!__traits(compiles, IsShaderReady)) /// /// Specify Raylib version by using a member: `raylibStack.v5_5()` will create a stack for Raylib 5.5. /// -/// `raylibStack` provides a default implementation for `TimeIO`, `HoverIO`, `FocusIO`, `ActionIO` and `FileIO`, -/// on top of all the systems provided by Raylib itself: `CanvasIO`, `KeyboardIO`, `MouseIO`, `ClipboardIO` -/// and `ImageLoadIO`. +/// `raylibStack` provides a default implementation for `TimeIO`, `PreferenceIO`, `HoverIO`, `FocusIO`, `ActionIO` +/// and `FileIO`, on top of all the systems provided by Raylib itself: `CanvasIO`, `KeyboardIO`, `MouseIO`, +/// `ClipboardIO` and `ImageLoadIO`. enum raylibStack = RaylibViewBuilder!RaylibStack.init; /// `raylibView` implements some I/O functionality using the Raylib library, namely `CanvasIO`, `KeyboardIO`, @@ -102,13 +103,14 @@ struct RaylibViewBuilder(alias T) { /// * `HoverIO` for mouse support. Fluid does not presently support mobile devices through Raylib, and Raylib's /// desktop version does not fully support touchscreens (as GLFW does not). /// * `FocusIO` for keyboard and gamepad support. Gamepad support may currently be limited. +/// * `TimeIO` for measuring time between mouse clicks. +/// * `PreferenceIO` for user preferences from the system. /// /// There is a few systems that `RaylibView` does not require, but are included in `RaylibStack` to support /// commonly needed functionality: /// /// * `ActionIO` for translating user input into a format Fluid nodes can understand. /// * `FileIO` for loading and writing files. -/// * `TimeIO` for measuring time. /// /// `RaylibView` itself provides a number of I/O systems using functionality from the Raylib library: /// @@ -121,6 +123,8 @@ class RaylibView(RaylibViewVersion raylibVersion) : Node, CanvasIO, MouseIO, Key HoverIO hoverIO; FocusIO focusIO; + TimeIO timeIO; + PreferenceIO preferenceIO; public { @@ -155,6 +159,7 @@ class RaylibView(RaylibViewVersion raylibVersion) : Node, CanvasIO, MouseIO, Key // I/O Pointer _mousePointer; Appender!(KeyboardKey[]) _heldKeys; + MultipleClickSensor _multiClickSensor; } @@ -172,6 +177,8 @@ class RaylibView(RaylibViewVersion raylibVersion) : Node, CanvasIO, MouseIO, Key require(focusIO); require(hoverIO); + require(timeIO); + require(preferenceIO); hoverIO.loadTo(_mousePointer); // Fetch data from Raylib @@ -240,6 +247,18 @@ class RaylibView(RaylibViewVersion raylibVersion) : Node, CanvasIO, MouseIO, Key _mousePointer.position = toFluid(GetMousePosition); _mousePointer.scroll = scroll(); _mousePointer.isScrollHeld = false; + _mousePointer.clickCount = 0; + + // Detect multiple mouse clicks + if (IsMouseButtonDown(MouseButton.MOUSE_BUTTON_LEFT)) { + _multiClickSensor.hold(timeIO, preferenceIO, _mousePointer); + _mousePointer.clickCount = _multiClickSensor.clicks; + } + else if (IsMouseButtonReleased(MouseButton.MOUSE_BUTTON_LEFT)) { + _multiClickSensor.activate(); + _mousePointer.clickCount = _multiClickSensor.clicks; + } + hoverIO.loadTo(_mousePointer); // Set cursor icon @@ -698,14 +717,15 @@ class RaylibView(RaylibViewVersion raylibVersion) : Node, CanvasIO, MouseIO, Key /// information. /// /// On top of systems already provided by `RaylibView`, `RaylibStack` also includes `HoverIO`, `FocusIO`, `ActionIO`, -/// `TimeIO` and `FileIO`. You can access them through fields named `hoverIO`, `focusIO`, `actionIO`, `timeIO` -/// and `fileIO` respectively. +/// `PreferenceIO`, `TimeIO` and `FileIO`. You can access them through fields named `hoverIO`, `focusIO`, `actionIO`, +/// `preferenceIO`, `timeIO` and `fileIO` respectively. class RaylibStack(RaylibViewVersion raylibVersion) : Node { - import fluid.time_chain; import fluid.hover_chain; import fluid.focus_chain; import fluid.input_map_chain; + import fluid.preference_chain; + import fluid.time_chain; import fluid.file_chain; public { @@ -719,6 +739,9 @@ class RaylibStack(RaylibViewVersion raylibVersion) : Node { /// ditto InputMapChain actionIO; + /// ditto + PreferenceChain preferenceIO; + /// ditto TimeChain timeIO; @@ -736,12 +759,13 @@ class RaylibStack(RaylibViewVersion raylibVersion) : Node { this(Node next) { chain( - timeIO = timeChain(), - actionIO = inputMapChain(), - focusIO = focusChain(), - hoverIO = hoverChain(), - fileIO = fileChain(), - raylibIO = raylibView(next), + preferenceIO = preferenceChain(), + timeIO = timeChain(), + actionIO = inputMapChain(), + focusIO = focusChain(), + hoverIO = hoverChain(), + fileIO = fileChain(), + raylibIO = raylibView(next), ); } @@ -749,7 +773,7 @@ class RaylibStack(RaylibViewVersion raylibVersion) : Node { /// Returns: /// The first node in the stack. inout(NodeChain) root() inout { - return actionIO; + return preferenceIO; } /// Returns: diff --git a/source/fluid/text_input.d b/source/fluid/text_input.d index 2e1abfb..20fc849 100644 --- a/source/fluid/text_input.d +++ b/source/fluid/text_input.d @@ -1539,7 +1539,7 @@ class TextInput : InputNode!Node, FluidScrollable, HoverScrollable { // Turn on selection from now on, disable it once released selectionMovement = true; - final switch (_clickCount % 3) { + final switch ((pointer.clickCount + 2) % 3) { // First click, merely move the caret while selecting case 0: break; @@ -1572,10 +1572,14 @@ class TextInput : InputNode!Node, FluidScrollable, HoverScrollable { Pointer pointer; pointer.position = io.mousePosition; + pointer.clickCount = _clickCount + 1; if (tree.isMouseActive!(FluidInputAction.press)) { press(pointer); } + + pointer.clickCount = _clickCount + 1; + pressAndHold(pointer); } From 07c65a077bb15a3817c35a33f93ceb96f90fad7a Mon Sep 17 00:00:00 2001 From: Artha Date: Thu, 16 Jan 2025 16:52:59 +0100 Subject: [PATCH 4/6] Implement multiclick in PointerAction --- source/fluid/io/hover.d | 29 ++++++++++++++++++++++------- source/fluid/text_input.d | 3 +++ tests/nodes/text_input.d | 2 +- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/source/fluid/io/hover.d b/source/fluid/io/hover.d index 9c144f0..08b5f6a 100644 --- a/source/fluid/io/hover.d +++ b/source/fluid/io/hover.d @@ -732,12 +732,14 @@ class PointerAction : TreeAction, Publisher!PointerAction, IO { /// Run an input action on the currently hovered node, if any. /// Params: - /// actionID = ID of the action to run. - /// isActive = "Active" status of the action. + /// actionID = ID of the action to run. + /// isActive = "Active" status of the action. + /// clickCount = Set to 2 to simulate a double click, 3 to simulate a triple click, etc. /// Returns: /// True if the action was handled, false if not. - bool runInputAction(immutable InputActionID actionID, bool isActive = true) { + bool runInputAction(immutable InputActionID actionID, bool isActive = true, int clickCount = 1) { + pointer.clickCount = clickCount; hoverIO.loadTo(pointer); auto hoverable = hoverIO.hoverOf(pointer); @@ -757,16 +759,28 @@ class PointerAction : TreeAction, Publisher!PointerAction, IO { } /// ditto - bool runInputAction(alias action)(bool isActive = true) { + bool runInputAction(alias action)(bool isActive = true, int clickCount = 1) { alias actionID = inputActionID!action; - return runInputAction(actionID, isActive); + return runInputAction(actionID, isActive, clickCount); } - /// Shorthand for `runInputAction!(FluidInputAction.press)` - alias press = runInputAction!(FluidInputAction.press); + /// Perform a left click. + /// Params: + /// isActive = Trigger input actions (like a mouse release event) if true, emulate holding if false. + /// clickCount = Set to 2 to emulate a double click, 3 to emulate a triple click, etc. + /// Returns: + /// True if the action was handled, false if not. + bool click(alias action)(bool isActive = true, int clickCount = 1) { + + this.pointer.clickCount = clickCount; + return runInputAction!(FluidInputAction.press)(actionID, isActive); + + } + + alias press = click; override void beforeDraw(Node node, Rectangle) { @@ -786,6 +800,7 @@ class PointerAction : TreeAction, Publisher!PointerAction, IO { pointer.isDisabled = true; pointer.scroll = Vector2(); pointer.isScrollHeld = false; + pointer.clickCount = 0; hoverIO.loadTo(pointer); _onInteraction(this); diff --git a/source/fluid/text_input.d b/source/fluid/text_input.d index 20fc849..b2ee924 100644 --- a/source/fluid/text_input.d +++ b/source/fluid/text_input.d @@ -1539,6 +1539,9 @@ class TextInput : InputNode!Node, FluidScrollable, HoverScrollable { // Turn on selection from now on, disable it once released selectionMovement = true; + // Multi-click not supported + if (pointer.clickCount == 0) return; + final switch ((pointer.clickCount + 2) % 3) { // First click, merely move the caret while selecting diff --git a/tests/nodes/text_input.d b/tests/nodes/text_input.d index 718d1da..b8c2614 100644 --- a/tests/nodes/text_input.d +++ b/tests/nodes/text_input.d @@ -1390,7 +1390,7 @@ unittest { assert(action.isHovered(input)); - action.press; + action.press(true, i+1); root.draw(); // Double-clicked From 552c33b2b201dcf955a129ee03f8735ba877c852 Mon Sep 17 00:00:00 2001 From: Artha Date: Thu, 16 Jan 2025 16:54:56 +0100 Subject: [PATCH 5/6] Fix TextInput not clearing selection status --- source/fluid/text_input.d | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/fluid/text_input.d b/source/fluid/text_input.d index b2ee924..899facf 100644 --- a/source/fluid/text_input.d +++ b/source/fluid/text_input.d @@ -1591,7 +1591,9 @@ class TextInput : InputNode!Node, FluidScrollable, HoverScrollable { protected override bool hoverImpl() { + // Disable selection when not holding if (hoverIO) { + selectionMovement = false; } return false; From bfb076d1f85ee90e907392f15aa3aed731963f4f Mon Sep 17 00:00:00 2001 From: Artha Date: Thu, 16 Jan 2025 19:27:23 +0100 Subject: [PATCH 6/6] Add ActionIO.noopEvent `noopEvent` is checked first when evaluated by ActionIO. This means a `PointerAction` can run an input action just like it has been triggered by a `HoverChain`. --- source/fluid/input_map_chain.d | 9 +++++++-- source/fluid/io/action.d | 37 +++++++++++++++++++++++++++++++--- source/fluid/io/hover.d | 20 +++++++++--------- tests/nodes/text_input.d | 2 +- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/source/fluid/input_map_chain.d b/source/fluid/input_map_chain.d index 9f36f49..18a4ae7 100644 --- a/source/fluid/input_map_chain.d +++ b/source/fluid/input_map_chain.d @@ -81,7 +81,7 @@ class InputMapChain : NodeChain, ActionIO { /// Find the given event type among ones that were emitted this frame. /// Safety: - /// The range has to be exhaused immediately. + /// The range has to be exhausted immediately. /// No input events can be emitted before the range is disposed of, or the range will break. /// Params: /// code = Input event code to find. @@ -93,7 +93,7 @@ class InputMapChain : NodeChain, ActionIO { } - /// Detect all input actions that should be emitted as a consequence of the events that occured this frame. + /// Detect all input actions that should be emitted as a consequence of the events that occurred this frame. /// Clears the current list of events when done. private void processEvents() @trusted { @@ -101,6 +101,11 @@ class InputMapChain : NodeChain, ActionIO { bool handled; + // Test noop event first + foreach (event; findEvents(noopEvent.code)) { + return; + } + // Test all mappings foreach (layer; map.layers) { diff --git a/source/fluid/io/action.d b/source/fluid/io/action.d index 426f64a..918a0e6 100644 --- a/source/fluid/io/action.d +++ b/source/fluid/io/action.d @@ -30,9 +30,35 @@ interface ActionIO : IO { } - /// Create an input event to which `ActionIO` should always have bound to the `CoreAction.frame` input action. + enum Event { + noopEvent, + frameEvent, + } + + /// Create an input event which should never activate any input action. For propagation purposes, this event + /// always counts as handled. + /// + /// The usual purpose of this event is to prevent input actions from running, assuming the `ActionIO` system's + /// logic stops once an event is handled. For example, `fluid.io.hover.PointerAction` emits this event when + /// it is ordered to run an input action, effectively overriding `ActionIO`'s response. + /// + /// See_Also: + /// `frameEvent` + /// Params: + /// isActive = Should the input event be marked as active or not. Defaults to true. + /// Returns: + /// An instance of `Event.noopEvent`. + static InputEvent noopEvent(bool isActive = true) { + + const code = InputEventCode(ioID!ActionIO, Event.noopEvent); + + return InputEvent(code, isActive); + + } + + /// Create an input event to which `ActionIO` should always bind to the `CoreAction.frame` input action. /// Consequently, `ActionIO` always responds with a `CoreAction.frame` input action after processing remaining - /// input actions. + /// input actions. This can be cancelled by emitting a `noopEvent` before the `frameEvent` is handled. /// /// This can be used by device and input handling I/Os to detect the moment after which all input actions have /// been processed. This means that it can be used to develop fallback mechanisms like `hoverImpl` @@ -40,9 +66,14 @@ interface ActionIO : IO { /// /// Note that `CoreAction.frame` might, or might not, be emitted if another action event has been emitted during /// the same frame. `InputMapChain` will only emit `CoreAction.frame` is no other input action has been handled. + /// + /// See_Also: + /// `noopEvent` + /// Returns: + /// An instance of `Event.frameEvent`. static InputEvent frameEvent() { - const code = InputEventCode(ioID!ActionIO, 1); + const code = InputEventCode(ioID!ActionIO, Event.frameEvent); const isActive = false; return InputEvent(code, isActive); diff --git a/source/fluid/io/hover.d b/source/fluid/io/hover.d index 08b5f6a..fba0283 100644 --- a/source/fluid/io/hover.d +++ b/source/fluid/io/hover.d @@ -734,18 +734,16 @@ class PointerAction : TreeAction, Publisher!PointerAction, IO { /// Params: /// actionID = ID of the action to run. /// isActive = "Active" status of the action. - /// clickCount = Set to 2 to simulate a double click, 3 to simulate a triple click, etc. /// Returns: /// True if the action was handled, false if not. - bool runInputAction(immutable InputActionID actionID, bool isActive = true, int clickCount = 1) { + bool runInputAction(immutable InputActionID actionID, bool isActive = true) { - pointer.clickCount = clickCount; hoverIO.loadTo(pointer); auto hoverable = hoverIO.hoverOf(pointer); - // Emit a matching, fake hover event - const code = InputEventCode(ioID!HoverIO, -1); - const event = InputEvent(code, isActive); + // Emit a matching, fake hover event, to inform HoverIO of this + // If HoverIO uses ActionIO, ActionIO should recognize and prioritize this event + const event = ActionIO.noopEvent(isActive); hoverIO.emitEvent(pointer, event); // No hoverable @@ -759,11 +757,11 @@ class PointerAction : TreeAction, Publisher!PointerAction, IO { } /// ditto - bool runInputAction(alias action)(bool isActive = true, int clickCount = 1) { + bool runInputAction(alias action)(bool isActive = true) { alias actionID = inputActionID!action; - return runInputAction(actionID, isActive, clickCount); + return runInputAction(actionID, isActive); } @@ -773,10 +771,10 @@ class PointerAction : TreeAction, Publisher!PointerAction, IO { /// clickCount = Set to 2 to emulate a double click, 3 to emulate a triple click, etc. /// Returns: /// True if the action was handled, false if not. - bool click(alias action)(bool isActive = true, int clickCount = 1) { + bool click(bool isActive = true, int clickCount = 1) { - this.pointer.clickCount = clickCount; - return runInputAction!(FluidInputAction.press)(actionID, isActive); + pointer.clickCount = clickCount; + return runInputAction!(FluidInputAction.press)(isActive); } diff --git a/tests/nodes/text_input.d b/tests/nodes/text_input.d index b8c2614..d3228da 100644 --- a/tests/nodes/text_input.d +++ b/tests/nodes/text_input.d @@ -1341,7 +1341,7 @@ unittest { Rule.selectionBackgroundColor = color("#02a"), ), ), - chain(hover, input) + chain(inputMapChain(), hover, input) ); input.value = "Hello, World! Foo, bar, scroll this input"; input.caretToEnd();