-
-
Save Invertex/db99b1b16ca53805ae02697b1a51ea77 to your computer and use it in GitHub Desktop.
| using UnityEngine; | |
| using UnityEngine.InputSystem; | |
| //!!>> This script should NOT be placed in an "Editor" folder. Ideally placed in a "Plugins" folder. | |
| namespace Invertex.UnityInputExtensions.Interactions | |
| { | |
| using System; | |
| //https://gist.github.com/Invertex | |
| /// <summary> | |
| /// Custom Hold interaction for New Input System. | |
| /// With this, the .performed callback will be called everytime the Input System updates. | |
| /// Allowing a purely callback based approach to a button hold instead of polling it in an Update() loop or creating specific logic for it | |
| /// .started will be called when the 'pressPoint' threshold has been met and held for the 'duration' (unless 'Trigger .started on Press Point' is checked). | |
| /// .performed will continue to be called each frame after `.started` has triggered (or every amount of time set for "Performed Interval") | |
| /// .cancelled will be called when no-longer actuated (but only if the input has actually 'started' triggering | |
| /// </summary> | |
| #if UNITY_EDITOR | |
| using UnityEditor; | |
| using UnityEngine.UIElements; | |
| //Allow for the interaction to be utilized outside of Play Mode and so that it will actually show up as an option in the Input Manager | |
| [UnityEditor.InitializeOnLoad] | |
| #endif | |
| [UnityEngine.Scripting.Preserve, System.ComponentModel.DisplayName("Holding"), System.Serializable] | |
| public class CustomHoldingInteraction : IInputInteraction | |
| { | |
| /// <summary> | |
| /// Amount of actuation required before a control is considered pressed. | |
| /// </summary> | |
| /// <remarks> | |
| /// If zero (default), defaults to <see cref="InputSettings.defaultButtonPressPoint"/>. | |
| /// </remarks> | |
| [Tooltip("The amount of actuation a control requires before being considered pressed. If not set, default to " | |
| + "'Default Press Point' in the global input settings.")] | |
| public float delayBetweenPerformed = 0f; | |
| public bool triggerStartedOnPressPoint = false; | |
| public bool useDefaultSettingsPressPoint = true; | |
| public float pressPoint = InputSystem.settings.defaultButtonPressPoint; | |
| public bool useDefaultSettingsDuration = true; | |
| public float duration = InputSystem.settings.defaultHoldTime; | |
| private float _heldTime = 0f; | |
| private float pressPointOrDefault => useDefaultSettingsPressPoint || pressPoint <= 0 ? InputSystem.settings.defaultButtonPressPoint : pressPoint; | |
| private float durationOrDefault => useDefaultSettingsDuration || duration < 0 ? InputSystem.settings.defaultHoldTime : duration; | |
| private InputInteractionContext ctx; | |
| private void OnUpdate() | |
| { | |
| var isActuated = ctx.ControlIsActuated(pressPointOrDefault); | |
| var phase = ctx.phase; | |
| //Cancel and cleanup our action if it's no-longer actuated or been externally changed to a stopped state. | |
| if (phase == InputActionPhase.Canceled || phase == InputActionPhase.Disabled || !ctx.action.actionMap.enabled || !isActuated) | |
| { | |
| Cancel(ref ctx); | |
| return; | |
| } | |
| _heldTime += Time.deltaTime; | |
| bool holdDurationElapsed = _heldTime >= durationOrDefault; | |
| if (!holdDurationElapsed && !triggerStartedOnPressPoint) { return; } | |
| if (phase == InputActionPhase.Waiting){ ctx.Started(); return; } | |
| if (!holdDurationElapsed) { return; } | |
| if (phase == InputActionPhase.Started) { ctx.PerformedAndStayPerformed(); return; } | |
| float heldMinusDelay = _heldTime - delayBetweenPerformed; | |
| //Held time has exceed our minimum hold time, plus any delay we've set, | |
| //so perform it and assign back the hold time without the delay time to let increment back up again | |
| if (heldMinusDelay >= durationOrDefault) | |
| { | |
| _heldTime = heldMinusDelay; | |
| ctx.PerformedAndStayPerformed(); | |
| } | |
| } | |
| public void Process(ref InputInteractionContext context) | |
| { | |
| ctx = context; //Ensure our Update always has access to the most recently updated context | |
| if (!ctx.ControlIsActuated(pressPointOrDefault)) { Cancel(ref context); return; } //Actuation changed and thus no longer performed, cancel it all. | |
| if (ctx.phase != InputActionPhase.Performed && ctx.phase != InputActionPhase.Started) | |
| { | |
| EnableInputHooks(); | |
| } | |
| } | |
| private void Cleanup() | |
| { | |
| DisableInputHooks(); | |
| _heldTime = 0f; | |
| } | |
| private void Cancel(ref InputInteractionContext context) | |
| { | |
| Cleanup(); | |
| if (context.phase == InputActionPhase.Performed || context.phase == InputActionPhase.Started) | |
| { //Input was being held when this call was made. Trigger the .cancelled event. | |
| context.Canceled(); | |
| } | |
| } | |
| public void Reset() => Cleanup(); | |
| private void OnLayoutChange(string layoutName, InputControlLayoutChange change) => Reset(); | |
| private void OnDeviceChange(InputDevice device, InputDeviceChange change) => Reset(); | |
| #if UNITY_EDITOR | |
| private void PlayModeStateChange(UnityEditor.PlayModeStateChange state) => Reset(); | |
| #endif | |
| private void EnableInputHooks() | |
| { | |
| InputSystem.onAfterUpdate -= OnUpdate; //Safeguard for duplicate registrations | |
| InputSystem.onAfterUpdate += OnUpdate; | |
| //In case layout or device changes, we'll want to trigger a cancelling of the current input action subscription to avoid errors. | |
| InputSystem.onLayoutChange -= OnLayoutChange; | |
| InputSystem.onLayoutChange += OnLayoutChange; | |
| InputSystem.onDeviceChange -= OnDeviceChange; | |
| InputSystem.onDeviceChange += OnDeviceChange; | |
| //Prevent the update hook from persisting across a play mode change to avoid errors. | |
| #if UNITY_EDITOR | |
| UnityEditor.EditorApplication.playModeStateChanged -= PlayModeStateChange; | |
| UnityEditor.EditorApplication.playModeStateChanged += PlayModeStateChange; | |
| #endif | |
| } | |
| private void DisableInputHooks() | |
| { | |
| InputSystem.onAfterUpdate -= OnUpdate; | |
| InputSystem.onLayoutChange -= OnLayoutChange; | |
| InputSystem.onDeviceChange -= OnDeviceChange; | |
| #if UNITY_EDITOR | |
| UnityEditor.EditorApplication.playModeStateChanged -= PlayModeStateChange; | |
| #endif | |
| } | |
| [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)] | |
| static void RegisterInteraction() | |
| { | |
| if (InputSystem.TryGetInteraction("CustomHolding") == null) | |
| { //For some reason if this is called again when it already exists, it permanently removees it from the drop-down options... So have to check first | |
| InputSystem.RegisterInteraction<CustomHoldingInteraction>("CustomHolding"); | |
| } | |
| } | |
| //Constructor will be called by our Editor [InitializeOnLoad] attribute when outside Play Mode | |
| static CustomHoldingInteraction() => RegisterInteraction(); | |
| } | |
| #if UNITY_EDITOR | |
| internal class CustomHoldInteractionEditor : UnityEngine.InputSystem.Editor.InputParameterEditor<CustomHoldingInteraction> | |
| { | |
| private static GUIContent pressPointWarning, holdTimeWarning, pressPointLabel, holdTimeLabel, startedTriggerOnPPLabel, startedTriggerOnPPToggleLabel, delayPerformedLabel; | |
| #if UNITY_2021_3_OR_NEWER | |
| public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback) | |
| { | |
| var container = new VisualElement(); | |
| var settingsContainer = new VisualElement { style = { flexDirection = FlexDirection.Column, marginRight = 6 } }; | |
| var delayBetweenPerformedFloat = new FloatField(delayPerformedLabel.text) | |
| { | |
| tooltip = delayPerformedLabel.tooltip, | |
| value = target.delayBetweenPerformed | |
| }; | |
| var startTriggerOnPressPointToggle = new Toggle(startedTriggerOnPPLabel.text) | |
| { | |
| tooltip = startedTriggerOnPPLabel.tooltip, | |
| value = target.triggerStartedOnPressPoint | |
| }; | |
| //// PRESS POINT SETTING | |
| var pressPointContainer = new VisualElement { style = { flexDirection = FlexDirection.Row } }; | |
| var pressPointFloat = new FloatField(pressPointLabel.text) | |
| { | |
| tooltip = pressPointLabel.tooltip, | |
| value = target.pressPoint | |
| }; | |
| var pressPointUseDefaultToggle = new Toggle("Default") { value = target.useDefaultSettingsPressPoint }; | |
| var usingDefaultPressPointTip = new HelpBox(pressPointWarning.text, HelpBoxMessageType.None); | |
| var openSettingsBtnPressPoint = new UnityEngine.UIElements.Button(() => { SettingsService.OpenProjectSettings("Project/Input System Package"); }) { text = "Open Input Settings" }; | |
| pressPointFloat.style.flexShrink = 0.2f; | |
| pressPointFloat.style.flexBasis = new StyleLength(200); | |
| pressPointFloat.SetEnabled(!target.useDefaultSettingsPressPoint); | |
| pressPointUseDefaultToggle.style.flexDirection = FlexDirection.RowReverse; | |
| usingDefaultPressPointTip.style.display = target.useDefaultSettingsPressPoint ? DisplayStyle.Flex : DisplayStyle.None; | |
| openSettingsBtnPressPoint.style.display = target.useDefaultSettingsPressPoint ? DisplayStyle.Flex : DisplayStyle.None; | |
| openSettingsBtnPressPoint.style.alignSelf = Align.FlexEnd; | |
| ///// Hold Duration Setting | |
| var holdDurationContainer = new VisualElement { style = { flexDirection = FlexDirection.Row } }; | |
| var holdDurationMinFloat = new FloatField(holdTimeLabel.text) | |
| { | |
| tooltip = holdTimeLabel.tooltip, | |
| value = target.duration | |
| }; | |
| var holdDurationDefaultToggle = new Toggle("Default") { value = target.useDefaultSettingsDuration }; | |
| var usingDefaultHoldDurationTip = new HelpBox(holdTimeWarning.text, HelpBoxMessageType.None); | |
| var openSettingsBtnHoldDuration = new UnityEngine.UIElements.Button(() => { SettingsService.OpenProjectSettings("Project/Input System Package"); }) { text = "Open Input Settings" }; | |
| holdDurationMinFloat.style.flexShrink = 0.2f; | |
| holdDurationMinFloat.style.flexBasis = new StyleLength(200); | |
| holdDurationMinFloat?.SetEnabled(!target.useDefaultSettingsDuration); | |
| holdDurationDefaultToggle.style.flexDirection = FlexDirection.RowReverse; | |
| usingDefaultHoldDurationTip.style.display = target.useDefaultSettingsDuration ? DisplayStyle.Flex : DisplayStyle.None; | |
| openSettingsBtnHoldDuration.style.display = target.useDefaultSettingsDuration ? DisplayStyle.Flex : DisplayStyle.None; | |
| openSettingsBtnHoldDuration.style.alignSelf = Align.FlexEnd; | |
| // Setup Events | |
| delayBetweenPerformedFloat.RegisterValueChangedCallback(evt => { target.delayBetweenPerformed = evt.newValue; }); | |
| delayBetweenPerformedFloat.RegisterCallback<BlurEvent>(_ => { onChangedCallback?.Invoke(); }); | |
| startTriggerOnPressPointToggle.RegisterValueChangedCallback(evt => { | |
| target.triggerStartedOnPressPoint = evt.newValue; | |
| onChangedCallback(); | |
| }); | |
| pressPointFloat.RegisterValueChangedCallback(evt => { | |
| target.pressPoint = evt.newValue; | |
| bool isDefault = evt.newValue < Mathf.Epsilon; | |
| usingDefaultPressPointTip.style.display = isDefault || target.useDefaultSettingsPressPoint ? DisplayStyle.Flex : DisplayStyle.None; | |
| }); | |
| // Trigger onChangedCallback here to avoid it happening with each typed digit and causing stalls | |
| pressPointFloat.RegisterCallback<BlurEvent>(_ => { onChangedCallback?.Invoke(); }); | |
| holdDurationMinFloat.RegisterValueChangedCallback(evt => { | |
| target.duration = evt.newValue; | |
| bool isDefault = evt.newValue <= Mathf.Epsilon; | |
| usingDefaultHoldDurationTip.style.display = isDefault || target.useDefaultSettingsDuration ? DisplayStyle.Flex : DisplayStyle.None; | |
| }); | |
| holdDurationMinFloat.RegisterCallback<BlurEvent>(_ => { onChangedCallback?.Invoke(); }); | |
| pressPointUseDefaultToggle.RegisterValueChangedCallback((evt) => { | |
| target.useDefaultSettingsPressPoint = evt.newValue; | |
| pressPointFloat?.SetEnabled(!evt.newValue); | |
| usingDefaultPressPointTip.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None; | |
| openSettingsBtnPressPoint.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None; | |
| onChangedCallback(); | |
| }); | |
| holdDurationDefaultToggle.RegisterValueChangedCallback((evt) => { | |
| target.useDefaultSettingsDuration = evt.newValue; | |
| holdDurationMinFloat?.SetEnabled(!evt.newValue); | |
| usingDefaultHoldDurationTip.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None; | |
| openSettingsBtnHoldDuration.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None; | |
| onChangedCallback(); | |
| }); | |
| usingDefaultPressPointTip.style.display = target.pressPoint <= Mathf.Epsilon || target.useDefaultSettingsPressPoint ? DisplayStyle.Flex : DisplayStyle.None; | |
| usingDefaultHoldDurationTip.style.display = target.duration <= Mathf.Epsilon || target.useDefaultSettingsDuration ? DisplayStyle.Flex : DisplayStyle.None; | |
| // Begin adding elems | |
| settingsContainer.Add(delayBetweenPerformedFloat); | |
| settingsContainer.Add(startTriggerOnPressPointToggle); | |
| // Press Point Elems | |
| pressPointContainer.Add(pressPointFloat); | |
| pressPointContainer.Add(pressPointUseDefaultToggle); | |
| settingsContainer.Add(pressPointContainer); | |
| settingsContainer.Add(usingDefaultPressPointTip); | |
| settingsContainer.Add(openSettingsBtnPressPoint); | |
| // Hold Duration Elems | |
| holdDurationContainer.Add(holdDurationMinFloat); | |
| holdDurationContainer.Add(holdDurationDefaultToggle); | |
| settingsContainer.Add(holdDurationContainer); | |
| settingsContainer.Add(usingDefaultHoldDurationTip); | |
| settingsContainer.Add(openSettingsBtnHoldDuration); | |
| container.Add(settingsContainer); | |
| root.Add(container); | |
| } | |
| public override void OnGUI() { } | |
| #else | |
| public override void OnGUI() | |
| { | |
| target.delayBetweenPerformed = EditorGUILayout.FloatField(delayPerformedLabel, target.delayBetweenPerformed, GUILayout.ExpandWidth(false)); | |
| EditorGUILayout.BeginHorizontal(); | |
| EditorGUILayout.LabelField(startedTriggerOnPPLabel, GUILayout.Width(205f)); | |
| target.triggerStartedOnPressPoint = GUILayout.Toggle(target.triggerStartedOnPressPoint, startedTriggerOnPPToggleLabel, GUILayout.ExpandWidth(false)); | |
| EditorGUILayout.EndHorizontal(); | |
| DrawDisableIfDefault(ref target.pressPoint, ref target.useDefaultSettingsPressPoint, pressPointLabel, pressPointWarning); | |
| DrawDisableIfDefault(ref target.duration, ref target.useDefaultSettingsDuration, holdTimeLabel, holdTimeWarning, -Mathf.Epsilon); | |
| } | |
| private void DrawDisableIfDefault(ref float value, ref bool useDefault, GUIContent fieldName, GUIContent warningText, float compareOffset = 0f) | |
| { | |
| EditorGUILayout.BeginHorizontal(); | |
| EditorGUI.BeginDisabledGroup(useDefault); | |
| value = EditorGUILayout.FloatField(fieldName, value, GUILayout.ExpandWidth(false)); | |
| value = Mathf.Clamp(value, 0f, float.MaxValue); | |
| EditorGUI.EndDisabledGroup(); | |
| GUIContent content = | |
| EditorGUIUtility.TrTextContent("Default", | |
| $"If enabled, the default {fieldName.text.ToLower()} " + | |
| $"configured globally in the input settings is used.{System.Environment.NewLine}" + | |
| "See Edit >> Project Settings... >> Input System Package."); | |
| useDefault = GUILayout.Toggle(useDefault, content, GUILayout.ExpandWidth(false)); | |
| EditorGUILayout.EndHorizontal(); | |
| if (useDefault || value <= 0 + compareOffset) | |
| { | |
| EditorGUILayout.HelpBox(warningText); | |
| EditorGUILayout.BeginHorizontal(); | |
| GUILayout.FlexibleSpace(); | |
| GUIContent settingsLabel = EditorGUIUtility.TrTextContent("Open Input Settings"); | |
| if (GUILayout.Button(settingsLabel, EditorStyles.miniButton)) | |
| SettingsService.OpenProjectSettings("Project/Input System Package"); | |
| EditorGUILayout.EndHorizontal(); | |
| } | |
| } | |
| #endif | |
| protected override void OnEnable() | |
| { | |
| delayPerformedLabel = new GUIContent("'Performed' interval (s)", $"Delay in seconds between each <b>.performed</b> call.{System.Environment.NewLine}" + | |
| "At the default value of 0, it will be every frame."); | |
| startedTriggerOnPPLabel = new GUIContent("Trigger 'Started' on Press Point", $"Trigger the <b>.started</b> event as soon as input actuated beyond \"Press Point\",{System.Environment.NewLine}" + | |
| $"instead of waiting for the \"Min Hold Time\" as well."); | |
| startedTriggerOnPPToggleLabel = new GUIContent("", startedTriggerOnPPLabel.tooltip); | |
| pressPointLabel = new GUIContent("Press Point", $"The minimum amount this input's actuation value must exceed to be considered \"held\".{System.Environment.NewLine}" + | |
| "Value less-than or equal to 0 will result in the 'Default Button Press Point' value being used from your 'Project Settings > Input System'."); | |
| holdTimeLabel = new GUIContent("Min Hold Time", $"The minimum amount of realtime seconds before the input is considered \"held\".{System.Environment.NewLine}" + | |
| "Value less-than or equal to 0 will result in the 'Default Hold Time' value being used from your 'Project Settings > Input System'."); | |
| pressPointWarning = EditorGUIUtility.TrTextContent("Using \"Default Button Press Point\" set in project-wide input settings."); | |
| holdTimeWarning = EditorGUIUtility.TrTextContent("Using \"Default Hold Time\" set in project-wide input settings."); | |
| } | |
| } | |
| } | |
| #endif |
Thanks for the heads up and figuring out the issue!
I've taken a bit simpler approach to fixing it by separating the cleanup logic from the Cancel and Reset and not making Reset() call Cancel(), since it should already be implied that it is entering the Canceled state if Reset() is being called.
Hopefully it's fully solved :)
I have found that sometimes, when I was messing with multiple input devices at once, and switching back and forth from one to another, the _heldTime variable was not reset. I have dug a bit into it, and it looks like _isCanceled flag was not being correctly reset. I am not an expert, but the only place I can find that _isCanceled is being reset to false is on the Reset() function, but that one has an initial exit clause that breaks out of the function if _isCanceled == true. I have added a reset such as:
if(_isCanceled) { _isCanceled = false; return; }
and this seems to do the trick. Am I missing out on something, any reason this shouldn't be added?
@fralvarezz Thanks for the comment! You seem to be using an outdated version of the script though. The version in this post (July 11th) doesn't use an _isCanceled bool to track that, but relies more on Unity's internal systems instead. Try out the current version and let me know if you still have the issue!
Oh, I'm very sorry, I am not sure how I missed that! I have pulled the latest version, and it seems to be a bit more consistent on my end, although I have managed to trigger a very similar scenario. Actuating and releasing two input devices at a time linked to the same action can lead to a weird state, where not matter what you do or how long you press a given input, the state of the InputActionContext.phase will be "Waiting". Using another input device once will cancel out this behavior and everything will start working great again.
I am aware that this is an edge case scenario and most games don't even allow having multiple devices for the same input action at once, so I'm not expecting anybody to fix it, although it's probably good to know. I'll be looking a bit more into this myself, too, and will try to reproduce it (and hopefully find a solution!) in an empty project that's less cluttered than my game :)
For reference too, I'm on Unity 2020.3.48, and Input System 1.7.0
Great plug-in, thank you!
Error logs are output during Unity playback and application execution.
The CustomHold seems to be working correctly.
Is there any way to prevent the error from occurring?
No IInputInteraction with name 'CustomHolding' (mentioned in 'CustomHolding(delayBetweenPerformed=0.2)') has been registered
UnityEngine.Debug:ExtractStackTraceNoAlloc (byte*,int,string)
UnityEngine.StackTraceUtility:ExtractStackTrace ()
UnityEngine.DebugLogHandler:Internal_Log (UnityEngine.LogType,UnityEngine.LogOption,string,UnityEngine.Object)
UnityEngine.DebugLogHandler:LogFormat (UnityEngine.LogType,UnityEngine.Object,string,object[])
UnityEngine.Logger:Log (UnityEngine.LogType,object)
UnityEngine.Debug:LogError (object)
UnityEngine.InputSystem.InputBindingResolver:InstantiateWithParameters<UnityEngine.InputSystem.IInputInteraction> (UnityEngine.InputSystem.Utilities.TypeTable,string,UnityEngine.InputSystem.IInputInteraction[]&,int&,UnityEngine.InputSystem.InputActionMap,UnityEngine.InputSystem.InputBinding&) (at ./Library/PackageCache/com.unity.inputsystem/InputSystem/Actions/InputBindingResolver.cs:614)
UnityEngine.InputSystem.InputBindingResolver:AddActionMap (UnityEngine.InputSystem.InputActionMap) (at ./Library/PackageCache/com.unity.inputsystem/InputSystem/Actions/InputBindingResolver.cs:335)
UnityEngine.InputSystem.InputActionMap:ResolveBindings () (at ./Library/PackageCache/com.unity.inputsystem/InputSystem/Actions/InputActionMap.cs:1372)
UnityEngine.InputSystem.InputActionMap:ResolveBindingsIfNecessary () (at ./Library/PackageCache/com.unity.inputsystem/InputSystem/Actions/InputActionMap.cs:1237)
UnityEngine.InputSystem.InputActionMap:Enable () (at ./Library/PackageCache/com.unity.inputsystem/InputSystem/Actions/InputActionMap.cs:541)
UnityEngine.InputSystem.InputActionAsset:Enable () (at ./Library/PackageCache/com.unity.inputsystem/InputSystem/Actions/InputActionAsset.cs:804)
UnityEngine.InputSystem.InputSystem:EnableActions () (at ./Library/PackageCache/com.unity.inputsystem/InputSystem/InputSystem.cs:3033)
UnityEngine.InputSystem.InputSystem:Reset (bool,UnityEngine.InputSystem.LowLevel.IInputRuntime) (at ./Library/PackageCache/com.unity.inputsystem/InputSystem/InputSystem.cs:3911)
UnityEngine.InputSystem.InputSystem:InitializeInEditor (UnityEngine.InputSystem.LowLevel.IInputRuntime) (at ./Library/PackageCache/com.unity.inputsystem/InputSystem/InputSystem.cs:3537)
UnityEngine.InputSystem.InputSystem:.cctor () (at ./Library/PackageCache/com.unity.inputsystem/InputSystem/InputSystem.cs:3499)
System.Runtime.CompilerServices.RuntimeHelpers:RunClassConstructor (System.RuntimeTypeHandle)
UnityEditor.EditorAssemblies:ProcessInitializeOnLoadAttributes (System.Type[])
The error was resolved by increasing the version of Unity
The error was resolved by increasing the version of Unity
What version did you upload?
I am using Unity 600.3.7.
Does it sound familiar to someone ?
Might be something they changed in the Input package recently. I'll check it out later today.
Is this after already restarting the Editor? Sometimes on first import it can be kinda messed up.
Not checked after restarting the editor. I will do it.
I am also installing the last version (6000.3.9).
I am not familiar with Custom Interaction but checked your code and the code and didn't found anything obvious for now.
Seems to be at some point OnGUI() was deprecated as an Editor UI rendering option, at least in this context, and are forced to use the newer UIElements API through the OnDrawVisualElements() method.
So I'll have to figure out what version that change happened and update this code to use that new system.
Hah, so they deprecated it back in November and it made it into the 18.0 branch: Unity-Technologies/InputSystem#2283
But it broke a bunch of stuff from them so they have a revert: Unity-Technologies/InputSystem#2347
But that isn't in a released version yet...
I probably should update the code anyways just for futureproofing though.
@rxra Updated it to use the UIElements system. Should hopefully work well!
Ok, it is what I thought (the deprecation) but regarding the doc I was not sure. Perhaps I didn't check the right doc/version.
Thanks I will try it latter today (I try to debug it/fixed/proposed something if I found issue).

Update: After disabling stack traces I found it was a stack overflow going on. I've uploaded a fix here: https://gist.github.com/SenshiSentou/384c5ca22282544cb630d24719301f4e
The gist of it is there is an infinite recursion going on between calling
Reset(), which callsCancel(), which callscontext.Canceled(), which triggers anotherReset(), which repeats the cycle. The quick and dirty fix I implemented adds an_isCanceledflag to prevent this infinite recursion.There is a nicer way to do this however. I noticed
Cancel()'sref contextparameter to be unnecessary, as it can just accessctxalready. I merged it intoReset()in my gist, but there should probably be a proper separation between cancelling the event, and resetting internal state (_heldTimeand the delegates). If I end up refactoring it later I'll post here again :)