DefaultPointerInputDetector.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. // Copyright (c) 2024 Vuplex Inc. All rights reserved.
  2. //
  3. // Licensed under the Vuplex Commercial Software Library License, you may
  4. // not use this file except in compliance with the License. You may obtain
  5. // a copy of the License at
  6. //
  7. // https://vuplex.com/commercial-library-license
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #pragma warning disable CS0414
  15. using System;
  16. using System.Reflection;
  17. using UnityEngine;
  18. using UnityEngine.EventSystems;
  19. using UnityEngine.UI;
  20. using Vuplex.WebView.Internal;
  21. #if VUPLEX_MRTK
  22. using Microsoft.MixedReality.Toolkit.Input;
  23. #endif
  24. namespace Vuplex.WebView {
  25. [HelpURL("https://developer.vuplex.com/webview/IPointerInputDetector")]
  26. public class DefaultPointerInputDetector : MonoBehaviour,
  27. IPointerInputDetector,
  28. #if VUPLEX_MRTK
  29. IMixedRealityPointerHandler,
  30. // When using MRTK, don't implement the standard Unity event interfaces because
  31. // it causes CanvasWebViewPrefab to receive click events twice (once through MRTK and
  32. // once through the standard Unity interfaces).
  33. #else
  34. IBeginDragHandler,
  35. IDragHandler,
  36. IPointerClickHandler,
  37. IPointerDownHandler,
  38. IPointerEnterHandler,
  39. IPointerExitHandler,
  40. #if UNITY_2021_1_OR_NEWER
  41. IPointerMoveHandler,
  42. #endif
  43. IPointerUpHandler,
  44. #endif
  45. IScrollHandler {
  46. public event EventHandler<EventArgs<Vector2>> BeganDrag;
  47. public event EventHandler<EventArgs<Vector2>> Dragged;
  48. public event EventHandler<PointerEventArgs> PointerDown;
  49. public event EventHandler PointerEntered;
  50. public event EventHandler<EventArgs<Vector2>> PointerExited;
  51. public event EventHandler<EventArgs<Vector2>> PointerMoved;
  52. public event EventHandler<PointerEventArgs> PointerUp;
  53. public event EventHandler<ScrolledEventArgs> Scrolled;
  54. public bool PointerMovedEnabled { get; set; }
  55. /// <see cref="IBeginDragHandler"/>
  56. public void OnBeginDrag(PointerEventData eventData) {
  57. _raiseBeganDragEvent(_convertToEventArgs(eventData));
  58. }
  59. /// <see cref="IDragHandler"/>
  60. public void OnDrag(PointerEventData eventData) {
  61. // The point is Vector3.zero when the user drags off of the screen.
  62. if (!_positionIsZero(eventData)) {
  63. _raiseDraggedEvent(_convertToEventArgs(eventData));
  64. }
  65. }
  66. /// <summary>
  67. /// VRIF requires IPointerClickHandler to be implemented in order to detect the object
  68. /// and invoke its OnPointerDown() and OnPointerUp() methods.
  69. /// </summary>
  70. /// <see cref="IPointerClickHandler"/>
  71. public void OnPointerClick(PointerEventData eventData) {}
  72. /// <see cref="IPointerDownHandler"/>
  73. public virtual void OnPointerDown(PointerEventData eventData) {
  74. // StandaloneInputModule and InputSystemUIInputModule both have an issue where clickCount is 1 less than what it should be in OnPointerDown:
  75. // https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-68720
  76. // This issue has been observed in all of the Unity versions tested: 2020.3, 2022.3, 2023.2
  77. // Here's an example of what logging the clickCount in OnPointerDown and OnPointerUp for a double click looks like:
  78. // > OnPointerDown clickCount: 0
  79. // > OnPointerUp clickCount: 1
  80. // > OnPointerDown clickCount: 1
  81. // > OnPointerUp clickCount: 2
  82. // Originally this class tried to compensate for that by adding 1 to the OnPointerDown() clickCount value,
  83. // but Unity 2020.3 and 2021.3 (but not 2022.3) have another issue where after a double click, the OnPointerDown() clickCount value
  84. // is incorrectly set to 2 on the next click after a double click.
  85. // As a workaround, this class rolls its own click count detection instead.
  86. var now = DateTime.Now;
  87. var millisecondsSinceLastPointerDown = (now - _lastPointerDownDateTime).TotalMilliseconds;
  88. _lastPointerDownDateTime = now;
  89. var isDoubleClick = millisecondsSinceLastPointerDown <= 500;
  90. _clickCount = isDoubleClick ? _clickCount + 1 : 1;
  91. _raisePointerDownEvent(_convertToPointerEventArgs(eventData));
  92. }
  93. /// <see cref="IPointerEnterHandler"/>
  94. public void OnPointerEnter(PointerEventData eventData) {
  95. _isHovering = true;
  96. _raisePointerEnteredEvent(EventArgs.Empty);
  97. }
  98. /// <see cref="IPointerExitHandler"/>
  99. public void OnPointerExit(PointerEventData eventData) {
  100. _isHovering = false;
  101. // When StandaloneInputModule triggers OnPointerExit, eventData.pointerCurrentRaycast.worldPosition is usually Vector3.zero,
  102. // so for world space, just fallback to sending a normalized point of Vector2.zero.
  103. var point = _positionIsZero(eventData) ? Vector2.zero : _convertToNormalizedPoint(eventData);
  104. // Since this is an exit event, the coordinate can sometimes be just outside the bounds of [0, 1], so clamp it to [0, 1].
  105. for (var i = 0; i < 2; i++) {
  106. if (point[i] < 0f) {
  107. point[i] = 0f;
  108. } else if (point[i] > 1f) {
  109. point[i] = 1f;
  110. }
  111. }
  112. _raisePointerExitedEvent(new EventArgs<Vector2>(point));
  113. }
  114. /// <see cref="IPointerMoveHandler"/>
  115. public void OnPointerMove(PointerEventData eventData) {
  116. if (!(PointerMovedEnabled && _isHovering)) {
  117. return;
  118. }
  119. var point = _convertToNormalizedPoint(eventData);
  120. if (!(point.x >= 0f && point.y >= 0f)) {
  121. // This can happen while the prefab is being resized.
  122. return;
  123. }
  124. if (_previousPointerMovedPoint == point) {
  125. return;
  126. }
  127. _previousPointerMovedPoint = point;
  128. _raisePointerMovedEvent(new EventArgs<Vector2>(point));
  129. }
  130. /// <see cref="IPointerUpHandler"/>
  131. public virtual void OnPointerUp(PointerEventData eventData) {
  132. _raisePointerUpEvent(_convertToPointerEventArgs(eventData));
  133. }
  134. /// <see cref="IScrollHandler"/>
  135. public void OnScroll(PointerEventData eventData) {
  136. var scrollDelta = -1 * eventData.scrollDelta;
  137. _raiseScrolledEvent(new ScrolledEventArgs(scrollDelta, _convertToNormalizedPoint(eventData)));
  138. }
  139. int _clickCount = 1;
  140. DateTime _lastPointerDownDateTime = DateTime.Now;
  141. bool _isHovering;
  142. Vector2 _previousPointerMovedPoint;
  143. EventArgs<Vector2> _convertToEventArgs(Vector3 worldPosition) {
  144. var screenPoint = _convertToNormalizedPoint(worldPosition);
  145. return new EventArgs<Vector2>(screenPoint);
  146. }
  147. EventArgs<Vector2> _convertToEventArgs(PointerEventData pointerEventData) {
  148. var screenPoint = _convertToNormalizedPoint(pointerEventData);
  149. return new EventArgs<Vector2>(screenPoint);
  150. }
  151. protected virtual Vector2 _convertToNormalizedPoint(PointerEventData pointerEventData) {
  152. return _convertToNormalizedPoint(pointerEventData.pointerCurrentRaycast.worldPosition);
  153. }
  154. protected virtual Vector2 _convertToNormalizedPoint(Vector3 worldPosition) {
  155. // Note: transform.parent is WebViewPrefabResizer
  156. var localPosition = transform.parent.InverseTransformPoint(worldPosition);
  157. var point = new Vector2(1 - localPosition.x, -1 * localPosition.y);
  158. // In some cases, the point may be outside the range of [0, 1], so we need to clamp it to [0, 1]. Scenarios where that's the case:
  159. // - OnPointerExit()
  160. // - OnPointerUp(), if the mouse button is released after dragging outside of the webview.
  161. for (var i = 0; i < 2; i++) {
  162. if (point[i] < 0f) {
  163. point[i] = 0f;
  164. } else if (point[i] > 1f) {
  165. point[i] = 1f;
  166. }
  167. }
  168. return point;
  169. }
  170. PointerEventArgs _convertToPointerEventArgs(PointerEventData eventData) {
  171. return new PointerEventArgs {
  172. Point = _convertToNormalizedPoint(eventData),
  173. Button = (MouseButton)eventData.button,
  174. ClickCount = _clickCount
  175. };
  176. }
  177. PointerEventData _getLastPointerEventData() {
  178. var currentInputModule = EventSystem.current == null ? null : EventSystem.current.currentInputModule;
  179. // Support for input modules that derive from PointerInputModule, like StandaloneInputModule.
  180. var pointerInputModule = currentInputModule as PointerInputModule;
  181. if (pointerInputModule != null) {
  182. // Use reflection to get access to the protected GetPointerData()
  183. // method. Unity isn't going to change this API because most input modules
  184. // extend PointerInputModule. Note that GetPointerData() is used instead
  185. // of GetLastPointerEventData() because the latter doesn't work with
  186. // the Oculus SDK's OVRInputModule.
  187. var args = new object[] { PointerInputModule.kMouseLeftId, null, false };
  188. pointerInputModule.GetType().InvokeMember(
  189. "GetPointerData",
  190. BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.NonPublic,
  191. null,
  192. pointerInputModule,
  193. args
  194. );
  195. // The second argument is an out param.
  196. var pointerEventData = args[1] as PointerEventData;
  197. return pointerEventData;
  198. }
  199. #if ENABLE_INPUT_SYSTEM
  200. // Support for the new InputSystem's InputSystemUIInputModule.
  201. var uiInputModule = currentInputModule as UnityEngine.InputSystem.UI.InputSystemUIInputModule;
  202. if (uiInputModule != null) {
  203. var pointerEventData = new PointerEventData(EventSystem.current);
  204. var raycastResult = uiInputModule.GetLastRaycastResult(0);
  205. pointerEventData.position = raycastResult.screenPosition;
  206. pointerEventData.pointerCurrentRaycast = uiInputModule.GetLastRaycastResult(0);
  207. return pointerEventData;
  208. }
  209. #endif
  210. #if VUPLEX_XR_INTERACTION_TOOLKIT
  211. // Support for XR Interaction Toolkit.
  212. return Internal.XritPointerEventHelper.Instance.LastPointerEventData;
  213. #else
  214. return null;
  215. #endif
  216. }
  217. protected virtual bool _positionIsZero(PointerEventData eventData) => eventData.pointerCurrentRaycast.worldPosition == Vector3.zero;
  218. protected void _raiseBeganDragEvent(EventArgs<Vector2> eventArgs) => BeganDrag?.Invoke(this, eventArgs);
  219. protected void _raiseDraggedEvent(EventArgs<Vector2> eventArgs) => Dragged?.Invoke(this, eventArgs);
  220. protected void _raisePointerDownEvent(PointerEventArgs eventArgs) => PointerDown?.Invoke(this, eventArgs);
  221. protected void _raisePointerEnteredEvent(EventArgs eventArgs) => PointerEntered?.Invoke(this, eventArgs);
  222. protected void _raisePointerExitedEvent(EventArgs<Vector2> eventArgs) => PointerExited?.Invoke(this, eventArgs);
  223. // IPointerMoveHandler was added in Unity 2021.1, so this method attempts to manually call OnPointerMove
  224. // for versions of Unity older than 2021.1.
  225. void _processLegacyPointerMoveHandler() {
  226. var eventData = _getLastPointerEventData();
  227. if (eventData != null) {
  228. OnPointerMove(eventData);
  229. }
  230. }
  231. protected void _raisePointerMovedEvent(EventArgs<Vector2> eventArgs) => PointerMoved?.Invoke(this, eventArgs);
  232. protected void _raisePointerUpEvent(PointerEventArgs eventArgs) => PointerUp?.Invoke(this, eventArgs);
  233. protected void _raiseScrolledEvent(ScrolledEventArgs eventArgs) => Scrolled?.Invoke(this, eventArgs);
  234. protected virtual void Update() {
  235. #if !UNITY_2021_1_OR_NEWER
  236. _processLegacyPointerMoveHandler();
  237. #endif
  238. }
  239. // Code specific to Microsoft's Mixed Reality Toolkit.
  240. #if VUPLEX_MRTK
  241. bool _beganDragEmitted;
  242. /// <see cref="IMixedRealityPointerHandler"/>
  243. public void OnPointerClicked(MixedRealityPointerEventData eventData) {}
  244. /// <see cref="IMixedRealityPointerHandler"/>
  245. public void OnPointerDragged(MixedRealityPointerEventData eventData) {
  246. var eventArgs = _convertToEventArgs(eventData.Pointer.Result.Details.Point);
  247. if (_beganDragEmitted) {
  248. _raiseDraggedEvent(eventArgs);
  249. } else {
  250. _beganDragEmitted = true;
  251. _raiseBeganDragEvent(eventArgs);
  252. }
  253. }
  254. /// <see cref="IMixedRealityPointerHandler"/>
  255. public void OnPointerDown(MixedRealityPointerEventData eventData) {
  256. // Set IsTargetPositionLockedOnFocusLock to false, or else the Point
  257. // coordinates will be locked and won't change in OnPointerDragged or OnPointerUp.
  258. eventData.Pointer.IsTargetPositionLockedOnFocusLock = false;
  259. _beganDragEmitted = false;
  260. var screenPoint = _convertToNormalizedPoint(eventData.Pointer.Result.Details.Point);
  261. _raisePointerDownEvent(new PointerEventArgs { Point = screenPoint });
  262. }
  263. /// <see cref="IMixedRealityPointerHandler"/>
  264. public void OnPointerUp(MixedRealityPointerEventData eventData) {
  265. var screenPoint = _convertToNormalizedPoint(eventData.Pointer.Result.Details.Point);
  266. _raisePointerUpEvent(new PointerEventArgs { Point = screenPoint });
  267. }
  268. void Start() {
  269. WebViewLogger.Log("Just a heads-up: please ignore the warning 'BoxCollider is null...' warning from MRTK. WebViewPrefab doesn't use a BoxCollider, so it sets the bounds of NearInteractionTouchable manually, but MRTK doesn't provide a way to disable the warning.");
  270. // Add a NearInteractionTouchable script to allow touch interactions
  271. // to trigger the IMixedRealityPointerHandler methods.
  272. var touchable = gameObject.AddComponent<NearInteractionTouchable>();
  273. touchable.EventsToReceive = TouchableEventType.Pointer;
  274. touchable.SetBounds(Vector2.one);
  275. touchable.SetLocalForward(new Vector3(0, 0, -1));
  276. }
  277. #endif
  278. }
  279. }