using System; using System.Collections.Generic; using UnityEngine; #if UNITY_2017_2_OR_NEWER using UnityEngine.XR; #endif namespace ZenFulcrum.EmbeddedBrowser { /// /// Base class that handles input for mouse/touch/pointer/VR/nose inputs. /// The concept is thus: /// You have an arbitrary number of 3D (FPS player, VR pointer, and world ray) and /// 2D (mouse, touch, and screen position) pointers and you want any of them /// to be able to interact with the Browser. /// /// Concrete implementations of this class handle interacting with different rendered mediums /// (such as a mesh or a GUI renderer). /// [RequireComponent(typeof(Browser))] public abstract class PointerUIBase : MonoBehaviour, IBrowserUI { public readonly KeyEvents keyEvents = new KeyEvents(); protected Browser browser; protected bool appFocused = true; /// /// Called once per tick. Handlers registered here should look at the pointers they have /// and tell us about them. /// public event Action onHandlePointers = () => {}; protected int currentPointerId; protected readonly List currentPointers = new List(); [Tooltip( "When clicking, how far (in browser-space pixels) must the cursor be moved before we send this movement to the browser backend? " + "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." )] public float dragMovementThreshold = 0; /// /// Cursor location when we most recently went from 0 buttons to any buttons down. /// protected Vector2 mouseDownPosition; /// /// True when we have the any mouse button down AND we've moved at least dragMovementThreshold after that. /// protected bool dragging = false; #region Pointer Core public struct PointerState { /// /// Unique value for this pointer, to distinguish it by. Must be > 0. /// public int id; /// /// Is the pointer a 2d or 3d pointer? /// public bool is2D; public Vector2 position2D; public Ray position3D; /// /// Currently depressed "buttons" on this pointer. /// public MouseButton activeButtons; /// /// If the pointer can scroll, delta scrolling values since last frame. /// public Vector2 scrollDelta; } /// /// Called when the browser gets clicked with any mouse button. /// (More precisely, when we go from having no buttons down to 1+ buttons down.) /// public event Action onClick = () => {}; public virtual void Awake() { BrowserCursor = new BrowserCursor(); BrowserCursor.cursorChange += CursorUpdated; InputSettings = new BrowserInputSettings(); browser = GetComponent(); browser.UIHandler = this; onHandlePointers += OnHandlePointers; if (disableMouseEmulation) Input.simulateMouseWithTouches = false; } public virtual void InputUpdate() { keyEvents.InputUpdate(); p_currentDown = p_anyDown = p_currentOver = p_anyOver = -1; currentPointers.Clear(); onHandlePointers(); CalculatePointer(); } public void OnApplicationFocus(bool focused) { appFocused = focused; } /// /// Converts a 2D screen-space coordinate to browser-space coordinates. /// If the given point doesn't map to the browser, return float.NaN for the position. /// /// /// /// protected abstract Vector2 MapPointerToBrowser(Vector2 screenPosition, int pointerId); /// /// Converts a 3D world-space ray to a browser-space coordinate. /// If the given ray doesn't map to the browser, return float.NaN for the position. /// /// /// /// protected abstract Vector2 MapRayToBrowser(Ray worldRay, int pointerId); /// /// Returns the current position+rotation of the active pointer in world space. /// If there is none, or it doesn't make sense to map to world space, position will /// be NaNs. /// /// Coordinates are in world space. The rotation should point up in the direction the browser sees as up. /// Z+ should go "into" the browser surface. /// /// /// public abstract void GetCurrentHitLocation(out Vector3 pos, out Quaternion rot); /** Indexes into currentPointers for useful items this frame. -1 if N/A. */ protected int p_currentDown, p_anyDown, p_currentOver, p_anyOver; /// /// Feeds the state of the given pointer into the handler. /// /// public virtual void FeedPointerState(PointerState state) { if (state.is2D) state.position2D = MapPointerToBrowser(state.position2D, state.id); else { Debug.DrawRay(state.position3D.origin, state.position3D.direction * (Mathf.Min(500, maxDistance)), Color.cyan); state.position2D = MapRayToBrowser(state.position3D, state.id); //Debug.Log("Pointer " + state.id + " at " + state.position3D.origin + " pointing " + state.position3D.direction + " maps to " + state.position2D); } if (float.IsNaN(state.position2D.x)) return; if (state.id == currentPointerId) { p_currentOver = currentPointers.Count; if (state.activeButtons != 0) p_currentDown = currentPointers.Count; } else { p_anyOver = currentPointers.Count; if (state.activeButtons != 0) p_anyDown = currentPointers.Count; } currentPointers.Add(state); } protected virtual void CalculatePointer() { // if (!appFocused) { // MouseIsOff(); // return; // } /* * The position/priority we feed to the browser is determined in this order: * - Pointer we used earlier with a button down * - Pointer with a button down * - Pointer we used earlier * - Any pointer that is over the browser * Pointers that aren't over the browser are ignored. * If multiple pointers meet the criteria we may pick any that qualify. */ PointerState stateToUse; if (p_currentDown >= 0) { //last frame's pointer with a button down stateToUse = currentPointers[p_currentDown]; } else if (p_anyDown >= 0) { //mouse button count became > 0 this frame stateToUse = currentPointers[p_anyDown]; } else if (p_currentOver >= 0) { //just hovering (use the pointer from last frame) stateToUse = currentPointers[p_currentOver]; } else if (p_anyOver >= 0) { //just hovering (use any pointer over us) stateToUse = currentPointers[p_anyOver]; } else { //no pointers over us MouseIsOff(); return; } MouseIsOver(); if (MouseButtons == 0 && stateToUse.activeButtons != 0) { //no buttons -> 1+ buttons onClick(); //start drag prevention dragging = false; mouseDownPosition = stateToUse.position2D; } if (float.IsNaN(stateToUse.position2D.x)) Debug.LogError("Using an invalid pointer");// "shouldn't happen" if (stateToUse.activeButtons != 0 || MouseButtons != 0) { //Button(s) held or being released, do some extra logic to prevent unintentional dragging during clicks. if (!dragging && stateToUse.activeButtons != 0) {//only check distance if buttons(s) held and not already dragging //Check to see if we passed the drag threshold. var size = browser.Size; var distance = Vector2.Distance( Vector2.Scale(stateToUse.position2D, size),//convert from [0, 1] to pixels Vector2.Scale(mouseDownPosition, size)//convert from [0, 1] to pixels ); if (distance > dragMovementThreshold) { dragging = true; } } if (dragging) MousePosition = stateToUse.position2D; else MousePosition = mouseDownPosition; } else { //no buttons held (or being release), no need to fiddle with the position MousePosition = stateToUse.position2D; } MouseButtons = stateToUse.activeButtons; MouseScroll = stateToUse.scrollDelta; currentPointerId = stateToUse.id; } public void OnGUI() { keyEvents.Feed(Event.current); } protected bool mouseWasOver = false; protected void MouseIsOver() { MouseHasFocus = true; KeyboardHasFocus = true; if (BrowserCursor != null) { CursorUpdated(); } mouseWasOver = true; } protected void MouseIsOff() { // if (BrowserCursor != null && mouseWasOver) { // SetCursor(null); // } mouseWasOver = false; MouseHasFocus = false; if (focusForceCount <= 0) KeyboardHasFocus = false; MouseButtons = 0; MouseScroll = Vector2.zero; currentPointerId = 0; } protected void CursorUpdated() { // SetCursor(BrowserCursor); } private int focusForceCount = 0; /// /// Sets a flag to keep the keyboard focus on this browser, even if it has no pointers. /// Useful for focusing it to type things in via external keyboard. /// /// Call again with force = false to return to the default behavior. (You must call force /// on and force off an equal number of times to revert to the default behavior.) /// /// /// public void ForceKeyboardHasFocus(bool force) { if (force) ++focusForceCount; else --focusForceCount; if (focusForceCount == 1) KeyboardHasFocus = true; else if (focusForceCount == 0) KeyboardHasFocus = false; } #endregion #region Input Handlers [Tooltip("Camera to use to interpret 2D inputs and to originate FPS rays from. Defaults to Camera.main.")] public Camera viewCamera; public bool enableMouseInput = true; public bool enableTouchInput = true; public bool enableFPSInput = false; public bool enableVRInput = false; [Tooltip("(For ray-based interaction) How close must you be to the browser to be able to interact with it?")] public float maxDistance = float.PositiveInfinity; [Space(5)] [Tooltip("Disable Input.simulateMouseWithTouches globally. This will prevent touches from appearing as mouse events.")] public bool disableMouseEmulation = false; protected virtual void OnHandlePointers() { if (enableFPSInput) FeedFPS(); //special case to avoid duplicate pointer from the first touch (ignore mouse) if (enableMouseInput && enableTouchInput && Input.simulateMouseWithTouches && Input.touchCount > 0) { FeedTouchPointers(); return; } if (enableMouseInput) FeedMousePointer(); if (enableTouchInput) FeedTouchPointers(); #if UNITY_2017_2_OR_NEWER if (enableVRInput) FeedVRPointers(); #endif } /// /// Calls FeedPointerState with all the items in Input.touches. /// (Does not happen automatically, call when desired.) /// protected virtual void FeedTouchPointers() { for (int i = 0; i < Input.touchCount; i++) { var touch = Input.GetTouch(i); FeedPointerState(new PointerState { id = 10 + touch.fingerId, is2D = true, position2D = touch.position, activeButtons = (touch.phase == TouchPhase.Began || touch.phase == TouchPhase.Moved || touch.phase == TouchPhase.Stationary) ? MouseButton.Left : 0, }); } } /// /// Calls FeedPointerState with the current mouse state. /// (Does not happen automatically, call when desired.) /// protected virtual void FeedMousePointer() { var buttons = (MouseButton)0; if (Input.GetMouseButton(0)) buttons |= MouseButton.Left; if (Input.GetMouseButton(1)) buttons |= MouseButton.Right; if (Input.GetMouseButton(2)) buttons |= MouseButton.Middle; FeedPointerState(new PointerState { id = 1, is2D = true, position2D = Input.mousePosition, activeButtons = buttons, scrollDelta = Input.mouseScrollDelta, }); } protected virtual void FeedFPS() { var buttons = (Input.GetButton("Fire1") ? MouseButton.Left : 0) | (Input.GetButton("Fire2") ? MouseButton.Right : 0) | (Input.GetButton("Fire3") ? MouseButton.Middle : 0) ; var camera = viewCamera ? viewCamera : Camera.main; var scrollDelta = Input.mouseScrollDelta; //Don't double-count scrolling if we are processing the mouse too. if (enableMouseInput) scrollDelta = Vector2.zero; FeedPointerState(new PointerState { id = 2, is2D = false, position3D = new Ray(camera.transform.position, camera.transform.forward), activeButtons = buttons, scrollDelta = scrollDelta, }); } #if UNITY_2017_2_OR_NEWER protected VRBrowserHand[] vrHands = null; protected virtual void FeedVRPointers() { if (vrHands == null) { vrHands = FindObjectsOfType(); if (vrHands.Length == 0 && XRSettings.enabled) { Debug.LogWarning("VR input is enabled, but no VRBrowserHands were found in the scene", this); } } for (int i = 0; i < vrHands.Length; i++) { if (!vrHands[i].Tracked) continue; FeedPointerState(new PointerState { id = 100 + i, is2D = false, position3D = new Ray(vrHands[i].transform.position, vrHands[i].transform.forward), activeButtons = vrHands[i].DepressedButtons, scrollDelta = vrHands[i].ScrollDelta, }); } } #endif #endregion public virtual bool MouseHasFocus { get; protected set; } public virtual Vector2 MousePosition { get; protected set; } public virtual MouseButton MouseButtons { get; protected set; } public virtual Vector2 MouseScroll { get; protected set; } public virtual bool KeyboardHasFocus { get; protected set; } public virtual List KeyEvents { get { return keyEvents.Events; } } public virtual BrowserCursor BrowserCursor { get; protected set; } public virtual BrowserInputSettings InputSettings { get; protected set; } } }