PointerUIBase.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. using System;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. #if UNITY_2017_2_OR_NEWER
  5. using UnityEngine.XR;
  6. #endif
  7. namespace ZenFulcrum.EmbeddedBrowser {
  8. /// <summary>
  9. /// Base class that handles input for mouse/touch/pointer/VR/nose inputs.
  10. /// The concept is thus:
  11. /// You have an arbitrary number of 3D (FPS player, VR pointer, and world ray) and
  12. /// 2D (mouse, touch, and screen position) pointers and you want any of them
  13. /// to be able to interact with the Browser.
  14. ///
  15. /// Concrete implementations of this class handle interacting with different rendered mediums
  16. /// (such as a mesh or a GUI renderer).
  17. /// </summary>
  18. [RequireComponent(typeof(Browser))]
  19. public abstract class PointerUIBase : MonoBehaviour, IBrowserUI {
  20. public readonly KeyEvents keyEvents = new KeyEvents();
  21. protected Browser browser;
  22. protected bool appFocused = true;
  23. /// <summary>
  24. /// Called once per tick. Handlers registered here should look at the pointers they have
  25. /// and tell us about them.
  26. /// </summary>
  27. public event Action onHandlePointers = () => {};
  28. protected int currentPointerId;
  29. protected readonly List<PointerState> currentPointers = new List<PointerState>();
  30. [Tooltip(
  31. "When clicking, how far (in browser-space pixels) must the cursor be moved before we send this movement to the browser backend? " +
  32. "This helps keep us from unintentionally dragging when we meant to just click, esp. under VR where it's hard to hold the cursor still."
  33. )]
  34. public float dragMovementThreshold = 0;
  35. /// <summary>
  36. /// Cursor location when we most recently went from 0 buttons to any buttons down.
  37. /// </summary>
  38. protected Vector2 mouseDownPosition;
  39. /// <summary>
  40. /// True when we have the any mouse button down AND we've moved at least dragMovementThreshold after that.
  41. /// </summary>
  42. protected bool dragging = false;
  43. #region Pointer Core
  44. public struct PointerState {
  45. /// <summary>
  46. /// Unique value for this pointer, to distinguish it by. Must be > 0.
  47. /// </summary>
  48. public int id;
  49. /// <summary>
  50. /// Is the pointer a 2d or 3d pointer?
  51. /// </summary>
  52. public bool is2D;
  53. public Vector2 position2D;
  54. public Ray position3D;
  55. /// <summary>
  56. /// Currently depressed "buttons" on this pointer.
  57. /// </summary>
  58. public MouseButton activeButtons;
  59. /// <summary>
  60. /// If the pointer can scroll, delta scrolling values since last frame.
  61. /// </summary>
  62. public Vector2 scrollDelta;
  63. }
  64. /// <summary>
  65. /// Called when the browser gets clicked with any mouse button.
  66. /// (More precisely, when we go from having no buttons down to 1+ buttons down.)
  67. /// </summary>
  68. public event Action onClick = () => {};
  69. public virtual void Awake() {
  70. BrowserCursor = new BrowserCursor();
  71. BrowserCursor.cursorChange += CursorUpdated;
  72. InputSettings = new BrowserInputSettings();
  73. browser = GetComponent<Browser>();
  74. browser.UIHandler = this;
  75. onHandlePointers += OnHandlePointers;
  76. if (disableMouseEmulation) Input.simulateMouseWithTouches = false;
  77. }
  78. public virtual void InputUpdate() {
  79. keyEvents.InputUpdate();
  80. p_currentDown = p_anyDown = p_currentOver = p_anyOver = -1;
  81. currentPointers.Clear();
  82. onHandlePointers();
  83. CalculatePointer();
  84. }
  85. public void OnApplicationFocus(bool focused) {
  86. appFocused = focused;
  87. }
  88. /// <summary>
  89. /// Converts a 2D screen-space coordinate to browser-space coordinates.
  90. /// If the given point doesn't map to the browser, return float.NaN for the position.
  91. /// </summary>
  92. /// <param name="screenPosition"></param>
  93. /// <param name="pointerId"></param>
  94. /// <returns></returns>
  95. protected abstract Vector2 MapPointerToBrowser(Vector2 screenPosition, int pointerId);
  96. /// <summary>
  97. /// Converts a 3D world-space ray to a browser-space coordinate.
  98. /// If the given ray doesn't map to the browser, return float.NaN for the position.
  99. /// </summary>
  100. /// <param name="worldRay"></param>
  101. /// <param name="pointerId"></param>
  102. /// <returns></returns>
  103. protected abstract Vector2 MapRayToBrowser(Ray worldRay, int pointerId);
  104. /// <summary>
  105. /// Returns the current position+rotation of the active pointer in world space.
  106. /// If there is none, or it doesn't make sense to map to world space, position will
  107. /// be NaNs.
  108. ///
  109. /// Coordinates are in world space. The rotation should point up in the direction the browser sees as up.
  110. /// Z+ should go "into" the browser surface.
  111. /// </summary>
  112. /// <param name="pos"></param>
  113. /// <param name="rot"></param>
  114. public abstract void GetCurrentHitLocation(out Vector3 pos, out Quaternion rot);
  115. /** Indexes into currentPointers for useful items this frame. -1 if N/A. */
  116. protected int p_currentDown, p_anyDown, p_currentOver, p_anyOver;
  117. /// <summary>
  118. /// Feeds the state of the given pointer into the handler.
  119. /// </summary>
  120. /// <param name="state"></param>
  121. public virtual void FeedPointerState(PointerState state) {
  122. if (state.is2D) state.position2D = MapPointerToBrowser(state.position2D, state.id);
  123. else {
  124. Debug.DrawRay(state.position3D.origin, state.position3D.direction * (Mathf.Min(500, maxDistance)), Color.cyan);
  125. state.position2D = MapRayToBrowser(state.position3D, state.id);
  126. //Debug.Log("Pointer " + state.id + " at " + state.position3D.origin + " pointing " + state.position3D.direction + " maps to " + state.position2D);
  127. }
  128. if (float.IsNaN(state.position2D.x)) return;
  129. if (state.id == currentPointerId) {
  130. p_currentOver = currentPointers.Count;
  131. if (state.activeButtons != 0) p_currentDown = currentPointers.Count;
  132. } else {
  133. p_anyOver = currentPointers.Count;
  134. if (state.activeButtons != 0) p_anyDown = currentPointers.Count;
  135. }
  136. currentPointers.Add(state);
  137. }
  138. protected virtual void CalculatePointer() {
  139. // if (!appFocused) {
  140. // MouseIsOff();
  141. // return;
  142. // }
  143. /*
  144. * The position/priority we feed to the browser is determined in this order:
  145. * - Pointer we used earlier with a button down
  146. * - Pointer with a button down
  147. * - Pointer we used earlier
  148. * - Any pointer that is over the browser
  149. * Pointers that aren't over the browser are ignored.
  150. * If multiple pointers meet the criteria we may pick any that qualify.
  151. */
  152. PointerState stateToUse;
  153. if (p_currentDown >= 0) {
  154. //last frame's pointer with a button down
  155. stateToUse = currentPointers[p_currentDown];
  156. } else if (p_anyDown >= 0) {
  157. //mouse button count became > 0 this frame
  158. stateToUse = currentPointers[p_anyDown];
  159. } else if (p_currentOver >= 0) {
  160. //just hovering (use the pointer from last frame)
  161. stateToUse = currentPointers[p_currentOver];
  162. } else if (p_anyOver >= 0) {
  163. //just hovering (use any pointer over us)
  164. stateToUse = currentPointers[p_anyOver];
  165. } else {
  166. //no pointers over us
  167. MouseIsOff();
  168. return;
  169. }
  170. MouseIsOver();
  171. if (MouseButtons == 0 && stateToUse.activeButtons != 0) {
  172. //no buttons -> 1+ buttons
  173. onClick();
  174. //start drag prevention
  175. dragging = false;
  176. mouseDownPosition = stateToUse.position2D;
  177. }
  178. if (float.IsNaN(stateToUse.position2D.x)) Debug.LogError("Using an invalid pointer");// "shouldn't happen"
  179. if (stateToUse.activeButtons != 0 || MouseButtons != 0) {
  180. //Button(s) held or being released, do some extra logic to prevent unintentional dragging during clicks.
  181. if (!dragging && stateToUse.activeButtons != 0) {//only check distance if buttons(s) held and not already dragging
  182. //Check to see if we passed the drag threshold.
  183. var size = browser.Size;
  184. var distance = Vector2.Distance(
  185. Vector2.Scale(stateToUse.position2D, size),//convert from [0, 1] to pixels
  186. Vector2.Scale(mouseDownPosition, size)//convert from [0, 1] to pixels
  187. );
  188. if (distance > dragMovementThreshold) {
  189. dragging = true;
  190. }
  191. }
  192. if (dragging) MousePosition = stateToUse.position2D;
  193. else MousePosition = mouseDownPosition;
  194. } else {
  195. //no buttons held (or being release), no need to fiddle with the position
  196. MousePosition = stateToUse.position2D;
  197. }
  198. MouseButtons = stateToUse.activeButtons;
  199. MouseScroll = stateToUse.scrollDelta;
  200. currentPointerId = stateToUse.id;
  201. }
  202. public void OnGUI() {
  203. keyEvents.Feed(Event.current);
  204. }
  205. protected bool mouseWasOver = false;
  206. protected void MouseIsOver() {
  207. MouseHasFocus = true;
  208. KeyboardHasFocus = true;
  209. if (BrowserCursor != null) {
  210. CursorUpdated();
  211. }
  212. mouseWasOver = true;
  213. }
  214. protected void MouseIsOff() {
  215. // if (BrowserCursor != null && mouseWasOver) {
  216. // SetCursor(null);
  217. // }
  218. mouseWasOver = false;
  219. MouseHasFocus = false;
  220. if (focusForceCount <= 0) KeyboardHasFocus = false;
  221. MouseButtons = 0;
  222. MouseScroll = Vector2.zero;
  223. currentPointerId = 0;
  224. }
  225. protected void CursorUpdated() {
  226. // SetCursor(BrowserCursor);
  227. }
  228. private int focusForceCount = 0;
  229. /// <summary>
  230. /// Sets a flag to keep the keyboard focus on this browser, even if it has no pointers.
  231. /// Useful for focusing it to type things in via external keyboard.
  232. ///
  233. /// Call again with force = false to return to the default behavior. (You must call force
  234. /// on and force off an equal number of times to revert to the default behavior.)
  235. ///
  236. /// </summary>
  237. /// <param name="force"></param>
  238. public void ForceKeyboardHasFocus(bool force) {
  239. if (force) ++focusForceCount;
  240. else --focusForceCount;
  241. if (focusForceCount == 1) KeyboardHasFocus = true;
  242. else if (focusForceCount == 0) KeyboardHasFocus = false;
  243. }
  244. #endregion
  245. #region Input Handlers
  246. [Tooltip("Camera to use to interpret 2D inputs and to originate FPS rays from. Defaults to Camera.main.")]
  247. public Camera viewCamera;
  248. public bool enableMouseInput = true;
  249. public bool enableTouchInput = true;
  250. public bool enableFPSInput = false;
  251. public bool enableVRInput = false;
  252. [Tooltip("(For ray-based interaction) How close must you be to the browser to be able to interact with it?")]
  253. public float maxDistance = float.PositiveInfinity;
  254. [Space(5)]
  255. [Tooltip("Disable Input.simulateMouseWithTouches globally. This will prevent touches from appearing as mouse events.")]
  256. public bool disableMouseEmulation = false;
  257. protected virtual void OnHandlePointers() {
  258. if (enableFPSInput) FeedFPS();
  259. //special case to avoid duplicate pointer from the first touch (ignore mouse)
  260. if (enableMouseInput && enableTouchInput && Input.simulateMouseWithTouches && Input.touchCount > 0) {
  261. FeedTouchPointers();
  262. return;
  263. }
  264. if (enableMouseInput) FeedMousePointer();
  265. if (enableTouchInput) FeedTouchPointers();
  266. #if UNITY_2017_2_OR_NEWER
  267. if (enableVRInput) FeedVRPointers();
  268. #endif
  269. }
  270. /// <summary>
  271. /// Calls FeedPointerState with all the items in Input.touches.
  272. /// (Does not happen automatically, call when desired.)
  273. /// </summary>
  274. protected virtual void FeedTouchPointers() {
  275. for (int i = 0; i < Input.touchCount; i++) {
  276. var touch = Input.GetTouch(i);
  277. FeedPointerState(new PointerState {
  278. id = 10 + touch.fingerId,
  279. is2D = true,
  280. position2D = touch.position,
  281. activeButtons = (touch.phase == TouchPhase.Began || touch.phase == TouchPhase.Moved || touch.phase == TouchPhase.Stationary) ? MouseButton.Left : 0,
  282. });
  283. }
  284. }
  285. /// <summary>
  286. /// Calls FeedPointerState with the current mouse state.
  287. /// (Does not happen automatically, call when desired.)
  288. /// </summary>
  289. protected virtual void FeedMousePointer() {
  290. var buttons = (MouseButton)0;
  291. if (Input.GetMouseButton(0)) buttons |= MouseButton.Left;
  292. if (Input.GetMouseButton(1)) buttons |= MouseButton.Right;
  293. if (Input.GetMouseButton(2)) buttons |= MouseButton.Middle;
  294. FeedPointerState(new PointerState {
  295. id = 1,
  296. is2D = true,
  297. position2D = Input.mousePosition,
  298. activeButtons = buttons,
  299. scrollDelta = Input.mouseScrollDelta,
  300. });
  301. }
  302. protected virtual void FeedFPS() {
  303. var buttons =
  304. (Input.GetButton("Fire1") ? MouseButton.Left : 0) |
  305. (Input.GetButton("Fire2") ? MouseButton.Right : 0) |
  306. (Input.GetButton("Fire3") ? MouseButton.Middle : 0)
  307. ;
  308. var camera = viewCamera ? viewCamera : Camera.main;
  309. var scrollDelta = Input.mouseScrollDelta;
  310. //Don't double-count scrolling if we are processing the mouse too.
  311. if (enableMouseInput) scrollDelta = Vector2.zero;
  312. FeedPointerState(new PointerState {
  313. id = 2,
  314. is2D = false,
  315. position3D = new Ray(camera.transform.position, camera.transform.forward),
  316. activeButtons = buttons,
  317. scrollDelta = scrollDelta,
  318. });
  319. }
  320. #if UNITY_2017_2_OR_NEWER
  321. protected VRBrowserHand[] vrHands = null;
  322. protected virtual void FeedVRPointers() {
  323. if (vrHands == null) {
  324. vrHands = FindObjectsOfType<VRBrowserHand>();
  325. if (vrHands.Length == 0 && XRSettings.enabled) {
  326. Debug.LogWarning("VR input is enabled, but no VRBrowserHands were found in the scene", this);
  327. }
  328. }
  329. for (int i = 0; i < vrHands.Length; i++) {
  330. if (!vrHands[i].Tracked) continue;
  331. FeedPointerState(new PointerState {
  332. id = 100 + i,
  333. is2D = false,
  334. position3D = new Ray(vrHands[i].transform.position, vrHands[i].transform.forward),
  335. activeButtons = vrHands[i].DepressedButtons,
  336. scrollDelta = vrHands[i].ScrollDelta,
  337. });
  338. }
  339. }
  340. #endif
  341. #endregion
  342. public virtual bool MouseHasFocus { get; protected set; }
  343. public virtual Vector2 MousePosition { get; protected set; }
  344. public virtual MouseButton MouseButtons { get; protected set; }
  345. public virtual Vector2 MouseScroll { get; protected set; }
  346. public virtual bool KeyboardHasFocus { get; protected set; }
  347. public virtual List<Event> KeyEvents { get { return keyEvents.Events; } }
  348. public virtual BrowserCursor BrowserCursor { get; protected set; }
  349. public virtual BrowserInputSettings InputSettings { get; protected set; }
  350. }
  351. }