/************************************************************************************ Copyright : Copyright (c) Facebook Technologies, LLC and its affiliates. All rights reserved. Your use of this SDK or tool is subject to the Oculus SDK License Agreement, available at https://developer.oculus.com/licenses/oculussdk/ Unless required by applicable law or agreed to in writing, the Utilities SDK distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ************************************************************************************/ using System.Collections.Generic; using UnityEngine.EventSystems; using UnityEngine; using UnityEngine.Assertions; using System; namespace Oculus.Interaction { public class PointableCanvasEventArgs { public readonly Canvas Canvas; public readonly GameObject Hovered; public readonly bool Dragging; public PointableCanvasEventArgs(Canvas canvas, GameObject hovered, bool dragging) { Canvas = canvas; Hovered = hovered; Dragging = dragging; } } /// /// IPointerInteractableModule manages all InteractableCanvas events in /// the scene and translates them into pointer events for Unity Canvas UIs. /// public class PointableCanvasModule : PointerInputModule { public static event Action WhenSelected; public static event Action WhenUnselected; public static event Action WhenSelectableHovered; public static event Action WhenSelectableUnhovered; [SerializeField] private bool _useInitialPressPositionForDrag = true; private Camera _pointerEventCamera; private static PointableCanvasModule _instance = null; private static PointableCanvasModule Instance { get { if (_instance == null) { _instance = FindObjectOfType(); } return _instance; } } public static void RegisterPointableCanvas(IPointableCanvas pointerCanvas) { Assert.IsNotNull(Instance, "A PointableCanvasModule is required in the scene."); Instance.AddPointerCanvas(pointerCanvas); } public static void UnregisterPointableCanvas(IPointableCanvas pointerCanvas) { Instance?.RemovePointerCanvas(pointerCanvas); } private Dictionary _pointerMap = new Dictionary(); private List _raycastResultCache = new List(); private List _pointersForDeletion = new List(); private Dictionary> _pointerCanvasActionMap = new Dictionary>(); private void AddPointerCanvas(IPointableCanvas pointerCanvas) { Action pointerCanvasAction = (args) => HandlePointerEvent(pointerCanvas.Canvas, args); _pointerCanvasActionMap.Add(pointerCanvas, pointerCanvasAction); pointerCanvas.OnPointerEvent += pointerCanvasAction; } private void RemovePointerCanvas(IPointableCanvas pointerCanvas) { Action pointerCanvasAction = _pointerCanvasActionMap[pointerCanvas]; _pointerCanvasActionMap.Remove(pointerCanvas); pointerCanvas.OnPointerEvent -= pointerCanvasAction; List pointerIDs = new List(_pointerMap.Keys); foreach (int pointerID in pointerIDs) { Pointer pointer = _pointerMap[pointerID]; if (pointer.Canvas != pointerCanvas.Canvas) { continue; } pointer.MarkForDeletion(); _pointersForDeletion.Add(pointer); _pointerMap.Remove(pointerID); } } private void HandlePointerEvent(Canvas canvas, PointerArgs args) { Pointer pointer; switch (args.PointerEvent) { case PointerEvent.Hover: pointer = new Pointer(canvas); pointer.PointerEventData = new PointerEventData(eventSystem); pointer.SetPosition(args.Position); _pointerMap.Add(args.Identifier, pointer); break; case PointerEvent.Unhover: pointer = _pointerMap[args.Identifier]; _pointerMap.Remove(args.Identifier); pointer.MarkForDeletion(); _pointersForDeletion.Add(pointer); break; case PointerEvent.Select: pointer = _pointerMap[args.Identifier]; pointer.SetPosition(args.Position); pointer.Press(); break; case PointerEvent.Unselect: pointer = _pointerMap[args.Identifier]; pointer.SetPosition(args.Position); pointer.Release(); break; case PointerEvent.Move: pointer = _pointerMap[args.Identifier]; pointer.SetPosition(args.Position); break; } } /// /// Pointer class that is used for state associated with IPointables that are currently /// tracked by any IPointableCanvases in the scene. /// private class Pointer { public PointerEventData PointerEventData { get; set; } public bool MarkedForDeletion { get; private set; } private Canvas _canvas; public Canvas Canvas => _canvas; private Vector3 _position; public Vector3 Position => _position; private GameObject _hoveredSelectable; public GameObject HoveredSelectable => _hoveredSelectable; private bool _pressing = false; private bool _pressed; private bool _released; public Pointer(Canvas canvas) { _canvas = canvas; _pressed = _released = false; } public void Press() { if (_pressing) return; _pressing = true; _pressed = true; } public void Release() { if (!_pressing) return; _pressing = false; _released = true; } public void ReadAndResetPressedReleased(out bool pressed, out bool released) { pressed = _pressed; released = _released; _pressed = _released = false; } public void MarkForDeletion() { MarkedForDeletion = true; Release(); } public void SetPosition(Vector3 position) { _position = position; } public void SetHoveredSelectable(GameObject hoveredSelectable) { _hoveredSelectable = hoveredSelectable; } } protected bool _started = false; protected override void Start() { this.BeginStart(ref _started, base.Start); this.EndStart(ref _started); } protected override void OnEnable() { base.OnEnable(); if (_started) { _pointerEventCamera = gameObject.AddComponent(); _pointerEventCamera.nearClipPlane = 0.1f; // We do not need this camera to be enabled to serve this module's purposes: // as a dependency for Canvases and for its WorldToScreenPoint functionality _pointerEventCamera.enabled = false; } } protected override void OnDisable() { if (_started) { Destroy(_pointerEventCamera); _pointerEventCamera = null; } base.OnDisable(); } // Based On FindFirstRaycast protected static RaycastResult FindFirstRaycastWithinCanvas(List candidates, Canvas canvas) { GameObject candidateGameObject; Canvas candidateCanvas; for (var i = 0; i < candidates.Count; ++i) { candidateGameObject = candidates[i].gameObject; if (candidateGameObject == null) continue; candidateCanvas = candidateGameObject.GetComponentInParent(); if (candidateCanvas == null) continue; if (candidateCanvas.rootCanvas != canvas) continue; return candidates[i]; } return new RaycastResult(); } private void UpdateRaycasts(Pointer pointer, out bool pressed, out bool released) { PointerEventData pointerEventData = pointer.PointerEventData; Vector2 prevPosition = pointerEventData.position; Canvas canvas = pointer.Canvas; canvas.worldCamera = _pointerEventCamera; pointerEventData.Reset(); pointer.ReadAndResetPressedReleased(out pressed, out released); Vector3 position = Vector3.zero; var plane = new Plane(-1f * canvas.transform.forward, canvas.transform.position); var ray = new Ray(pointer.Position - canvas.transform.forward, canvas.transform.forward); float enter; if (plane.Raycast(ray, out enter)) { position = ray.GetPoint(enter); } // We need to position our camera at an offset from the Pointer position or else // a graphic raycast may ignore a world canvas that's outside of our regular camera view(s) _pointerEventCamera.transform.position = pointer.Position - canvas.transform.forward; _pointerEventCamera.transform.LookAt(pointer.Position, canvas.transform.up); Vector2 pointerPosition = _pointerEventCamera.WorldToScreenPoint(position); pointerEventData.position = pointerPosition; // RaycastAll raycasts against with every GraphicRaycaster in the scene, // including nested ones like in the case of a dropdown eventSystem.RaycastAll(pointerEventData, _raycastResultCache); RaycastResult firstResult = FindFirstRaycastWithinCanvas(_raycastResultCache, canvas); pointer.PointerEventData.pointerCurrentRaycast = firstResult; _raycastResultCache.Clear(); // We use a static translation offset from the canvas for 2D position delta tracking _pointerEventCamera.transform.position = canvas.transform.position - canvas.transform.forward; _pointerEventCamera.transform.LookAt(canvas.transform.position, canvas.transform.up); pointerPosition = _pointerEventCamera.WorldToScreenPoint(position); pointerEventData.position = pointerPosition; if (pressed) { pointerEventData.delta = Vector2.zero; } else { pointerEventData.delta = pointerEventData.position - prevPosition; } pointerEventData.button = PointerEventData.InputButton.Left; } public override void Process() { foreach (Pointer pointer in _pointersForDeletion) { ProcessPointer(pointer, true); } _pointersForDeletion.Clear(); foreach (Pointer pointer in _pointerMap.Values) { ProcessPointer(pointer); } } private void ProcessPointer(Pointer pointer, bool forceRelease = false) { bool pressed = false; bool released = false; bool wasDragging = pointer.PointerEventData.dragging; UpdateRaycasts(pointer, out pressed, out released); PointerEventData pointerEventData = pointer.PointerEventData; UpdatePointerEventData(pointerEventData, pressed, released); released |= forceRelease; if (!released) { ProcessMove(pointerEventData); ProcessDrag(pointerEventData); } else { HandlePointerExitAndEnter(pointerEventData, null); RemovePointerData(pointerEventData); } HandleSelectableHover(pointer, wasDragging); HandleSelectablePress(pointer, pressed, released, wasDragging); } private void HandleSelectableHover(Pointer pointer, bool wasDragging) { bool dragging = pointer.PointerEventData.dragging || wasDragging; GameObject currentOverGo = pointer.PointerEventData.pointerCurrentRaycast.gameObject; GameObject prevHoveredSelectable = pointer.HoveredSelectable; GameObject newHoveredSelectable = ExecuteEvents.GetEventHandler(currentOverGo); pointer.SetHoveredSelectable(newHoveredSelectable); if (newHoveredSelectable != null && newHoveredSelectable != prevHoveredSelectable) { WhenSelectableHovered?.Invoke(new PointableCanvasEventArgs(pointer.Canvas, pointer.HoveredSelectable, dragging)); } else if (prevHoveredSelectable != null && newHoveredSelectable == null) { WhenSelectableUnhovered?.Invoke(new PointableCanvasEventArgs(pointer.Canvas, pointer.HoveredSelectable, dragging)); } } private void HandleSelectablePress(Pointer pointer, bool pressed, bool released, bool wasDragging) { bool dragging = pointer.PointerEventData.dragging || wasDragging; if (pressed) { WhenSelected?.Invoke(new PointableCanvasEventArgs(pointer.Canvas, pointer.HoveredSelectable, dragging)); } else if (released && !pointer.MarkedForDeletion) { // Unity handles UI selection on release, so we verify the hovered element has been selected bool hasSelectedHoveredObject = pointer.HoveredSelectable != null && pointer.HoveredSelectable == pointer.PointerEventData.selectedObject; GameObject selectedObject = hasSelectedHoveredObject ? pointer.HoveredSelectable : null; WhenUnselected?.Invoke(new PointableCanvasEventArgs(pointer.Canvas, selectedObject, dragging)); } } /// /// This method is based on ProcessTouchPoint in StandaloneInputModule, /// but is instead used for Pointer events /// protected void UpdatePointerEventData(PointerEventData pointerEvent, bool pressed, bool released) { var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject; // PointerDown notification if (pressed) { pointerEvent.eligibleForClick = true; pointerEvent.delta = Vector2.zero; pointerEvent.dragging = false; pointerEvent.useDragThreshold = true; pointerEvent.pressPosition = pointerEvent.position; pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast; DeselectIfSelectionChanged(currentOverGo, pointerEvent); if (pointerEvent.pointerEnter != currentOverGo) { // send a pointer enter to the touched element if it isn't the one to select... HandlePointerExitAndEnter(pointerEvent, currentOverGo); pointerEvent.pointerEnter = currentOverGo; } // search for the control that will receive the press // if we can't find a press handler set the press // handler to be what would receive a click. var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler); // didnt find a press handler... search for a click handler if (newPressed == null) newPressed = ExecuteEvents.GetEventHandler(currentOverGo); float time = Time.unscaledTime; if (newPressed == pointerEvent.lastPress) { var diffTime = time - pointerEvent.clickTime; if (diffTime < 0.3f) ++pointerEvent.clickCount; else pointerEvent.clickCount = 1; pointerEvent.clickTime = time; } else { pointerEvent.clickCount = 1; } pointerEvent.pointerPress = newPressed; pointerEvent.rawPointerPress = currentOverGo; pointerEvent.clickTime = time; // Save the drag handler as well pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler(currentOverGo); if (pointerEvent.pointerDrag != null) ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag); } // PointerUp notification if (released) { ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler); // see if we mouse up on the same element that we clicked on... var pointerUpHandler = ExecuteEvents.GetEventHandler(currentOverGo); // PointerClick and Drop events if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick) { ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler); } if (pointerEvent.pointerDrag != null && pointerEvent.dragging) { ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler); } pointerEvent.eligibleForClick = false; pointerEvent.pointerPress = null; pointerEvent.rawPointerPress = null; if (pointerEvent.pointerDrag != null && pointerEvent.dragging) ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler); pointerEvent.dragging = false; pointerEvent.pointerDrag = null; // send exit events as we need to simulate this on touch up on touch device ExecuteEvents.ExecuteHierarchy(pointerEvent.pointerEnter, pointerEvent, ExecuteEvents.pointerExitHandler); pointerEvent.pointerEnter = null; } } /// /// Override of PointerInputModule's ProcessDrag to allow using the initial press position for drag begin. /// Set _useInitialPressPositionForDrag to false if you prefer the default behaviour of PointerInputModule. /// protected override void ProcessDrag(PointerEventData pointerEvent) { if (!pointerEvent.IsPointerMoving() || Cursor.lockState == CursorLockMode.Locked || pointerEvent.pointerDrag == null) return; if (!pointerEvent.dragging && ShouldStartDrag(pointerEvent.pressPosition, pointerEvent.position, eventSystem.pixelDragThreshold, pointerEvent.useDragThreshold)) { if (_useInitialPressPositionForDrag) { pointerEvent.position = pointerEvent.pressPosition; } ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.beginDragHandler); pointerEvent.dragging = true; } // Drag notification if (pointerEvent.dragging) { // Before doing drag we should cancel any pointer down state // And clear selection! if (pointerEvent.pointerPress != pointerEvent.pointerDrag) { ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler); pointerEvent.eligibleForClick = false; pointerEvent.pointerPress = null; pointerEvent.rawPointerPress = null; } ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.dragHandler); } } /// /// Used in PointerInputModule's ProcessDrag implementation. Brought into this subclass with a protected /// signature (as opposed to the parent's private signature) to be used in this subclass's overridden ProcessDrag. /// protected static bool ShouldStartDrag(Vector2 pressPos, Vector2 currentPos, float threshold, bool useDragThreshold) { if (!useDragThreshold) return true; return (pressPos - currentPos).sqrMagnitude >= threshold * threshold; } } }