#if PACKAGE_DOCS_GENERATION || UNITY_INPUT_SYSTEM_ENABLE_UI
using System;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.Serialization;
////FIXME: The UI is currently not reacting to pointers until they are moved after the UI module has been enabled. What needs to
//// happen is that point, trackedDevicePosition, and trackedDeviceOrientation have initial state checks. However, for touch,
//// we do *not* want to react to the initial value as then we also get presses (unlike with other pointers). Argh.
////REVIEW: I think this would be much better served by having a composite type input for each of the three basic types of input (pointer, navigation, tracked)
//// I.e. there'd be a PointerInput, a NavigationInput, and a TrackedInput composite. This would solve several problems in one go and make
//// it much more obvious which inputs go together.
////REVIEW: The current input model has too much complexity for pointer input; find a way to simplify this.
////REVIEW: how does this/uGUI support drag-scrolls on touch? [GESTURES]
////REVIEW: how does this/uGUI support two-finger right-clicks with touch? [GESTURES]
////TODO: add ability to query which device was last used with any of the actions
namespace UnityEngine.InputSystem.UI
{
///
/// Input module that takes its input from input actions.
///
///
/// This UI input module has the advantage over other such modules that it doesn't have to know
/// what devices and types of devices input is coming from. Instead, the actions hide the actual
/// sources of input from the module.
///
public class InputSystemUIInputModule : BaseInputModule
{
///
/// Whether to clear the current selection when a click happens that does not hit any GameObject.
///
/// If true (default), clicking outside of any GameObject will reset the current selection.
///
/// By toggling this behavior off, background clicks will keep the current selection. I.e.
/// EventSystem.currentSelectedGameObject will not be changed.
///
public bool deselectOnBackgroundClick
{
get => m_DeselectOnBackgroundClick;
set => m_DeselectOnBackgroundClick = value;
}
///
/// How to deal with the presence of pointer-type input from multiple devices.
///
/// Wh
public UIPointerBehavior pointerBehavior
{
get => m_PointerBehavior;
set => m_PointerBehavior = value;
}
///
/// Called by EventSystem when the input module is made current.
///
public override void ActivateModule()
{
base.ActivateModule();
// Select firstSelectedGameObject if nothing is selected ATM.
var toSelect = eventSystem.currentSelectedGameObject;
if (toSelect == null)
toSelect = eventSystem.firstSelectedGameObject;
eventSystem.SetSelectedGameObject(toSelect, GetBaseEventData());
}
///
/// Check whether the given pointer or touch is currently hovering over a GameObject.
///
/// ID of the pointer or touch. Meaning this should correspond to either
/// PointerEventData.pointerId or . The pointer ID
/// generally corresponds to the of the pointer device. An exception
/// to this are touches as a may have multiple pointers (one for each active
/// finger). For touch, you can use the of the touch.
///
/// To check whether any pointer is over a GameObject, simply pass a negative value such as -1.
/// True if the given pointer is currently hovering over a GameObject.
///
/// The result is true if the given pointer has caused an IPointerEnter event to be sent to a
/// GameObject.
///
/// This method can be invoked via EventSystem.current.IsPointerOverGameObject.
///
///
///
/// // In general, the pointer ID corresponds to the device ID:
/// EventSystem.current.IsPointerOverGameObject(XRController.leftHand.deviceId);
/// EventSystem.current.IsPointerOverGameObject(Mouse.current.deviceId);
///
/// // For touch input, pass the ID of a touch:
/// EventSystem.current.IsPointerOverGameObject(Touchscreen.primaryTouch.touchId.ReadValue());
///
/// // But can also pass the ID of the entire Touchscreen in which case the result
/// // is true if any touch is over a GameObject:
/// EventSystem.current.IsPointerOverGameObject(Touchscreen.current.deviceId);
///
/// // Finally, any negative value will be interpreted as "any pointer" and will
/// // return true if any one pointer is currently over a GameObject:
/// EventSystem.current.IsPointerOverGameObject(-1);
/// EventSystem.current.IsPointerOverGameObject(); // Equivalent.
///
///
///
///
///
public override bool IsPointerOverGameObject(int pointerOrTouchId)
{
var stateIndex = -1;
if (pointerOrTouchId < 0)
{
if (m_CurrentPointerId != -1)
{
stateIndex = m_CurrentPointerIndex;
}
else
{
// No current pointer. Can happen, for example, when a touch just ended and its pointer record
// was removed as a result. If we still have some active pointer, use it.
if (m_PointerStates.length > 0)
stateIndex = 0;
}
}
else
{
stateIndex = GetPointerStateIndexFor(pointerOrTouchId);
if (stateIndex == -1)
{
for (var i = 0; i < m_PointerStates.length; ++i)
{
var eventData = m_PointerStates[i].eventData;
if (eventData.touchId == pointerOrTouchId || (eventData.touchId != 0 && eventData.device.deviceId == pointerOrTouchId))
return eventData.pointerEnter != null;
}
}
}
if (stateIndex == -1)
return false;
return m_PointerStates[stateIndex].eventData.pointerEnter != null;
}
private RaycastResult PerformRaycast(ExtendedPointerEventData eventData)
{
if (eventData == null)
throw new ArgumentNullException(nameof(eventData));
// If it's an event from a tracked device, see if we have a TrackedDeviceRaycaster and give it
// the first shot.
if (eventData.pointerType == UIPointerType.Tracked && TrackedDeviceRaycaster.s_Instances.length > 0)
{
for (var i = 0; i < TrackedDeviceRaycaster.s_Instances.length; ++i)
{
var trackedDeviceRaycaster = TrackedDeviceRaycaster.s_Instances[i];
m_RaycastResultCache.Clear();
trackedDeviceRaycaster.PerformRaycast(eventData, m_RaycastResultCache);
if (m_RaycastResultCache.Count > 0)
{
var raycastResult = m_RaycastResultCache[0];
m_RaycastResultCache.Clear();
return raycastResult;
}
}
return default;
}
// Otherwise pass it along to the normal raycasting logic.
eventSystem.RaycastAll(eventData, m_RaycastResultCache);
var result = FindFirstRaycast(m_RaycastResultCache);
m_RaycastResultCache.Clear();
return result;
}
// Mouse, pen, touch, and tracked device pointer input all go through here.
private void ProcessPointer(ref PointerModel state)
{
if (!state.changedThisFrame)
return;
var eventData = state.eventData;
// Sync position.
var pointerType = eventData.pointerType;
if (pointerType == UIPointerType.MouseOrPen && Cursor.lockState == CursorLockMode.Locked)
{
eventData.position = new Vector2(-1, -1);
////REVIEW: This is consistent with StandaloneInputModule but having no deltas in locked mode seems wrong
eventData.delta = default;
}
else if (pointerType == UIPointerType.Tracked)
{
eventData.trackedDeviceOrientation = state.worldOrientation;
eventData.trackedDevicePosition = state.worldPosition;
}
else
{
eventData.delta = state.screenPosition - eventData.position;
eventData.position = state.screenPosition;
}
// Clear the 'used' flag.
eventData.Reset();
// Raycast from current position.
eventData.pointerCurrentRaycast = PerformRaycast(eventData);
// Sync position for tracking devices. For those, we can only do this
// after the raycast as the screen-space position is a byproduct of the raycast.
if (pointerType == UIPointerType.Tracked && eventData.pointerCurrentRaycast.isValid)
{
var screenPos = eventData.pointerCurrentRaycast.screenPosition;
eventData.delta = screenPos - eventData.position;
eventData.position = eventData.pointerCurrentRaycast.screenPosition;
}
////REVIEW: for touch, we only need the left button; should we skip right and middle button processing? then we also don't need to copy to/from the event
// Left mouse button. Movement and scrolling is processed with event set left button.
eventData.button = PointerEventData.InputButton.Left;
state.leftButton.CopyPressStateTo(eventData);
ProcessPointerMovement(ref state, eventData);
ProcessPointerButton(ref state.leftButton, eventData);
ProcessPointerButtonDrag(ref state.leftButton, eventData);
ProcessPointerScroll(ref state, eventData);
// Right mouse button.
eventData.button = PointerEventData.InputButton.Right;
state.rightButton.CopyPressStateTo(eventData);
ProcessPointerButton(ref state.rightButton, eventData);
ProcessPointerButtonDrag(ref state.rightButton, eventData);
// Middle mouse button.
eventData.button = PointerEventData.InputButton.Middle;
state.middleButton.CopyPressStateTo(eventData);
ProcessPointerButton(ref state.middleButton, eventData);
ProcessPointerButtonDrag(ref state.middleButton, eventData);
state.OnFrameFinished();
}
// if we are using a MultiplayerEventSystem, ignore any transforms
// not under the current MultiplayerEventSystem's root.
private bool PointerShouldIgnoreTransform(Transform t)
{
if (eventSystem is MultiplayerEventSystem multiplayerEventSystem && multiplayerEventSystem.playerRoot != null)
{
if (!t.IsChildOf(multiplayerEventSystem.playerRoot.transform))
return true;
}
return false;
}
private void ProcessPointerMovement(ref PointerModel pointer, ExtendedPointerEventData eventData)
{
var currentPointerTarget =
// If the pointer is a touch that was released this frame, we generate pointer-exit events
// and then later remove the pointer.
(eventData.pointerType == UIPointerType.Touch && pointer.leftButton.wasReleasedThisFrame) ||
(eventData.pointerType == UIPointerType.MouseOrPen && Cursor.lockState == CursorLockMode.Locked)
? null
: eventData.pointerCurrentRaycast.gameObject;
ProcessPointerMovement(eventData, currentPointerTarget);
}
private void ProcessPointerMovement(ExtendedPointerEventData eventData, GameObject currentPointerTarget)
{
// If we have no target or pointerEnter has been deleted,
// we just send exit events to anything we are tracking
// and then exit.
if (currentPointerTarget == null || eventData.pointerEnter == null)
{
for (var i = 0; i < eventData.hovered.Count; ++i)
ExecuteEvents.Execute(eventData.hovered[i], eventData, ExecuteEvents.pointerExitHandler);
eventData.hovered.Clear();
if (currentPointerTarget == null)
{
eventData.pointerEnter = null;
return;
}
}
if (eventData.pointerEnter == currentPointerTarget && currentPointerTarget)
return;
var commonRoot = FindCommonRoot(eventData.pointerEnter, currentPointerTarget)?.transform;
// We walk up the tree until a common root and the last entered and current entered object is found.
// Then send exit and enter events up to, but not including, the common root.
if (eventData.pointerEnter != null)
{
for (var current = eventData.pointerEnter.transform; current != null && current != commonRoot; current = current.parent)
{
ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerExitHandler);
eventData.hovered.Remove(current.gameObject);
}
}
eventData.pointerEnter = currentPointerTarget;
if (currentPointerTarget != null)
{
for (var current = currentPointerTarget.transform;
current != null && current != commonRoot && !PointerShouldIgnoreTransform(current);
current = current.parent)
{
ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerEnterHandler);
eventData.hovered.Add(current.gameObject);
}
}
}
private void ProcessPointerButton(ref PointerModel.ButtonState button, PointerEventData eventData)
{
var currentOverGo = eventData.pointerCurrentRaycast.gameObject;
if (currentOverGo != null && PointerShouldIgnoreTransform(currentOverGo.transform))
return;
// Button press.
if (button.wasPressedThisFrame)
{
eventData.delta = Vector2.zero;
eventData.dragging = false;
eventData.pressPosition = eventData.position;
eventData.pointerPressRaycast = eventData.pointerCurrentRaycast;
eventData.eligibleForClick = true;
var selectHandler = ExecuteEvents.GetEventHandler(currentOverGo);
// If we have clicked something new, deselect the old thing and leave 'selection handling' up
// to the press event (except if there's none and we're told to not deselect in that case).
if (selectHandler != eventSystem.currentSelectedGameObject && (selectHandler != null || m_DeselectOnBackgroundClick))
eventSystem.SetSelectedGameObject(null, eventData);
// Invoke OnPointerDown, if present.
var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, eventData, ExecuteEvents.pointerDownHandler);
// Detect clicks.
// NOTE: StandaloneInputModule does this *after* the click handler has been invoked -- which doesn't seem to
// make sense. We do it *before* IPointerClickHandler.
var time = InputRuntime.s_Instance.unscaledGameTime;
const float clickSpeed = 0.3f;
if (newPressed == eventData.lastPress && (time - eventData.clickTime) < clickSpeed)
++eventData.clickCount;
else
eventData.clickCount = 1;
eventData.clickTime = time;
// We didn't find a press handler, so we turn it into a click.
if (newPressed == null)
newPressed = ExecuteEvents.GetEventHandler(currentOverGo);
eventData.pointerPress = newPressed;
eventData.rawPointerPress = currentOverGo;
// Save the drag handler for drag events during this mouse down.
eventData.pointerDrag = ExecuteEvents.GetEventHandler(currentOverGo);
if (eventData.pointerDrag != null)
ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.initializePotentialDrag);
}
// Button release.
if (button.wasReleasedThisFrame)
{
ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerUpHandler);
var pointerUpHandler = ExecuteEvents.GetEventHandler(currentOverGo);
if (eventData.pointerPress == pointerUpHandler && eventData.eligibleForClick)
ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerClickHandler);
else if (eventData.dragging && eventData.pointerDrag != null)
ExecuteEvents.ExecuteHierarchy(currentOverGo, eventData, ExecuteEvents.dropHandler);
eventData.eligibleForClick = false;
eventData.pointerPress = null;
eventData.rawPointerPress = null;
if (eventData.dragging && eventData.pointerDrag != null)
ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.endDragHandler);
eventData.dragging = false;
eventData.pointerDrag = null;
}
button.CopyPressStateFrom(eventData);
}
private void ProcessPointerButtonDrag(ref PointerModel.ButtonState button, ExtendedPointerEventData eventData)
{
if (!eventData.IsPointerMoving() ||
(eventData.pointerType == UIPointerType.MouseOrPen && Cursor.lockState == CursorLockMode.Locked) ||
eventData.pointerDrag == null)
return;
if (!eventData.dragging)
{
if (!eventData.useDragThreshold || (eventData.pressPosition - eventData.position).sqrMagnitude >=
(double)eventSystem.pixelDragThreshold * eventSystem.pixelDragThreshold * (eventData.pointerType == UIPointerType.Tracked
? m_TrackedDeviceDragThresholdMultiplier
: 1))
{
ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.beginDragHandler);
eventData.dragging = true;
}
}
if (eventData.dragging)
{
// If we moved from our initial press object, process an up for that object.
if (eventData.pointerPress != eventData.pointerDrag)
{
ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerUpHandler);
eventData.eligibleForClick = false;
eventData.pointerPress = null;
eventData.rawPointerPress = null;
}
ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.dragHandler);
button.CopyPressStateFrom(eventData);
}
}
private static void ProcessPointerScroll(ref PointerModel pointer, PointerEventData eventData)
{
var scrollDelta = pointer.scrollDelta;
if (!Mathf.Approximately(scrollDelta.sqrMagnitude, 0.0f))
{
eventData.scrollDelta = scrollDelta;
var scrollHandler = ExecuteEvents.GetEventHandler(eventData.pointerEnter);
ExecuteEvents.ExecuteHierarchy(scrollHandler, eventData, ExecuteEvents.scrollHandler);
}
}
internal void ProcessNavigation(ref NavigationModel navigationState)
{
var usedSelectionChange = false;
if (eventSystem.currentSelectedGameObject != null)
{
var data = GetBaseEventData();
ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.updateSelectedHandler);
usedSelectionChange = data.used;
}
// Don't send move events if disabled in the EventSystem.
if (!eventSystem.sendNavigationEvents)
return;
// Process move.
var movement = navigationState.move;
if (!usedSelectionChange && (!Mathf.Approximately(movement.x, 0f) || !Mathf.Approximately(movement.y, 0f)))
{
var time = InputRuntime.s_Instance.unscaledGameTime;
var moveVector = navigationState.move;
var moveDirection = MoveDirection.None;
if (moveVector.sqrMagnitude > 0)
{
if (Mathf.Abs(moveVector.x) > Mathf.Abs(moveVector.y))
moveDirection = moveVector.x > 0 ? MoveDirection.Right : MoveDirection.Left;
else
moveDirection = moveVector.y > 0 ? MoveDirection.Up : MoveDirection.Down;
}
////REVIEW: is resetting move repeats when direction changes really useful behavior?
if (moveDirection != m_NavigationState.lastMoveDirection)
m_NavigationState.consecutiveMoveCount = 0;
if (moveDirection != MoveDirection.None)
{
var allow = true;
if (m_NavigationState.consecutiveMoveCount != 0)
{
if (m_NavigationState.consecutiveMoveCount > 1)
allow = time > m_NavigationState.lastMoveTime + moveRepeatRate;
else
allow = time > m_NavigationState.lastMoveTime + moveRepeatDelay;
}
if (allow)
{
var eventData = m_NavigationState.eventData;
if (eventData == null)
{
eventData = new AxisEventData(eventSystem);
m_NavigationState.eventData = eventData;
}
eventData.Reset();
eventData.moveVector = moveVector;
eventData.moveDir = moveDirection;
ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, eventData, ExecuteEvents.moveHandler);
usedSelectionChange = eventData.used;
m_NavigationState.consecutiveMoveCount = m_NavigationState.consecutiveMoveCount + 1;
m_NavigationState.lastMoveTime = time;
m_NavigationState.lastMoveDirection = moveDirection;
}
}
else
m_NavigationState.consecutiveMoveCount = 0;
}
else
{
m_NavigationState.consecutiveMoveCount = 0;
}
// Process submit and cancel events.
if (!usedSelectionChange && eventSystem.currentSelectedGameObject != null)
{
var data = GetBaseEventData();
if (m_NavigationState.cancelButton.wasPressedThisFrame)
ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.cancelHandler);
if (!data.used && m_NavigationState.submitButton.wasPressedThisFrame)
ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.submitHandler);
}
m_NavigationState.OnFrameFinished();
}
[FormerlySerializedAs("m_RepeatDelay")]
[Tooltip("The Initial delay (in seconds) between an initial move action and a repeated move action.")]
[SerializeField]
private float m_MoveRepeatDelay = 0.5f;
[FormerlySerializedAs("m_RepeatRate")]
[Tooltip("The speed (in seconds) that the move action repeats itself once repeating (max 1 per frame).")]
[SerializeField]
private float m_MoveRepeatRate = 0.1f;
[Tooltip("Scales the Eventsystem.DragThreshold, for tracked devices, to make selection easier.")]
// Hide this while we still have to figure out what to do with this.
private float m_TrackedDeviceDragThresholdMultiplier = 2.0f;
///
/// Delay in seconds between an initial move action and a repeated move action while is actuated.
///
///
/// While is being held down, the input module will first wait for seconds
/// after the first actuation of and then trigger a move event every seconds.
///
///
///
///
public float moveRepeatDelay
{
get => m_MoveRepeatDelay;
set => m_MoveRepeatDelay = value;
}
///
/// Delay in seconds between repeated move actions while is actuated.
///
///
/// While is being held down, the input module will first wait for seconds
/// after the first actuation of and then trigger a move event every seconds.
///
/// Note that a maximum of one will be sent per frame. This means that even if multiple time
/// increments of the repeat delay have passed since the last update, only one move repeat event will be generated.
///
///
///
///
public float moveRepeatRate
{
get => m_MoveRepeatRate;
set => m_MoveRepeatRate = value;
}
[Obsolete("'repeatRate' has been obsoleted; use 'moveRepeatRate' instead. (UnityUpgradable) -> moveRepeatRate", false)]
public float repeatRate
{
get => moveRepeatRate;
set => moveRepeatRate = value;
}
[Obsolete("'repeatDelay' has been obsoleted; use 'moveRepeatDelay' instead. (UnityUpgradable) -> moveRepeatDelay", false)]
public float repeatDelay
{
get => moveRepeatDelay;
set => moveRepeatDelay = value;
}
///
/// Scales the drag threshold of EventSystem for tracked devices to make selection easier.
///
public float trackedDeviceDragThresholdMultiplier
{
get => m_TrackedDeviceDragThresholdMultiplier;
set => m_TrackedDeviceDragThresholdMultiplier = value;
}
private void SwapAction(ref InputActionReference property, InputActionReference newValue, bool actionsHooked, Action actionCallback)
{
if (property == newValue || (property != null && newValue != null && property.action == newValue.action))
return;
if (property != null && actionsHooked)
{
property.action.performed -= actionCallback;
property.action.canceled -= actionCallback;
}
property = newValue;
#if DEBUG
if (newValue != null && newValue.action != null && newValue.action.type != InputActionType.PassThrough)
{
Debug.LogWarning("Actions used with the UI input module should generally be set to Pass-Through type so that the module can properly distinguish between "
+ $"input from multiple devices (action {newValue.action} is set to {newValue.action.type})", this);
}
#endif
if (newValue != null && actionsHooked)
{
property.action.performed += actionCallback;
property.action.canceled += actionCallback;
}
}
///
/// An delivering a 2D screen position
/// used as a cursor for pointing at UI elements.
///
///
/// The values read from this action determine and .
///
/// Together with , , , and
/// , this forms the basis for pointer-type UI input.
///
///
///
/// var asset = ScriptableObject.Create<InputActionAsset>();
/// var map = asset.AddActionMap("UI");
/// var pointAction = map.AddAction("Point");
///
/// pointAction.AddBinding("<Mouse>/position");
/// pointAction.AddBinding("<Touchscreen>/touch*/position");
///
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point =
/// InputActionReference.Create(pointAction);
///
///
///
///
///
///
///
public InputActionReference point
{
get => m_PointAction;
set => SwapAction(ref m_PointAction, value, m_ActionsHooked, m_OnPointDelegate);
}
///
/// An delivering a Vector2 scroll wheel value
/// used for sending events.
///
///
/// The values read from this action determine .
///
/// Together with , , , and
/// , this forms the basis for pointer-type UI input.
///
/// Note that the action is optional. A pointer is fully functional with just
/// and alone.
///
///
///
/// var asset = ScriptableObject.Create<InputActionAsset>();
/// var map = asset.AddActionMap("UI");
/// var pointAction = map.AddAction("scroll");
/// var scrollAction = map.AddAction("scroll");
///
/// pointAction.AddBinding("<Mouse>/position");
/// pointAction.AddBinding("<Touchscreen>/touch*/position");
///
/// scrollAction.AddBinding("<Mouse>/scroll");
///
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point =
/// InputActionReference.Create(pointAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).scrollWheel =
/// InputActionReference.Create(scrollAction);
///
///
///
///
///
///
///
public InputActionReference scrollWheel
{
get => m_ScrollWheelAction;
set => SwapAction(ref m_ScrollWheelAction, value, m_ActionsHooked, m_OnScrollWheelDelegate);
}
///
/// An delivering a float button value that determines
/// whether the left button of a pointer is pressed.
///
///
/// Clicks on this button will use for .
///
/// Together with , , , and
/// , this forms the basis for pointer-type UI input.
///
/// Note that together with , this action is necessary for a pointer to be functional. The other clicks
/// and are optional, however.
///
///
///
/// var asset = ScriptableObject.Create<InputActionAsset>();
/// var map = asset.AddActionMap("UI");
/// var pointAction = map.AddAction("scroll");
/// var clickAction = map.AddAction("click");
///
/// pointAction.AddBinding("<Mouse>/position");
/// pointAction.AddBinding("<Touchscreen>/touch*/position");
///
/// clickAction.AddBinding("<Mouse>/leftButton");
/// clickAction.AddBinding("<Touchscreen>/touch*/press");
///
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point =
/// InputActionReference.Create(pointAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick =
/// InputActionReference.Create(clickAction);
///
///
///
///
///
///
///
public InputActionReference leftClick
{
get => m_LeftClickAction;
set => SwapAction(ref m_LeftClickAction, value, m_ActionsHooked, m_OnLeftClickDelegate);
}
///
/// An delivering a float button value that determines
/// whether the middle button of a pointer is pressed.
///
///
/// Clicks on this button will use for .
///
/// Together with , , , and
/// , this forms the basis for pointer-type UI input.
///
/// Note that the action is optional. A pointer is fully functional with just
/// and alone.
///
///
///
/// var asset = ScriptableObject.Create<InputActionAsset>();
/// var map = asset.AddActionMap("UI");
/// var pointAction = map.AddAction("scroll");
/// var leftClickAction = map.AddAction("leftClick");
/// var middleClickAction = map.AddAction("middleClick");
///
/// pointAction.AddBinding("<Mouse>/position");
/// pointAction.AddBinding("<Touchscreen>/touch*/position");
///
/// leftClickAction.AddBinding("<Mouse>/leftButton");
/// leftClickAction.AddBinding("<Touchscreen>/touch*/press");
///
/// middleClickAction.AddBinding("<Mouse>/middleButton");
///
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point =
/// InputActionReference.Create(pointAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick =
/// InputActionReference.Create(leftClickAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).middleClick =
/// InputActionReference.Create(middleClickAction);
///
///
///
///
///
///
///
public InputActionReference middleClick
{
get => m_MiddleClickAction;
set => SwapAction(ref m_MiddleClickAction, value, m_ActionsHooked, m_OnMiddleClickDelegate);
}
///
/// An delivering a float" button value that determines
/// whether the right button of a pointer is pressed.
///
///
/// Clicks on this button will use for .
///
/// Together with , , , and
/// , this forms the basis for pointer-type UI input.
///
/// Note that the action is optional. A pointer is fully functional with just
/// and alone.
///
///
///
/// var asset = ScriptableObject.Create<InputActionAsset>();
/// var map = asset.AddActionMap("UI");
/// var pointAction = map.AddAction("scroll");
/// var leftClickAction = map.AddAction("leftClick");
/// var rightClickAction = map.AddAction("rightClick");
///
/// pointAction.AddBinding("<Mouse>/position");
/// pointAction.AddBinding("<Touchscreen>/touch*/position");
///
/// leftClickAction.AddBinding("<Mouse>/leftButton");
/// leftClickAction.AddBinding("<Touchscreen>/touch*/press");
///
/// rightClickAction.AddBinding("<Mouse>/rightButton");
///
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point =
/// InputActionReference.Create(pointAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick =
/// InputActionReference.Create(leftClickAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).rightClick =
/// InputActionReference.Create(rightClickAction);
///
///
///
///
///
///
///
public InputActionReference rightClick
{
get => m_RightClickAction;
set => SwapAction(ref m_RightClickAction, value, m_ActionsHooked, m_OnRightClickDelegate);
}
///
/// An delivering a Vector2 2D motion vector
/// used for sending navigation events.
///
///
/// The events generated from this input will be received by .
///
/// This action together with and form the sources for navigation-style
/// UI input.
///
///
///
/// var asset = ScriptableObject.Create<InputActionAsset>();
/// var map = asset.AddActionMap("UI");
/// var pointAction = map.AddAction("move");
/// var submitAction = map.AddAction("submit");
/// var cancelAction = map.AddAction("cancel");
///
/// moveAction.AddBinding("<Gamepad>/*stick");
/// moveAction.AddBinding("<Gamepad>/dpad");
/// submitAction.AddBinding("<Gamepad>/buttonSouth");
/// cancelAction.AddBinding("<Gamepad>/buttonEast");
///
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).move =
/// InputActionReference.Create(moveAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).submit =
/// InputActionReference.Create(submitAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).cancelAction =
/// InputActionReference.Create(cancelAction);
///
///
///
///
///
public InputActionReference move
{
get => m_MoveAction;
set => SwapAction(ref m_MoveAction, value, m_ActionsHooked, m_OnMoveDelegate);
}
///
/// An delivering a float button value that determines when ISubmitHandler
/// is triggered.
///
///
/// The events generated from this input will be received by .
///
/// This action together with and form the sources for navigation-style
/// UI input.
///
///
///
/// var asset = ScriptableObject.Create<InputActionAsset>();
/// var map = asset.AddActionMap("UI");
/// var pointAction = map.AddAction("move");
/// var submitAction = map.AddAction("submit");
/// var cancelAction = map.AddAction("cancel");
///
/// moveAction.AddBinding("<Gamepad>/*stick");
/// moveAction.AddBinding("<Gamepad>/dpad");
/// submitAction.AddBinding("<Gamepad>/buttonSouth");
/// cancelAction.AddBinding("<Gamepad>/buttonEast");
///
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).move =
/// InputActionReference.Create(moveAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).submit =
/// InputActionReference.Create(submitAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).cancelAction =
/// InputActionReference.Create(cancelAction);
///
///
///
///
///
public InputActionReference submit
{
get => m_SubmitAction;
set => SwapAction(ref m_SubmitAction, value, m_ActionsHooked, m_OnSubmitDelegate);
}
///
/// An delivering a float button value that determines when ICancelHandler
/// is triggered.
///
///
/// The events generated from this input will be received by .
///
/// This action together with and form the sources for navigation-style
/// UI input.
///
///
///
/// var asset = ScriptableObject.Create<InputActionAsset>();
/// var map = asset.AddActionMap("UI");
/// var pointAction = map.AddAction("move");
/// var submitAction = map.AddAction("submit");
/// var cancelAction = map.AddAction("cancel");
///
/// moveAction.AddBinding("<Gamepad>/*stick");
/// moveAction.AddBinding("<Gamepad>/dpad");
/// submitAction.AddBinding("<Gamepad>/buttonSouth");
/// cancelAction.AddBinding("<Gamepad>/buttonEast");
///
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).move =
/// InputActionReference.Create(moveAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).submit =
/// InputActionReference.Create(submitAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).cancelAction =
/// InputActionReference.Create(cancelAction);
///
///
///
///
///
public InputActionReference cancel
{
get => m_CancelAction;
set => SwapAction(ref m_CancelAction, value, m_ActionsHooked, m_OnCancelDelegate);
}
///
/// An delivering a Quaternion value reflecting the orientation of s.
/// In combination with , this is used to determine the transform of tracked devices from which
/// to raycast into the UI scene.
///
///
/// and together replace for
/// UI input from . Other than that, UI input for tracked devices is no different from "normal"
/// pointer-type input. This means that , , , and
/// can all be used for tracked device input like for regular pointer input.
///
///
///
/// var asset = ScriptableObject.Create<InputActionAsset>();
/// var map = asset.AddActionMap("UI");
/// var positionAction = map.AddAction("position");
/// var orientationAction = map.AddAction("orientation");
/// var clickAction = map.AddAction("click");
///
/// positionAction.AddBinding("<TrackedDevice>/devicePosition");
/// orientationAction.AddBinding("<TrackedDevice>/deviceRotation");
/// clickAction.AddBinding("<TrackedDevice>/trigger");
///
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).trackedDevicePosition =
/// InputActionReference.Create(positionAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).trackedDeviceOrientation =
/// InputActionReference.Create(orientationAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick =
/// InputActionReference.Create(clickAction);
///
///
///
///
public InputActionReference trackedDeviceOrientation
{
get => m_TrackedDeviceOrientationAction;
set => SwapAction(ref m_TrackedDeviceOrientationAction, value, m_ActionsHooked, m_OnTrackedDeviceOrientationDelegate);
}
///
/// An delivering a Vector3 value reflecting the position of s.
/// In combination with , this is used to determine the transform of tracked devices from which
/// to raycast into the UI scene.
///
///
/// and together replace for
/// UI input from . Other than that, UI input for tracked devices is no different from "normal"
/// pointer-type input. This means that , , , and
/// can all be used for tracked device input like for regular pointer input.
///
///
///
/// var asset = ScriptableObject.Create<InputActionAsset>();
/// var map = asset.AddActionMap("UI");
/// var positionAction = map.AddAction("position");
/// var orientationAction = map.AddAction("orientation");
/// var clickAction = map.AddAction("click");
///
/// positionAction.AddBinding("<TrackedDevice>/devicePosition");
/// orientationAction.AddBinding("<TrackedDevice>/deviceRotation");
/// clickAction.AddBinding("<TrackedDevice>/trigger");
///
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).trackedDevicePosition =
/// InputActionReference.Create(positionAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).trackedDeviceOrientation =
/// InputActionReference.Create(orientationAction);
/// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick =
/// InputActionReference.Create(clickAction);
///
///
///
///
public InputActionReference trackedDevicePosition
{
get => m_TrackedDevicePositionAction;
set => SwapAction(ref m_TrackedDevicePositionAction, value, m_ActionsHooked, m_OnTrackedDevicePositionDelegate);
}
[Obsolete("'trackedDeviceSelect' has been obsoleted; use 'leftClick' instead.", true)]
public InputActionReference trackedDeviceSelect
{
get => throw new InvalidOperationException();
set => throw new InvalidOperationException();
}
protected override void Awake()
{
base.Awake();
m_NavigationState.Reset();
}
protected override void OnDestroy()
{
base.OnDestroy();
UnhookActions();
}
protected override void OnEnable()
{
base.OnEnable();
if (m_OnControlsChangedDelegate == null)
m_OnControlsChangedDelegate = OnControlsChanged;
InputActionState.s_OnActionControlsChanged.AppendWithCapacity(m_OnControlsChangedDelegate);
HookActions();
EnableAllActions();
}
protected override void OnDisable()
{
base.OnDisable();
var i = InputActionState.s_OnActionControlsChanged.IndexOfReference(m_OnControlsChangedDelegate);
if (i != -1)
InputActionState.s_OnActionControlsChanged.RemoveAtWithCapacity(i);
DisableAllActions();
UnhookActions();
}
private bool IsAnyActionEnabled()
{
return (m_PointAction?.action?.enabled ?? true) &&
(m_LeftClickAction?.action?.enabled ?? true) &&
(m_RightClickAction?.action?.enabled ?? true) &&
(m_MiddleClickAction?.action?.enabled ?? true) &&
(m_MoveAction?.action?.enabled ?? true) &&
(m_SubmitAction?.action?.enabled ?? true) &&
(m_CancelAction?.action?.enabled ?? true) &&
(m_ScrollWheelAction?.action?.enabled ?? true) &&
(m_TrackedDeviceOrientationAction?.action?.enabled ?? true) &&
(m_TrackedDevicePositionAction?.action?.enabled ?? true);
}
private void EnableAllActions()
{
if (!IsAnyActionEnabled())
{
m_PointAction?.action?.Enable();
m_LeftClickAction?.action?.Enable();
m_RightClickAction?.action?.Enable();
m_MiddleClickAction?.action?.Enable();
m_MoveAction?.action?.Enable();
m_SubmitAction?.action?.Enable();
m_CancelAction?.action?.Enable();
m_ScrollWheelAction?.action?.Enable();
m_TrackedDeviceOrientationAction?.action?.Enable();
m_TrackedDevicePositionAction?.action?.Enable();
m_OwnsEnabledState = true;
}
}
private void DisableAllActions()
{
if (m_OwnsEnabledState)
{
m_OwnsEnabledState = false;
m_PointAction?.action?.Disable();
m_LeftClickAction?.action?.Disable();
m_RightClickAction?.action?.Disable();
m_MiddleClickAction?.action?.Disable();
m_MoveAction?.action?.Disable();
m_SubmitAction?.action?.Disable();
m_CancelAction?.action?.Disable();
m_ScrollWheelAction?.action?.Disable();
m_TrackedDeviceOrientationAction?.action?.Disable();
m_TrackedDevicePositionAction?.action?.Disable();
}
}
private int GetPointerStateIndexFor(int pointerId)
{
if (pointerId == m_CurrentPointerId)
return m_CurrentPointerIndex;
for (var i = 0; i < m_PointerIds.length; ++i)
if (m_PointerIds[i] == pointerId)
return i;
return -1;
}
private ref PointerModel GetPointerStateForIndex(int index)
{
if (index == 0)
return ref m_PointerStates.firstValue;
return ref m_PointerStates.additionalValues[index - 1];
}
private ref PointerModel GetPointerStateFor(ref InputAction.CallbackContext context)
{
var index = GetPointerStateIndexFor(context.control);
return ref GetPointerStateForIndex(index);
}
// This is the key method for determining which pointer a particular input is associated with.
// The principal determinant is the device that is sending the input which, in general, is expected
// to be a Pointer (Mouse, Pen, Touchscreen) or TrackedDevice.
//
// Note, however, that the input is not guaranteed to even come from a pointer-like device. One can
// bind the space key to a left click, for example. As long as we have an active pointer that can
// deliver position input, we accept that setup and treat pressing the space key the same as pressing
// the left button input on the respective pointer.
//
// Quite a lot going on in this method but we're dealing with three different UI interaction paradigms
// here which we all support from a single input path and allow seamless switching between.
private int GetPointerStateIndexFor(InputControl control)
{
Debug.Assert(control != null, "Control must not be null");
////REVIEW: Any way we can cut down on the hops all over memory that we're doing here?
var device = control.device;
////TODO: We're repeatedly inspecting the control setup here. Do this once and only redo it if the control setup changes.
////REVIEW: It seems wrong that we are picking up an input here that is *NOT* reflected in our actions. We just end
//// up reading a touchId control implicitly instead of allowing actions to deliver IDs to us. On the other hand,
//// making that setup explicit in actions may be quite awkward and not nearly as robust.
// Determine the pointer (and touch) ID. We default the pointer ID to the device
// ID of the InputDevice.
var pointerId = device.deviceId;
var touchId = 0;
var controlParent = control.parent;
if (controlParent is TouchControl touchControl)
touchId = touchControl.touchId.ReadValue();
// Could be it's a toplevel control on Touchscreen (like "/position"). In that case,
// read the touch ID from primaryTouch.
else if (controlParent is Touchscreen touchscreen)
touchId = touchscreen.primaryTouch.touchId.ReadValue();
if (touchId != 0)
pointerId = ExtendedPointerEventData.MakePointerIdForTouch(pointerId, touchId);
// Early out if it's the last used pointer.
// NOTE: Can't just compare by device here because of touchscreens potentially having multiple associated pointers.
if (m_CurrentPointerId == pointerId)
return m_CurrentPointerIndex;
// Search m_PointerIds for an existing entry.
// NOTE: This is a linear search but m_PointerIds is only IDs and the number of concurrent pointers
// should be very low at any one point (in fact, we don't generally expect to have more than one
// which is why we are using InlinedArrays).
for (var i = 0; i < m_PointerIds.length; i++)
{
if (m_PointerIds[i] == pointerId)
{
// Existing entry found. Make it the current pointer.
m_CurrentPointerId = pointerId;
m_CurrentPointerIndex = i;
m_CurrentPointerType = m_PointerStates[i].pointerType;
return i;
}
}
// Determine pointer type.
var pointerType = UIPointerType.None;
if (touchId != 0)
pointerType = UIPointerType.Touch;
else if (HaveControlForDevice(device, point))
pointerType = UIPointerType.MouseOrPen;
else if (HaveControlForDevice(device, trackedDevicePosition))
pointerType = UIPointerType.Tracked;
// For SingleMouseOrPenButMultiTouchAndTrack, we keep a single pointer for mouse and pen but only for as
// long as there is no touch or tracked input. If we get that kind, we remove the mouse/pen pointer.
if (m_PointerBehavior == UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack && pointerType != UIPointerType.None)
{
if (pointerType == UIPointerType.MouseOrPen)
{
// We have input on a mouse or pen. Kill all touch and tracked pointers we may have.
for (var i = 0; i < m_PointerStates.length; ++i)
{
if (m_PointerStates[i].pointerType != UIPointerType.MouseOrPen)
{
SendPointerExitEventsAndRemovePointer(i);
--i;
}
}
}
else
{
// We have touch or tracked input. Kill mouse/pen pointer, if we have it.
for (var i = 0; i < m_PointerStates.length; ++i)
{
if (m_PointerStates[i].pointerType == UIPointerType.MouseOrPen)
{
SendPointerExitEventsAndRemovePointer(i);
--i;
}
}
}
}
////REVIEW: For touch, probably makes sense to force-ignore any input other than from primaryTouch.
// If the behavior is SingleUnifiedPointer, we only ever create a single pointer state
// and use that for all pointer input that is coming in.
if ((m_PointerBehavior == UIPointerBehavior.SingleUnifiedPointer && pointerType != UIPointerType.None) ||
(m_PointerBehavior == UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack && pointerType == UIPointerType.MouseOrPen))
{
if (m_CurrentPointerIndex == -1)
{
m_CurrentPointerIndex = AllocatePointer(pointerId, touchId, pointerType, device);
}
else
{
// Update pointer record to reflect current device. We know they're different because we checked
// m_CurrentPointerId earlier in the method.
// NOTE: This path may repeatedly switch the pointer type and ID on the same single event instance.
ref var pointer = ref GetPointerStateForIndex(m_CurrentPointerIndex);
var eventData = pointer.eventData;
eventData.device = device;
eventData.pointerType = pointerType;
eventData.pointerId = pointerId;
eventData.touchId = touchId;
// Make sure these don't linger around when we switch to a different kind of pointer.
eventData.trackedDeviceOrientation = default;
eventData.trackedDevicePosition = default;
}
m_CurrentPointerId = pointerId;
m_CurrentPointerType = pointerType;
return m_CurrentPointerIndex;
}
// No existing record for the device. Find out if the device has the ability to point at all.
// If not, we need to use a pointer state from a different device (if present).
var index = -1;
if (pointerType != UIPointerType.None)
{
// Device has an associated position input. Create a new pointer record.
index = AllocatePointer(pointerId, touchId, pointerType, device);
}
else
{
// Device has no associated position input. Find a pointer device to route the change into.
// As a last resort, create a pointer without a position input.
// If we have a current pointer, route the input into that. The majority of times we end
// up in this branch, this should settle things.
if (m_CurrentPointerId != -1)
return m_CurrentPointerIndex;
// NOTE: In most cases, we end up here when there is input on a non-pointer device bound to one of the pointer-related
// actions before there is input from a pointer device. In this scenario, we don't have a pointer state allocated
// for the device yet.
// If we have anything bound to the `point` action, create a pointer for it.
var pointControls = point?.action?.controls;
var pointerDevice = pointControls.HasValue && pointControls.Value.Count > 0 ? pointControls.Value[0].device : null;
if (pointerDevice != null && !(pointerDevice is Touchscreen)) // Touchscreen only temporarily allocate pointer states.
{
// Create MouseOrPen style pointer.
index = AllocatePointer(pointerDevice.deviceId, 0, UIPointerType.MouseOrPen, pointerDevice);
}
else
{
// Do the same but look at the `position` action.
var positionControls = trackedDevicePosition?.action?.controls;
var trackedDevice = positionControls.HasValue && positionControls.Value.Count > 0
? positionControls.Value[0].device
: null;
if (trackedDevice != null)
{
// Create a Tracked style pointer.
index = AllocatePointer(trackedDevice.deviceId, 0, UIPointerType.Tracked, trackedDevice);
}
else
{
// We got input from a non-pointer device and apparently there's no pointer we can route the
// input into. Just create a pointer state for the device and leave it at that.
index = AllocatePointer(pointerId, 0, UIPointerType.None, device);
}
}
}
m_CurrentPointerId = pointerId;
m_CurrentPointerIndex = index;
m_CurrentPointerType = pointerType;
return index;
}
private int AllocatePointer(int pointerId, int touchId, UIPointerType pointerType, InputDevice device)
{
// Recover event instance from previous record.
var eventData = default(ExtendedPointerEventData);
if (m_PointerStates.Capacity > m_PointerStates.length)
{
if (m_PointerStates.length == 0)
eventData = m_PointerStates.firstValue.eventData;
else
eventData = m_PointerStates.additionalValues[m_PointerStates.length - 1].eventData;
}
// Or allocate event.
if (eventData == null)
eventData = new ExtendedPointerEventData(eventSystem);
// Allocate state.
m_PointerIds.AppendWithCapacity(pointerId);
return m_PointerStates.AppendWithCapacity(new PointerModel(pointerId, touchId, pointerType, device, eventData));
}
private void SendPointerExitEventsAndRemovePointer(int index)
{
var eventData = m_PointerStates[index].eventData;
if (eventData.pointerEnter != null)
ProcessPointerMovement(eventData, null);
RemovePointerAtIndex(index);
}
private void RemovePointerAtIndex(int index)
{
Debug.Assert(m_PointerStates[index].eventData.pointerEnter == null, "Pointer should have exited all objects before being removed");
// Retain event data so that we can reuse the event the next time we allocate a PointerModel record.
var eventData = m_PointerStates[index].eventData;
Debug.Assert(eventData != null, "Pointer state should have an event instance!");
// Remove. Note that we may change the order of pointers here. This can save us needless copying
// and m_CurrentPointerIndex should be the only index we get around for longer.
m_PointerIds.RemoveAtByMovingTailWithCapacity(index);
m_PointerStates.RemoveAtByMovingTailWithCapacity(index);
Debug.Assert(m_PointerIds.length == m_PointerStates.length, "Pointer ID array should match state array in length");
if (index == m_CurrentPointerIndex)
{
m_CurrentPointerId = -1;
m_CurrentPointerIndex = -1;
}
// Put event instance back in place at one past last entry of array (which we know we have
// as we just erased one entry). This entry will be the next one that will be used when we
// allocate a new entry.
// Wipe the event.
// NOTE: We only wipe properties here that contain reference data. The rest we rely on
// the event handling code to initialize when using the event.
eventData.hovered.Clear();
eventData.device = null;
eventData.pointerCurrentRaycast = default;
eventData.pointerPressRaycast = default;
eventData.pointerPress = default; // Twice to wipe lastPress, too.
eventData.pointerPress = default;
eventData.pointerDrag = default;
eventData.pointerEnter = default;
eventData.rawPointerPress = default;
if (m_PointerStates.length == 0)
m_PointerStates.firstValue.eventData = eventData;
else
m_PointerStates.additionalValues[m_PointerStates.length - 1].eventData = eventData;
}
// Remove any pointer that no longer has the ability to point.
private void PurgeStalePointers()
{
for (var i = 0; i < m_PointerStates.length; ++i)
{
ref var state = ref GetPointerStateForIndex(i);
var device = state.eventData.device;
if (!HaveControlForDevice(device, point) &&
!HaveControlForDevice(device, trackedDevicePosition) &&
!HaveControlForDevice(device, trackedDeviceOrientation))
{
SendPointerExitEventsAndRemovePointer(i);
--i;
}
}
}
private static bool HaveControlForDevice(InputDevice device, InputActionReference actionReference)
{
var action = actionReference?.action;
if (action == null)
return false;
var controls = action.controls;
for (var i = 0; i < controls.Count; ++i)
if (controls[i].device == device)
return true;
return false;
}
private void OnPoint(InputAction.CallbackContext context)
{
ref var state = ref GetPointerStateFor(ref context);
state.screenPosition = context.ReadValue();
}
////REVIEW: How should we handle clickCount here? There's only one for the entire device yet right and middle clicks
//// are independent of left clicks. ATM we ignore native click counts and do click detection for all clicks
//// ourselves just like StandaloneInputModule does.
private void OnLeftClick(InputAction.CallbackContext context)
{
ref var state = ref GetPointerStateFor(ref context);
state.leftButton.isPressed = context.ReadValueAsButton();
state.changedThisFrame = true;
}
private void OnRightClick(InputAction.CallbackContext context)
{
ref var state = ref GetPointerStateFor(ref context);
state.rightButton.isPressed = context.ReadValueAsButton();
state.changedThisFrame = true;
}
private void OnMiddleClick(InputAction.CallbackContext context)
{
ref var state = ref GetPointerStateFor(ref context);
state.middleButton.isPressed = context.ReadValueAsButton();
state.changedThisFrame = true;
}
internal const float kPixelPerLine = 20;
private void OnScroll(InputAction.CallbackContext context)
{
ref var state = ref GetPointerStateFor(ref context);
// The old input system reported scroll deltas in lines, we report pixels.
// Need to scale as the UI system expects lines.
state.scrollDelta = context.ReadValue() * (1 / kPixelPerLine);
}
private void OnMove(InputAction.CallbackContext context)
{
m_NavigationState.move = context.ReadValue();
}
private void OnSubmit(InputAction.CallbackContext context)
{
m_NavigationState.submitButton.isPressed = context.ReadValueAsButton();
}
private void OnCancel(InputAction.CallbackContext context)
{
m_NavigationState.cancelButton.isPressed = context.ReadValueAsButton();
}
private void OnTrackedDeviceOrientation(InputAction.CallbackContext context)
{
ref var state = ref GetPointerStateFor(ref context);
state.worldOrientation = context.ReadValue();
}
private void OnTrackedDevicePosition(InputAction.CallbackContext context)
{
ref var state = ref GetPointerStateFor(ref context);
state.worldPosition = context.ReadValue();
}
private void OnControlsChanged(object obj)
{
PurgeStalePointers();
}
public override void Process()
{
// Reset devices of changes since we don't want to spool up changes once we gain focus.
if (!eventSystem.isFocused)
{
m_NavigationState.OnFrameFinished();
for (var i = 0; i < m_PointerStates.length; ++i)
m_PointerStates[i].OnFrameFinished();
}
else
{
ProcessNavigation(ref m_NavigationState);
for (var i = 0; i < m_PointerStates.length; i++)
{
ref var state = ref GetPointerStateForIndex(i);
ProcessPointer(ref state);
// If it's a touch and the touch has ended, release the pointer state.
// NOTE: We have no guarantee that the system reuses touch IDs so the touch ID we used
// as a pointer ID may be a one-off thing.
if (state.pointerType == UIPointerType.Touch && !state.leftButton.isPressed)
{
RemovePointerAtIndex(i);
--i;
}
}
}
}
private void HookActions()
{
if (m_ActionsHooked)
return;
if (m_OnPointDelegate == null)
m_OnPointDelegate = OnPoint;
if (m_OnLeftClickDelegate == null)
m_OnLeftClickDelegate = OnLeftClick;
if (m_OnRightClickDelegate == null)
m_OnRightClickDelegate = OnRightClick;
if (m_OnMiddleClickDelegate == null)
m_OnMiddleClickDelegate = OnMiddleClick;
if (m_OnScrollWheelDelegate == null)
m_OnScrollWheelDelegate = OnScroll;
if (m_OnMoveDelegate == null)
m_OnMoveDelegate = OnMove;
if (m_OnSubmitDelegate == null)
m_OnSubmitDelegate = OnSubmit;
if (m_OnCancelDelegate == null)
m_OnCancelDelegate = OnCancel;
if (m_OnTrackedDeviceOrientationDelegate == null)
m_OnTrackedDeviceOrientationDelegate = OnTrackedDeviceOrientation;
if (m_OnTrackedDevicePositionDelegate == null)
m_OnTrackedDevicePositionDelegate = OnTrackedDevicePosition;
SetActionCallbacks(true);
}
private void UnhookActions()
{
if (!m_ActionsHooked)
return;
SetActionCallbacks(false);
}
private void SetActionCallbacks(bool install)
{
m_ActionsHooked = install;
SetActionCallback(m_PointAction, m_OnPointDelegate, install);
SetActionCallback(m_MoveAction, m_OnMoveDelegate, install);
SetActionCallback(m_LeftClickAction, m_OnLeftClickDelegate, install);
SetActionCallback(m_RightClickAction, m_OnRightClickDelegate, install);
SetActionCallback(m_MiddleClickAction, m_OnMiddleClickDelegate, install);
SetActionCallback(m_SubmitAction, m_OnSubmitDelegate, install);
SetActionCallback(m_CancelAction, m_OnCancelDelegate, install);
SetActionCallback(m_ScrollWheelAction, m_OnScrollWheelDelegate, install);
SetActionCallback(m_TrackedDeviceOrientationAction, m_OnTrackedDeviceOrientationDelegate, install);
SetActionCallback(m_TrackedDevicePositionAction, m_OnTrackedDevicePositionDelegate, install);
}
private static void SetActionCallback(InputActionReference actionReference, Action callback, bool install)
{
if (!install && callback == null)
return;
if (actionReference == null)
return;
var action = actionReference.action;
if (action == null)
return;
if (install)
{
action.performed += callback;
action.canceled += callback;
}
else
{
action.performed -= callback;
action.canceled -= callback;
}
}
private InputActionReference UpdateReferenceForNewAsset(InputActionReference actionReference)
{
var oldAction = actionReference?.action;
if (oldAction == null)
return null;
var oldActionMap = oldAction.actionMap;
Debug.Assert(oldActionMap != null, "Not expected to end up with a singleton action here");
var newActionMap = m_ActionsAsset.FindActionMap(oldActionMap.name);
if (newActionMap == null)
return null;
var newAction = newActionMap.FindAction(oldAction.name);
if (newAction == null)
return null;
return InputActionReference.Create(newAction);
}
public InputActionAsset actionsAsset
{
get => m_ActionsAsset;
set
{
if (value != m_ActionsAsset)
{
var wasEnabled = IsAnyActionEnabled();
DisableAllActions();
m_ActionsAsset = value;
point = UpdateReferenceForNewAsset(point);
move = UpdateReferenceForNewAsset(move);
leftClick = UpdateReferenceForNewAsset(leftClick);
rightClick = UpdateReferenceForNewAsset(rightClick);
middleClick = UpdateReferenceForNewAsset(middleClick);
scrollWheel = UpdateReferenceForNewAsset(scrollWheel);
submit = UpdateReferenceForNewAsset(submit);
cancel = UpdateReferenceForNewAsset(cancel);
if (wasEnabled)
EnableAllActions();
}
}
}
[SerializeField, HideInInspector] private InputActionAsset m_ActionsAsset;
[SerializeField, HideInInspector] private InputActionReference m_PointAction;
[SerializeField, HideInInspector] private InputActionReference m_MoveAction;
[SerializeField, HideInInspector] private InputActionReference m_SubmitAction;
[SerializeField, HideInInspector] private InputActionReference m_CancelAction;
[SerializeField, HideInInspector] private InputActionReference m_LeftClickAction;
[SerializeField, HideInInspector] private InputActionReference m_MiddleClickAction;
[SerializeField, HideInInspector] private InputActionReference m_RightClickAction;
[SerializeField, HideInInspector] private InputActionReference m_ScrollWheelAction;
[SerializeField, HideInInspector] private InputActionReference m_TrackedDevicePositionAction;
[SerializeField, HideInInspector] private InputActionReference m_TrackedDeviceOrientationAction;
[SerializeField] private bool m_DeselectOnBackgroundClick = true;
[SerializeField] private UIPointerBehavior m_PointerBehavior = UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack;
private bool m_OwnsEnabledState;
private bool m_ActionsHooked;
private Action m_OnPointDelegate;
private Action m_OnMoveDelegate;
private Action m_OnSubmitDelegate;
private Action m_OnCancelDelegate;
private Action m_OnLeftClickDelegate;
private Action m_OnRightClickDelegate;
private Action m_OnMiddleClickDelegate;
private Action m_OnScrollWheelDelegate;
private Action m_OnTrackedDevicePositionDelegate;
private Action m_OnTrackedDeviceOrientationDelegate;
private Action