2023-03-28 13:24:16 -04:00
#if PACKAGE_DOCS_GENERATION | | UNITY_INPUT_SYSTEM_ENABLE_UI
using System ;
using System.Collections.Generic ;
using UnityEngine.EventSystems ;
using UnityEngine.InputSystem.Controls ;
using UnityEngine.InputSystem.LowLevel ;
using UnityEngine.InputSystem.Utilities ;
using UnityEngine.Serialization ;
using UnityEngine.UI ;
#if UNITY_EDITOR
using UnityEditor ;
#endif
////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.
//// NOTE: This does not actually solve the problem. Even if, for example, we have a PointerInput value struct and a PointerInputComposite
//// that binds the individual inputs to controls, and then we use it to bind touch0 as a pointer input source, there may still be multiple
//// touchscreens and thus multiple touches coming in through the same composite. This leads back to the same situation.
////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
////REVIEW: also give access to the last/current UI event?
////TODO: ToString() method a la PointerInputModule
namespace UnityEngine.InputSystem.UI
{
/// <summary>
/// Input module that takes its input from <see cref="InputAction">input actions</see>.
/// </summary>
/// <remarks>
/// 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.
///
/// When adding this component from code (such as through <c>GameObject.AddComponent</c>), the
/// resulting module will automatically have a set of default input actions assigned to it
/// (see <see cref="AssignDefaultActions"/>).
/// </remarks>
[HelpURL(InputSystem.kDocUrl + "/manual/UISupport.html#setting-up-ui-input")]
public class InputSystemUIInputModule : BaseInputModule
{
/// <summary>
/// Whether to clear the current selection when a click happens that does not hit any <c>GameObject</c>.
/// </summary>
/// <value>If true (default), clicking outside of any GameObject will reset the current selection.</value>
/// <remarks>
/// By toggling this behavior off, background clicks will keep the current selection. I.e.
/// <c>EventSystem.currentSelectedGameObject</c> will not be changed.
/// </remarks>
public bool deselectOnBackgroundClick
{
get = > m_DeselectOnBackgroundClick ;
set = > m_DeselectOnBackgroundClick = value ;
}
/// <summary>
/// How to deal with the presence of pointer-type input from multiple devices.
/// </summary>
/// <remarks>
/// By default, this is set to <see cref="UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack"/> which will
/// treat input from <see cref="Mouse"/> and <see cref="Pen"/> devices as coming from a single on-screen pointer
/// but will treat input from devices such as <see cref="XR.XRController"/> and <see cref="Touchscreen"/> as
/// their own discrete pointers.
///
/// The primary effect of this setting is to determine whether the user can concurrently point at more than
/// a single UI element or not. Whenever multiple pointers are allowed, more than one element may have a pointer
/// over it at any one point and thus several elements can be interacted with concurrently.
/// </remarks>
public UIPointerBehavior pointerBehavior
{
get = > m_PointerBehavior ;
set = > m_PointerBehavior = value ;
}
/// <summary>
/// Where to position the pointer when the cursor is locked.
/// </summary>
/// <remarks>
/// By default, the pointer is positioned at -1, -1 in screen space when the cursor is locked. This has implications
/// for using ray casters like <see cref="PhysicsRaycaster"/> because the raycasts will be sent from the pointer
/// position. By setting the value of <see cref="cursorLockBehavior"/> to <see cref="CursorLockBehavior.ScreenCenter"/>,
/// the raycasts will be sent from the center of the screen. This is useful when trying to interact with world space UI
/// using the <see cref="IPointerEnterHandler"/> and <see cref="IPointerExitHandler"/> interfaces when the cursor
/// is locked.
/// </remarks>
/// <see cref="Cursor.lockState"/>
public CursorLockBehavior cursorLockBehavior
{
get = > m_CursorLockBehavior ;
set = > m_CursorLockBehavior = value ;
}
/// <summary>
/// A root game object to support correct navigation in local multi-player UIs.
/// <remarks>
/// In local multi-player games where each player has their own UI, players should not be able to navigate into
/// another player's UI. Each player should have their own instance of an InputSystemUIInputModule, and this property
/// should be set to the root game object containing all UI objects for that player. If set, navigation using the
/// <see cref="InputSystemUIInputModule.move"/> action will be constrained to UI objects under that root.
/// </remarks>
/// </summary>
internal GameObject localMultiPlayerRoot
{
get = > m_LocalMultiPlayerRoot ;
set = > m_LocalMultiPlayerRoot = value ;
}
/// <summary>
/// Called by <c>EventSystem</c> when the input module is made current.
/// </summary>
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 ( ) ) ;
}
/// <summary>
/// Check whether the given pointer or touch is currently hovering over a <c>GameObject</c>.
/// </summary>
/// <param name="pointerOrTouchId">ID of the pointer or touch. Meaning this should correspond to either
/// <c>PointerEventData.pointerId</c> or <see cref="ExtendedPointerEventData.touchId"/>. The pointer ID
/// generally corresponds to the <see cref="InputDevice.deviceId"/> of the pointer device. An exception
/// to this are touches as a <see cref="Touchscreen"/> may have multiple pointers (one for each active
/// finger). For touch, you can use the <see cref="TouchControl.touchId"/> of the touch.
///
/// Note that for touch, a pointer will stay valid for one frame before being removed. In other words,
/// when <see cref="TouchPhase.Ended"/> or <see cref="TouchPhase.Canceled"/> is received for a touch
/// and the touch was over a <c>GameObject</c>, the associated pointer is still considered over that
/// object for the frame in which the touch ended.
///
/// To check whether any pointer is over a <c>GameObject</c>, simply pass a negative value such as -1.</param>
/// <returns>True if the given pointer is currently hovering over a <c>GameObject</c>.</returns>
/// <remarks>
/// The result is true if the given pointer has caused an <c>IPointerEnter</c> event to be sent to a
/// <c>GameObject</c>.
///
/// This method can be invoked via <c>EventSystem.current.IsPointerOverGameObject</c>.
///
/// Be aware that this method relies on state set up during UI event processing that happens in <c>EventSystem.Update</c>,
/// that is, as part of <c>MonoBehaviour</c> updates. This step happens <em>after</em> input processing.
/// Thus, calling this method earlier than that in the frame will make it poll state from <em>last</em> frame.
///
/// Calling this method from within an <see cref="InputAction"/> callback (such as <see cref="InputAction.performed"/>)
/// will result in a warning. See the "UI vs Game Input" sample shipped with the Input System package for
/// how to deal with this fact.
///
/// <example>
/// <code>
/// // 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.
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="ExtendedPointerEventData.touchId"/>
/// <seealso cref="InputDevice.deviceId"/>
public override bool IsPointerOverGameObject ( int pointerOrTouchId )
{
if ( InputSystem . isProcessingEvents )
Debug . LogWarning (
"Calling IsPointerOverGameObject() from within event processing (such as from InputAction callbacks) will not work as expected; it will query UI state from the last frame" ) ;
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 )
return false ;
return m_PointerStates [ stateIndex ] . eventData . pointerEnter ! = null ;
}
/// <summary>
/// Returns the most recent raycast information for a given pointer or touch.
/// </summary>
/// <param name="pointerOrTouchId">ID of the pointer or touch. Meaning this should correspond to either
/// <c>PointerEventData.pointerId</c> or <see cref="ExtendedPointerEventData.touchId"/>. The pointer ID
/// generally corresponds to the <see cref="InputDevice.deviceId"/> of the pointer device. An exception
/// to this are touches as a <see cref="Touchscreen"/> may have multiple pointers (one for each active
/// finger). For touch, you can use the <see cref="TouchControl.touchId"/> of the touch.
///
/// Negative values will return an invalid <see cref="RaycastResult"/>.</param>
/// <returns>The most recent raycast information.</returns>
/// <remarks>
/// This method is for the most recent raycast, but depending on when it's called is not guaranteed to be for the current frame.
/// This method can be used to determine raycast distances and hit information for visualization.
/// <br />
/// Use <see cref="RaycastResult.isValid"/> to determine if pointer hit anything.
/// </remarks>
/// <seealso cref="ExtendedPointerEventData.touchId"/>
/// <seealso cref="InputDevice.deviceId"/>
public RaycastResult GetLastRaycastResult ( int pointerOrTouchId )
{
var stateIndex = GetPointerStateIndexFor ( pointerOrTouchId ) ;
if ( stateIndex = = - 1 )
return default ;
return m_PointerStates [ stateIndex ] . eventData . pointerCurrentRaycast ;
}
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 )
{
var eventData = state . eventData ;
// Sync position.
var pointerType = eventData . pointerType ;
if ( pointerType = = UIPointerType . MouseOrPen & & Cursor . lockState = = CursorLockMode . Locked )
{
eventData . position = m_CursorLockBehavior = = CursorLockBehavior . OutsideScreen ?
new Vector2 ( - 1 , - 1 ) :
new Vector2 ( Screen . width / 2f , Screen . height / 2f ) ;
////REVIEW: This is consistent with StandaloneInputModule but having no deltas in locked mode seems wrong
eventData . delta = default ;
}
else if ( pointerType = = UIPointerType . Tracked )
{
var position = state . worldPosition ;
var rotation = state . worldOrientation ;
if ( m_XRTrackingOrigin ! = null )
{
position = m_XRTrackingOrigin . TransformPoint ( position ) ;
rotation = m_XRTrackingOrigin . rotation * rotation ;
}
eventData . trackedDeviceOrientation = rotation ;
eventData . trackedDevicePosition = position ;
}
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 ) ;
// Unlike StandaloneInputModule, we process moves before processing buttons. This way
// UI elements get pointer enters/exits before they get button ups/downs and clicks.
ProcessPointerMovement ( ref state , eventData ) ;
// We always need to process move-related events in order to get PointerEnter and Exit events
// when we change UI state (e.g. show/hide objects) without moving the pointer. This unfortunately
// also means that we will invariably raycast on every update.
// However, after that, early out at this point when there's no changes to the pointer state (except
// for tracked pointers as the tracking origin may have moved).
if ( ! state . changedThisFrame & & ( xrTrackingOrigin = = null | | state . pointerType ! = UIPointerType . Tracked ) )
return ;
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 ) ;
}
// 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 the *previous* frame, we generate pointer-exit events
// and then later remove the pointer.
eventData . pointerType = = UIPointerType . Touch & & ! pointer . leftButton . isPressed & & ! pointer . leftButton . wasReleasedThisFrame
? null
: eventData . pointerCurrentRaycast . gameObject ;
ProcessPointerMovement ( eventData , currentPointerTarget ) ;
}
private void ProcessPointerMovement ( ExtendedPointerEventData eventData , GameObject currentPointerTarget )
{
#if UNITY_2021_1_OR_NEWER
// If the pointer moved, send move events to all UI elements the pointer is
// currently over.
var wasMoved = eventData . IsPointerMoving ( ) ;
if ( wasMoved )
{
for ( var i = 0 ; i < eventData . hovered . Count ; + + i )
ExecuteEvents . Execute ( eventData . hovered [ i ] , eventData , ExecuteEvents . pointerMoveHandler ) ;
}
#endif
// 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 ) ;
#if UNITY_2021_1_OR_NEWER
if ( wasMoved )
ExecuteEvents . Execute ( current . gameObject , eventData , ExecuteEvents . pointerMoveHandler ) ;
#endif
eventData . hovered . Add ( current . gameObject ) ;
}
}
}
private const float kClickSpeed = 0.3f ;
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 )
{
button . pressTime = InputRuntime . s_Instance . unscaledGameTime ;
eventData . delta = Vector2 . zero ;
eventData . dragging = false ;
eventData . pressPosition = eventData . position ;
eventData . pointerPressRaycast = eventData . pointerCurrentRaycast ;
eventData . eligibleForClick = true ;
eventData . useDragThreshold = true ;
var selectHandler = ExecuteEvents . GetEventHandler < ISelectHandler > ( 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 ) ;
// If no GO responded to OnPointerDown, look for one that responds to OnPointerClick.
// NOTE: This only looks up the handler. We don't invoke OnPointerClick here.
if ( newPressed = = null )
newPressed = ExecuteEvents . GetEventHandler < IPointerClickHandler > ( currentOverGo ) ;
// Reset click state if delay to last release was too long or if we didn't
// press on the same object as last time. The latter part we don't know until
// we've actually run the press handler.
button . clickedOnSameGameObject = newPressed = = eventData . lastPress & & button . pressTime - eventData . clickTime < = kClickSpeed ;
if ( eventData . clickCount > 0 & & ! button . clickedOnSameGameObject )
{
eventData . clickCount = default ;
eventData . clickTime = default ;
}
// Set pointerPress. This nukes lastPress. Meaning that after OnPointerDown, lastPress will
// become null.
eventData . pointerPress = newPressed ;
eventData . rawPointerPress = currentOverGo ;
// Save the drag handler for drag events during this mouse down.
eventData . pointerDrag = ExecuteEvents . GetEventHandler < IDragHandler > ( currentOverGo ) ;
if ( eventData . pointerDrag ! = null )
ExecuteEvents . Execute ( eventData . pointerDrag , eventData , ExecuteEvents . initializePotentialDrag ) ;
}
// Button release.
if ( button . wasReleasedThisFrame )
{
// Check for click. Release must be on same GO that we pressed on and we must not
// have moved beyond our move tolerance (doing so will set eligibleForClick to false).
// NOTE: There's two difference to click handling here compared to StandaloneInputModule.
// 1) StandaloneInputModule counts clicks entirely on press meaning that clickCount is increased
// before a click has actually happened.
// 2) StandaloneInputModule increases click counts even if something is eventually not deemed a
// click and OnPointerClick is thus never invoked.
var pointerClickHandler = ExecuteEvents . GetEventHandler < IPointerClickHandler > ( currentOverGo ) ;
var isClick = eventData . pointerPress = = pointerClickHandler & & eventData . eligibleForClick ;
if ( isClick )
{
// Count clicks.
if ( button . clickedOnSameGameObject )
{
// We re-clicked on the same UI element within 0.3 seconds so count
// it as a repeat click.
+ + eventData . clickCount ;
}
else
{
// First click on this object.
eventData . clickCount = 1 ;
}
eventData . clickTime = InputRuntime . s_Instance . unscaledGameTime ;
}
// Invoke OnPointerUp.
ExecuteEvents . Execute ( eventData . pointerPress , eventData , ExecuteEvents . pointerUpHandler ) ;
// Invoke OnPointerClick or OnDrop.
if ( isClick )
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 . ignoreNextClick = false ;
}
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 ;
// Detect drags.
if ( ! eventData . dragging )
{
if ( ! eventData . useDragThreshold | | ( eventData . pressPosition - eventData . position ) . sqrMagnitude > =
( double ) eventSystem . pixelDragThreshold * eventSystem . pixelDragThreshold * ( eventData . pointerType = = UIPointerType . Tracked
? m_TrackedDeviceDragThresholdMultiplier
: 1 ) )
{
// Started dragging. Invoke OnBeginDrag.
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 < IScrollHandler > ( 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 ExtendedAxisEventData ( eventSystem ) ;
m_NavigationState . eventData = eventData ;
}
eventData . Reset ( ) ;
eventData . moveVector = moveVector ;
eventData . moveDir = moveDirection ;
if ( IsMoveAllowed ( eventData ) )
{
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 )
{
// NOTE: Whereas we use callbacks for the other actions, we rely on WasPressedThisFrame() for
// submit and cancel. This makes their behavior inconsistent with pointer click behavior where
// a click will register on button *up*, but consistent with how other UI systems work where
// click occurs on key press. This nuance in behavior becomes important in combination with
// action enable/disable changes in response to submit or cancel. We react to button *down*
// instead of *up*, so button *up* will come in *after* we have applied the state change.
var submitAction = m_SubmitAction ? . action ;
var cancelAction = m_CancelAction ? . action ;
var data = GetBaseEventData ( ) ;
if ( cancelAction ! = null & & cancelAction . WasPressedThisFrame ( ) )
ExecuteEvents . Execute ( eventSystem . currentSelectedGameObject , data , ExecuteEvents . cancelHandler ) ;
if ( ! data . used & & submitAction ! = null & & submitAction . WasPressedThisFrame ( ) )
ExecuteEvents . Execute ( eventSystem . currentSelectedGameObject , data , ExecuteEvents . submitHandler ) ;
}
}
private bool IsMoveAllowed ( AxisEventData eventData )
{
if ( m_LocalMultiPlayerRoot = = null )
return true ;
if ( eventSystem . currentSelectedGameObject = = null )
return true ;
var selectable = eventSystem . currentSelectedGameObject . GetComponent < Selectable > ( ) ;
if ( selectable = = null )
return true ;
Selectable navigationTarget = null ;
switch ( eventData . moveDir )
{
case MoveDirection . Right :
navigationTarget = selectable . FindSelectableOnRight ( ) ;
break ;
case MoveDirection . Up :
navigationTarget = selectable . FindSelectableOnUp ( ) ;
break ;
case MoveDirection . Left :
navigationTarget = selectable . FindSelectableOnLeft ( ) ;
break ;
case MoveDirection . Down :
navigationTarget = selectable . FindSelectableOnDown ( ) ;
break ;
}
if ( navigationTarget = = null )
return true ;
return navigationTarget . transform . IsChildOf ( m_LocalMultiPlayerRoot . transform ) ;
}
[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 ;
[Tooltip("Transform representing the real world origin for tracking devices. When using the XR Interaction Toolkit, this should be pointing to the XR Rig's Transform.")]
[SerializeField]
private Transform m_XRTrackingOrigin ;
/// <summary>
/// Delay in seconds between an initial move action and a repeated move action while <see cref="move"/> is actuated.
/// </summary>
/// <remarks>
/// While <see cref="move"/> is being held down, the input module will first wait for <see cref="moveRepeatDelay"/> seconds
/// after the first actuation of <see cref="move"/> and then trigger a move event every <see cref="moveRepeatRate"/> seconds.
/// </remarks>
/// <seealso cref="moveRepeatRate"/>
/// <seealso cref="AxisEventData"/>
/// <see cref="move"/>
public float moveRepeatDelay
{
get = > m_MoveRepeatDelay ;
set = > m_MoveRepeatDelay = value ;
}
/// <summary>
/// Delay in seconds between repeated move actions while <see cref="move"/> is actuated.
/// </summary>
/// <remarks>
/// While <see cref="move"/> is being held down, the input module will first wait for <see cref="moveRepeatDelay"/> seconds
/// after the first actuation of <see cref="move"/> and then trigger a move event every <see cref="moveRepeatRate"/> seconds.
///
/// Note that a maximum of one <see cref="AxisEventData"/> 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.
/// </remarks>
/// <seealso cref="moveRepeatDelay"/>
/// <seealso cref="AxisEventData"/>
/// <see cref="move"/>
public float moveRepeatRate
{
get = > m_MoveRepeatRate ;
set = > m_MoveRepeatRate = value ;
}
private bool explictlyIgnoreFocus = > InputSystem . settings . backgroundBehavior = = InputSettings . BackgroundBehavior . IgnoreFocus ;
private bool shouldIgnoreFocus
{
// By default, key this on whether running the background is enabled or not. Rationale is that
// if running in the background is enabled, we already have rules in place what kind of input
// is allowed through and what isn't. And for the input that *IS* allowed through, the UI should
// react.
get = > explictlyIgnoreFocus | | InputRuntime . s_Instance . runInBackground ;
}
[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 ;
}
/// <summary>
/// A <see cref="Transform"/> representing the real world origin for tracking devices.
/// This is used to convert real world positions and rotations for <see cref="UIPointerType.Tracked"/> pointers into Unity's global space.
/// When using the XR Interaction Toolkit, this should be pointing to the XR Rig's Transform.
/// </summary>
/// <remarks>This will transform all tracked pointers. If unset, or set to null, the Unity world origin will be used as the basis for all tracked positions and rotations.</remarks>
public Transform xrTrackingOrigin
{
get = > m_XRTrackingOrigin ;
set = > m_XRTrackingOrigin = value ;
}
/// <summary>
/// Scales the drag threshold of <c>EventSystem</c> for tracked devices to make selection easier.
/// </summary>
public float trackedDeviceDragThresholdMultiplier
{
get = > m_TrackedDeviceDragThresholdMultiplier ;
set = > m_TrackedDeviceDragThresholdMultiplier = value ;
}
private void SwapAction ( ref InputActionReference property , InputActionReference newValue , bool actionsHooked , Action < InputAction . CallbackContext > actionCallback )
{
if ( property = = newValue | | ( property ! = null & & newValue ! = null & & property . action = = newValue . action ) )
return ;
if ( property ! = null & & actionCallback ! = null & & actionsHooked )
{
property . action . performed - = actionCallback ;
property . action . canceled - = actionCallback ;
}
var oldActionNull = property ? . action = = null ;
var oldActionEnabled = property ? . action ! = null & & property . action . enabled ;
TryDisableInputAction ( property ) ;
property = newValue ;
#if DEBUG
// We source inputs from arbitrary pointers through a set of pointer-related actions (point, click, etc). This means that in any frame,
// multiple pointers may pipe input through to the same action and we do not want the disambiguation code in InputActionState.ShouldIgnoreControlStateChange()
// to prevent input from getting to us. Thus, these actions should generally be set to InputActionType.PassThrough.
//
// We treat navigation actions differently as there is only a single NavigationModel for the UI that all navigation input feeds into.
// Thus, those actions should be configured with disambiguation active (i.e. Move should be a Value action and Submit and Cancel should
// be Button actions). This is especially important for Submit and Cancel as we get proper press and release action this way.
if ( newValue ! = null & & newValue . action ! = null & & newValue . action . type ! = InputActionType . PassThrough & & ! IsNavigationAction ( newValue ) )
{
Debug . LogWarning ( "Pointer-related 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 pointers (action {newValue.action} is set to {newValue.action.type})" , this ) ;
}
#endif
if ( newValue ? . action ! = null & & actionCallback ! = null & & actionsHooked )
{
property . action . performed + = actionCallback ;
property . action . canceled + = actionCallback ;
}
if ( isActiveAndEnabled & & newValue ? . action ! = null & & ( oldActionEnabled | | oldActionNull ) )
EnableInputAction ( property ) ;
}
#if DEBUG
private bool IsNavigationAction ( InputActionReference reference )
{
return reference = = m_SubmitAction | | reference = = m_CancelAction | | reference = = m_MoveAction ;
}
#endif
/// <summary>
/// An <see cref="InputAction"/> delivering a <see cref="Vector2"/> 2D screen position
/// used as a cursor for pointing at UI elements.
/// </summary>
/// <remarks>
/// The values read from this action determine <see cref="PointerEventData.position"/> and <see cref="PointerEventData.delta"/>.
///
/// Together with <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and
/// <see cref="scrollWheel"/>, this forms the basis for pointer-type UI input.
///
/// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
/// <see cref="InputAction.expectedControlType"/> set to <c>"Vector2"</c>.
///
/// <example>
/// <code>
/// 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);
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="leftClick"/>
/// <seealso cref="rightClick"/>
/// <seealso cref="middleClick"/>
/// <seealso cref="scrollWheel"/>
public InputActionReference point
{
get = > m_PointAction ;
set = > SwapAction ( ref m_PointAction , value , m_ActionsHooked , m_OnPointDelegate ) ;
}
/// <summary>
/// An <see cref="InputAction"/> delivering a <c>Vector2</c> scroll wheel value
/// used for sending <see cref="PointerEventData"/> events.
/// </summary>
/// <remarks>
/// The values read from this action determine <see cref="PointerEventData.scrollDelta"/>.
///
/// Together with <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and
/// <see cref="point"/>, this forms the basis for pointer-type UI input.
///
/// Note that the action is optional. A pointer is fully functional with just <see cref="point"/>
/// and <see cref="leftClick"/> alone.
///
/// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
/// <see cref="InputAction.expectedControlType"/> set to <c>"Vector2"</c>.
///
/// <example>
/// <code>
/// 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);
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="leftClick"/>
/// <seealso cref="rightClick"/>
/// <seealso cref="middleClick"/>
/// <seealso cref="point"/>
public InputActionReference scrollWheel
{
get = > m_ScrollWheelAction ;
set = > SwapAction ( ref m_ScrollWheelAction , value , m_ActionsHooked , m_OnScrollWheelDelegate ) ;
}
/// <summary>
/// An <see cref="InputAction"/> delivering a <c>float</c> button value that determines
/// whether the left button of a pointer is pressed.
/// </summary>
/// <remarks>
/// Clicks on this button will use <see cref="PointerEventData.InputButton.Left"/> for <see cref="PointerEventData.button"/>.
///
/// Together with <see cref="point"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and
/// <see cref="scrollWheel"/>, this forms the basis for pointer-type UI input.
///
/// Note that together with <see cref="point"/>, this action is necessary for a pointer to be functional. The other clicks
/// and <see cref="scrollWheel"/> are optional, however.
///
/// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
/// <see cref="InputAction.expectedControlType"/> set to <c>"Button"</c>.
///
/// <example>
/// <code>
/// 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);
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="rightClick"/>
/// <seealso cref="middleClick"/>
/// <seealso cref="scrollWheel"/>
/// <seealso cref="point"/>
public InputActionReference leftClick
{
get = > m_LeftClickAction ;
set = > SwapAction ( ref m_LeftClickAction , value , m_ActionsHooked , m_OnLeftClickDelegate ) ;
}
/// <summary>
/// An <see cref="InputAction"/> delivering a <c>float</c> button value that determines
/// whether the middle button of a pointer is pressed.
/// </summary>
/// <remarks>
/// Clicks on this button will use <see cref="PointerEventData.InputButton.Middle"/> for <see cref="PointerEventData.button"/>.
///
/// Together with <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="scrollWheel"/>, and
/// <see cref="point"/>, this forms the basis for pointer-type UI input.
///
/// Note that the action is optional. A pointer is fully functional with just <see cref="point"/>
/// and <see cref="leftClick"/> alone.
///
/// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
/// <see cref="InputAction.expectedControlType"/> set to <c>"Button"</c>.
///
/// <example>
/// <code>
/// 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);
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="leftClick"/>
/// <seealso cref="rightClick"/>
/// <seealso cref="scrollWheel"/>
/// <seealso cref="point"/>
public InputActionReference middleClick
{
get = > m_MiddleClickAction ;
set = > SwapAction ( ref m_MiddleClickAction , value , m_ActionsHooked , m_OnMiddleClickDelegate ) ;
}
/// <summary>
/// An <see cref="InputAction"/> delivering a <c>float"</c> button value that determines
/// whether the right button of a pointer is pressed.
/// </summary>
/// <remarks>
/// Clicks on this button will use <see cref="PointerEventData.InputButton.Right"/> for <see cref="PointerEventData.button"/>.
///
/// Together with <see cref="leftClick"/>, <see cref="middleClick"/>, <see cref="scrollWheel"/>, and
/// <see cref="point"/>, this forms the basis for pointer-type UI input.
///
/// Note that the action is optional. A pointer is fully functional with just <see cref="point"/>
/// and <see cref="leftClick"/> alone.
///
/// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
/// <see cref="InputAction.expectedControlType"/> set to <c>"Button"</c>.
///
/// <example>
/// <code>
/// 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);
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="leftClick"/>
/// <seealso cref="middleClick"/>
/// <seealso cref="scrollWheel"/>
/// <seealso cref="point"/>
public InputActionReference rightClick
{
get = > m_RightClickAction ;
set = > SwapAction ( ref m_RightClickAction , value , m_ActionsHooked , m_OnRightClickDelegate ) ;
}
/// <summary>
/// An <see cref="InputAction"/> delivering a <c>Vector2</c> 2D motion vector
/// used for sending <see cref="AxisEventData"/> navigation events.
/// </summary>
/// <remarks>
/// The events generated from this input will be received by <see cref="IMoveHandler.OnMove"/>.
///
/// This action together with <see cref="submit"/> and <see cref="cancel"/> form the sources for navigation-style
/// UI input.
///
/// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
/// <see cref="InputAction.expectedControlType"/> set to <c>"Vector2"</c>.
///
/// <example>
/// <code>
/// 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);
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="submit"/>
/// <seealso cref="cancel"/>
public InputActionReference move
{
get = > m_MoveAction ;
set = > SwapAction ( ref m_MoveAction , value , m_ActionsHooked , m_OnMoveDelegate ) ;
}
/// <summary>
/// An <see cref="InputAction"/> delivering a <c>float</c> button value that determines when <c>ISubmitHandler</c>
/// is triggered.
/// </summary>
/// <remarks>
/// The events generated from this input will be received by <see cref="ISubmitHandler"/>.
///
/// This action together with <see cref="move"/> and <see cref="cancel"/> form the sources for navigation-style
/// UI input.
///
/// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.Button"/>.
///
/// <example>
/// <code>
/// 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);
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="move"/>
/// <seealso cref="cancel"/>
public InputActionReference submit
{
get = > m_SubmitAction ;
set = > SwapAction ( ref m_SubmitAction , value , m_ActionsHooked , null ) ;
}
/// <summary>
/// An <see cref="InputAction"/> delivering a <c>float</c> button value that determines when <c>ICancelHandler</c>
/// is triggered.
/// </summary>
/// <remarks>
/// The events generated from this input will be received by <see cref="ICancelHandler"/>.
///
/// This action together with <see cref="move"/> and <see cref="submit"/> form the sources for navigation-style
/// UI input.
///
/// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.Button"/>.
///
/// <example>
/// <code>
/// 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);
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="move"/>
/// <seealso cref="submit"/>
public InputActionReference cancel
{
get = > m_CancelAction ;
set = > SwapAction ( ref m_CancelAction , value , m_ActionsHooked , null ) ;
}
/// <summary>
/// An <see cref="InputAction"/> delivering a <c>Quaternion</c> value reflecting the orientation of <see cref="TrackedDevice"/>s.
/// In combination with <see cref="trackedDevicePosition"/>, this is used to determine the transform of tracked devices from which
/// to raycast into the UI scene.
/// </summary>
/// <remarks>
/// <see cref="trackedDeviceOrientation"/> and <see cref="trackedDevicePosition"/> together replace <see cref="point"/> for
/// UI input from <see cref="TrackedDevice"/>. Other than that, UI input for tracked devices is no different from "normal"
/// pointer-type input. This means that <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and
/// <see cref="scrollWheel"/> can all be used for tracked device input like for regular pointer input.
///
/// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
/// <see cref="InputAction.expectedControlType"/> set to <c>"Quaternion"</c>.
///
/// <example>
/// <code>
/// 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);
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="trackedDevicePosition"/>
public InputActionReference trackedDeviceOrientation
{
get = > m_TrackedDeviceOrientationAction ;
set = > SwapAction ( ref m_TrackedDeviceOrientationAction , value , m_ActionsHooked , m_OnTrackedDeviceOrientationDelegate ) ;
}
/// <summary>
/// An <see cref="InputAction"/> delivering a <c>Vector3</c> value reflecting the position of <see cref="TrackedDevice"/>s.
/// In combination with <see cref="trackedDeviceOrientation"/>, this is used to determine the transform of tracked devices from which
/// to raycast into the UI scene.
/// </summary>
/// <remarks>
/// <see cref="trackedDeviceOrientation"/> and <see cref="trackedDevicePosition"/> together replace <see cref="point"/> for
/// UI input from <see cref="TrackedDevice"/>. Other than that, UI input for tracked devices is no different from "normal"
/// pointer-type input. This means that <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and
/// <see cref="scrollWheel"/> can all be used for tracked device input like for regular pointer input.
///
/// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
/// <see cref="InputAction.expectedControlType"/> set to <c>"Vector3"</c>.
///
/// <example>
/// <code>
/// 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);
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="trackedDeviceOrientation"/>
public InputActionReference trackedDevicePosition
{
get = > m_TrackedDevicePositionAction ;
set = > SwapAction ( ref m_TrackedDevicePositionAction , value , m_ActionsHooked , m_OnTrackedDevicePositionDelegate ) ;
}
/// <summary>
/// Assigns default input actions asset and input actions, similar to how defaults are assigned when creating UI module in editor.
/// Useful for creating <see cref="InputSystemUIInputModule"/> at runtime.
/// </summary>
/// <remarks>
/// This instantiates <see cref="DefaultInputActions"/> and assigns it to <see cref="actionsAsset"/>. It also
/// assigns all the various individual actions such as <see cref="point"/> and <see cref="leftClick"/>.
///
/// Note that if an <c>InputSystemUIInputModule</c> component is programmatically added to a <c>GameObject</c>,
/// it will automatically receive the default actions as part of its <c>OnEnable</c> method. Use <see cref="UnassignActions"/>
/// to remove these assignments.
///
/// <example>
/// <code>
/// var go = new GameObject();
/// go.AddComponent<EventSystem>();
///
/// // Adding the UI module like this will implicitly enable it and thus lead to
/// // automatic assignment of the default input actions.
/// var uiModule = go.AddComponent<InputSystemUIInputModule>();
///
/// // Manually remove the default input actions.
/// uiModule.UnassignActions();
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="actionsAsset"/>
/// <seealso cref="DefaultInputActions"/>
public void AssignDefaultActions ( )
{
var defaultActions = new DefaultInputActions ( ) ;
actionsAsset = defaultActions . asset ;
cancel = InputActionReference . Create ( defaultActions . UI . Cancel ) ;
submit = InputActionReference . Create ( defaultActions . UI . Submit ) ;
move = InputActionReference . Create ( defaultActions . UI . Navigate ) ;
leftClick = InputActionReference . Create ( defaultActions . UI . Click ) ;
rightClick = InputActionReference . Create ( defaultActions . UI . RightClick ) ;
middleClick = InputActionReference . Create ( defaultActions . UI . MiddleClick ) ;
point = InputActionReference . Create ( defaultActions . UI . Point ) ;
scrollWheel = InputActionReference . Create ( defaultActions . UI . ScrollWheel ) ;
trackedDeviceOrientation = InputActionReference . Create ( defaultActions . UI . TrackedDeviceOrientation ) ;
trackedDevicePosition = InputActionReference . Create ( defaultActions . UI . TrackedDevicePosition ) ;
}
/// <summary>
/// Remove all action assignments, that is <see cref="actionsAsset"/> as well as all individual
/// actions such as <see cref="leftClick"/>.
/// </summary>
/// <remarks>
/// If the current actions were enabled by the UI input module, they will be disabled in the process.
/// </remarks>
/// <seealso cref="AssignDefaultActions"/>
public void UnassignActions ( )
{
actionsAsset = default ;
cancel = default ;
submit = default ;
move = default ;
leftClick = default ;
rightClick = default ;
middleClick = default ;
point = default ;
scrollWheel = default ;
trackedDeviceOrientation = default ;
trackedDevicePosition = default ;
}
[Obsolete("'trackedDeviceSelect' has been obsoleted; use 'leftClick' instead.", true)]
public InputActionReference trackedDeviceSelect
{
get = > throw new InvalidOperationException ( ) ;
set = > throw new InvalidOperationException ( ) ;
}
#if UNITY_EDITOR
protected override void Reset ( )
{
base . Reset ( ) ;
var asset = ( InputActionAsset ) AssetDatabase . LoadAssetAtPath (
UnityEngine . InputSystem . Editor . PlayerInputEditor . kDefaultInputActionsAssetPath ,
typeof ( InputActionAsset ) ) ;
// Setting default asset and actions when creating via inspector
Editor . InputSystemUIInputModuleEditor . ReassignActions ( this , asset ) ;
}
#endif
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_GlobalState . onActionControlsChanged . AddCallback ( m_OnControlsChangedDelegate ) ;
if ( HasNoActions ( ) )
AssignDefaultActions ( ) ;
ResetPointers ( ) ;
HookActions ( ) ;
EnableAllActions ( ) ;
}
protected override void OnDisable ( )
{
ResetPointers ( ) ;
InputActionState . s_GlobalState . onActionControlsChanged . RemoveCallback ( m_OnControlsChangedDelegate ) ;
DisableAllActions ( ) ;
UnhookActions ( ) ;
base . OnDisable ( ) ;
}
private void ResetPointers ( )
{
var numPointers = m_PointerStates . length ;
for ( var i = 0 ; i < numPointers ; + + i )
SendPointerExitEventsAndRemovePointer ( 0 ) ;
m_CurrentPointerId = - 1 ;
m_CurrentPointerIndex = - 1 ;
m_CurrentPointerType = UIPointerType . None ;
}
private bool HasNoActions ( )
{
if ( m_ActionsAsset ! = null )
return false ;
return m_PointAction ? . action = = null
& & m_LeftClickAction ? . action = = null
& & m_RightClickAction ? . action = = null
& & m_MiddleClickAction ? . action = = null
& & m_SubmitAction ? . action = = null
& & m_CancelAction ? . action = = null
& & m_ScrollWheelAction ? . action = = null
& & m_TrackedDeviceOrientationAction ? . action = = null
& & m_TrackedDevicePositionAction ? . action = = null ;
}
private void EnableAllActions ( )
{
EnableInputAction ( m_PointAction ) ;
EnableInputAction ( m_LeftClickAction ) ;
EnableInputAction ( m_RightClickAction ) ;
EnableInputAction ( m_MiddleClickAction ) ;
EnableInputAction ( m_MoveAction ) ;
EnableInputAction ( m_SubmitAction ) ;
EnableInputAction ( m_CancelAction ) ;
EnableInputAction ( m_ScrollWheelAction ) ;
EnableInputAction ( m_TrackedDeviceOrientationAction ) ;
EnableInputAction ( m_TrackedDevicePositionAction ) ;
}
private void DisableAllActions ( )
{
TryDisableInputAction ( m_PointAction , true ) ;
TryDisableInputAction ( m_LeftClickAction , true ) ;
TryDisableInputAction ( m_RightClickAction , true ) ;
TryDisableInputAction ( m_MiddleClickAction , true ) ;
TryDisableInputAction ( m_MoveAction , true ) ;
TryDisableInputAction ( m_SubmitAction , true ) ;
TryDisableInputAction ( m_CancelAction , true ) ;
TryDisableInputAction ( m_ScrollWheelAction , true ) ;
TryDisableInputAction ( m_TrackedDeviceOrientationAction , true ) ;
TryDisableInputAction ( m_TrackedDevicePositionAction , true ) ;
}
private void EnableInputAction ( InputActionReference inputActionReference )
{
var action = inputActionReference ? . action ;
if ( action = = null )
return ;
if ( s_InputActionReferenceCounts . TryGetValue ( action , out var referenceState ) )
{
referenceState . refCount + + ;
s_InputActionReferenceCounts [ action ] = referenceState ;
}
else
{
// if the action is already enabled but its reference count is zero then it was enabled by
// something outside the input module and the input module should never disable it.
referenceState = new InputActionReferenceState { refCount = 1 , enabledByInputModule = ! action . enabled } ;
s_InputActionReferenceCounts . Add ( action , referenceState ) ;
}
action . Enable ( ) ;
}
private void TryDisableInputAction ( InputActionReference inputActionReference , bool isComponentDisabling = false )
{
var action = inputActionReference ? . action ;
if ( action = = null )
return ;
// Don't decrement refCount when we were not responsible for incrementing it.
// I.e. when we were not enabled yet. When OnDisabled is called, isActiveAndEnabled will
// already have been set to false. In that case we pass isComponentDisabling to check if we
// came from OnDisabled and therefore need to allow disabling.
if ( ! isActiveAndEnabled & & ! isComponentDisabling )
return ;
if ( ! s_InputActionReferenceCounts . TryGetValue ( action , out var referenceState ) )
return ;
if ( referenceState . refCount - 1 = = 0 & & referenceState . enabledByInputModule )
{
action . Disable ( ) ;
s_InputActionReferenceCounts . Remove ( action ) ;
return ;
}
referenceState . refCount - - ;
s_InputActionReferenceCounts [ action ] = referenceState ;
}
private int GetPointerStateIndexFor ( int pointerOrTouchId )
{
if ( pointerOrTouchId = = m_CurrentPointerId )
return m_CurrentPointerIndex ;
for ( var i = 0 ; i < m_PointerIds . length ; + + i )
if ( m_PointerIds [ i ] = = pointerOrTouchId )
return i ;
// Search for Device or Touch Ids as a fallback
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 i ;
}
return - 1 ;
}
private ref PointerModel GetPointerStateForIndex ( int index )
{
if ( index = = 0 )
return ref m_PointerStates . firstValue ;
return ref m_PointerStates . additionalValues [ index - 1 ] ;
}
2023-05-07 18:43:11 -04:00
private int GetDisplayIndexFor ( InputControl control )
{
int displayIndex = 0 ;
if ( control . device is Pointer pointerCast )
{
displayIndex = pointerCast . displayIndex . ReadValue ( ) ;
Debug . Assert ( displayIndex < = byte . MaxValue , "Display index was larger than expected" ) ;
}
return displayIndex ;
}
2023-03-28 13:24:16 -04:00
private int GetPointerStateIndexFor ( ref InputAction . CallbackContext context )
{
if ( CheckForRemovedDevice ( ref context ) )
return - 1 ;
var phase = context . phase ;
return GetPointerStateIndexFor ( context . control , createIfNotExists : phase ! = InputActionPhase . Canceled ) ;
}
// 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 , bool createIfNotExists = true )
{
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 controlParent = control . parent ;
var touchControlIndex = m_PointerTouchControls . IndexOfReference ( controlParent ) ;
if ( touchControlIndex ! = - 1 )
{
// For touches, we cache a reference to the control of a pointer so that we don't
// have to continuously do ReadValue() on the touch ID control.
m_CurrentPointerId = m_PointerIds [ touchControlIndex ] ;
m_CurrentPointerIndex = touchControlIndex ;
m_CurrentPointerType = UIPointerType . Touch ;
return touchControlIndex ;
}
var pointerId = device . deviceId ;
var touchId = 0 ;
var touchPosition = Vector2 . zero ;
// Need to check if it's a touch so that we get a correct pointerId.
if ( controlParent is TouchControl touchControl )
{
2023-05-07 18:43:11 -04:00
touchId = touchControl . touchId . value ;
touchPosition = touchControl . position . value ;
2023-03-28 13:24:16 -04:00
}
// Could be it's a toplevel control on Touchscreen (like "<Touchscreen>/position"). In that case,
// read the touch ID from primaryTouch.
else if ( controlParent is Touchscreen touchscreen )
{
2023-05-07 18:43:11 -04:00
touchId = touchscreen . primaryTouch . touchId . value ;
touchPosition = touchscreen . primaryTouch . position . value ;
2023-03-28 13:24:16 -04:00
}
2023-05-07 18:43:11 -04:00
int displayIndex = GetDisplayIndexFor ( control ) ;
2023-03-28 13:24:16 -04:00
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).
if ( touchId = = 0 ) // Not necessary for touches; see above.
{
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 ;
}
}
}
if ( ! createIfNotExists )
return - 1 ;
// 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 )
{
2023-05-07 18:43:11 -04:00
m_CurrentPointerIndex = AllocatePointer ( pointerId , displayIndex , touchId , pointerType , control , device , touchId ! = 0 ? controlParent : null ) ;
2023-03-28 13:24:16 -04:00
}
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 . control = control ;
eventData . device = device ;
eventData . pointerType = pointerType ;
eventData . pointerId = pointerId ;
eventData . touchId = touchId ;
2023-05-07 18:43:11 -04:00
#if UNITY_2023_1_OR_NEWER
eventData . displayIndex = displayIndex ;
#endif
2023-03-28 13:24:16 -04:00
// Make sure these don't linger around when we switch to a different kind of pointer.
eventData . trackedDeviceOrientation = default ;
eventData . trackedDevicePosition = default ;
}
if ( pointerType = = UIPointerType . Touch )
GetPointerStateForIndex ( m_CurrentPointerIndex ) . screenPosition = touchPosition ;
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.
2023-05-07 18:43:11 -04:00
index = AllocatePointer ( pointerId , displayIndex , touchId , pointerType , control , device , touchId ! = 0 ? controlParent : null ) ;
2023-03-28 13:24:16 -04:00
}
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.
2023-05-07 18:43:11 -04:00
index = AllocatePointer ( pointerDevice . deviceId , displayIndex , 0 , UIPointerType . MouseOrPen , pointControls . Value [ 0 ] , pointerDevice ) ;
2023-03-28 13:24:16 -04:00
}
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.
2023-05-07 18:43:11 -04:00
index = AllocatePointer ( trackedDevice . deviceId , displayIndex , 0 , UIPointerType . Tracked , positionControls . Value [ 0 ] , trackedDevice ) ;
2023-03-28 13:24:16 -04:00
}
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.
2023-05-07 18:43:11 -04:00
index = AllocatePointer ( pointerId , displayIndex , 0 , UIPointerType . None , control , device ) ;
2023-03-28 13:24:16 -04:00
}
}
}
if ( pointerType = = UIPointerType . Touch )
GetPointerStateForIndex ( index ) . screenPosition = touchPosition ;
m_CurrentPointerId = pointerId ;
m_CurrentPointerIndex = index ;
m_CurrentPointerType = pointerType ;
return index ;
}
2023-05-07 18:43:11 -04:00
private int AllocatePointer ( int pointerId , int displayIndex , int touchId , UIPointerType pointerType , InputControl control , InputDevice device , InputControl touchControl = null )
2023-03-28 13:24:16 -04:00
{
// 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 ) ;
eventData . pointerId = pointerId ;
2023-05-07 18:43:11 -04:00
#if UNITY_2023_1_OR_NEWER
eventData . displayIndex = displayIndex ;
#endif
2023-03-28 13:24:16 -04:00
eventData . touchId = touchId ;
eventData . pointerType = pointerType ;
eventData . control = control ;
eventData . device = device ;
// Allocate state.
m_PointerIds . AppendWithCapacity ( pointerId ) ;
m_PointerTouchControls . AppendWithCapacity ( touchControl ) ;
return m_PointerStates . AppendWithCapacity ( new PointerModel ( 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!" ) ;
// Update current pointer, if necessary.
if ( index = = m_CurrentPointerIndex )
{
m_CurrentPointerId = - 1 ;
m_CurrentPointerIndex = - 1 ;
m_CurrentPointerType = default ;
}
else if ( m_CurrentPointerIndex = = m_PointerIds . length - 1 )
{
// We're about to move the last entry so update the index it will
// be at.
m_CurrentPointerIndex = index ;
}
// 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_PointerTouchControls . RemoveAtByMovingTailWithCapacity ( index ) ;
m_PointerStates . RemoveAtByMovingTailWithCapacity ( index ) ;
Debug . Assert ( m_PointerIds . length = = m_PointerStates . length , "Pointer ID array should match state array in length" ) ;
// 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 ( ! device . added | | // Check if device was removed altogether.
( ! HaveControlForDevice ( device , point ) & &
! HaveControlForDevice ( device , trackedDevicePosition ) & &
! HaveControlForDevice ( device , trackedDeviceOrientation ) ) )
{
SendPointerExitEventsAndRemovePointer ( i ) ;
- - i ;
}
}
m_NeedToPurgeStalePointers = false ;
}
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 ;
}
// The pointer actions we unfortunately cannot poll as we may be sourcing input from multiple pointers.
private void OnPointCallback ( InputAction . CallbackContext context )
{
// When a pointer is removed, there's like a non-zero coordinate on the position control and thus
// we will see cancellations on the "Point" action. Ignore these as they provide no useful values
// and we want to avoid doing a read of touch IDs in GetPointerStateFor() on an already removed
// touchscreen.
if ( CheckForRemovedDevice ( ref context ) | | context . canceled )
return ;
var index = GetPointerStateIndexFor ( context . control ) ;
if ( index = = - 1 )
return ;
ref var state = ref GetPointerStateForIndex ( index ) ;
state . screenPosition = context . ReadValue < Vector2 > ( ) ;
2023-05-07 18:43:11 -04:00
#if UNITY_2023_1_OR_NEWER
state . eventData . displayIndex = GetDisplayIndexFor ( context . control ) ;
#endif
2023-03-28 13:24:16 -04:00
}
// NOTE: In the click events, we specifically react to the Canceled phase to make sure we do NOT perform
// button *clicks* when an action resets. However, we still need to send pointer ups.
private bool IgnoreNextClick ( ref InputAction . CallbackContext context , bool wasPressed )
{
// If explicitly ignoring focus due to setting, never ignore clicks
if ( explictlyIgnoreFocus )
return false ;
// If a currently active click is cancelled (by focus change), ignore next click if device cannot run in background.
// This prevents the cancelled click event being registered when focus is returned i.e. if
// the button was released while another window was focused.
return context . canceled & & ! InputRuntime . s_Instance . isPlayerFocused & & ! context . control . device . canRunInBackground & & wasPressed ;
}
private void OnLeftClickCallback ( InputAction . CallbackContext context )
{
var index = GetPointerStateIndexFor ( ref context ) ;
if ( index = = - 1 )
return ;
ref var state = ref GetPointerStateForIndex ( index ) ;
bool wasPressed = state . leftButton . isPressed ;
state . leftButton . isPressed = context . ReadValueAsButton ( ) ;
state . changedThisFrame = true ;
if ( IgnoreNextClick ( ref context , wasPressed ) )
state . leftButton . ignoreNextClick = true ;
2023-05-07 18:43:11 -04:00
#if UNITY_2023_1_OR_NEWER
state . eventData . displayIndex = GetDisplayIndexFor ( context . control ) ;
#endif
2023-03-28 13:24:16 -04:00
}
private void OnRightClickCallback ( InputAction . CallbackContext context )
{
var index = GetPointerStateIndexFor ( ref context ) ;
if ( index = = - 1 )
return ;
ref var state = ref GetPointerStateForIndex ( index ) ;
bool wasPressed = state . rightButton . isPressed ;
state . rightButton . isPressed = context . ReadValueAsButton ( ) ;
state . changedThisFrame = true ;
if ( IgnoreNextClick ( ref context , wasPressed ) )
state . rightButton . ignoreNextClick = true ;
2023-05-07 18:43:11 -04:00
#if UNITY_2023_1_OR_NEWER
state . eventData . displayIndex = GetDisplayIndexFor ( context . control ) ;
#endif
2023-03-28 13:24:16 -04:00
}
private void OnMiddleClickCallback ( InputAction . CallbackContext context )
{
var index = GetPointerStateIndexFor ( ref context ) ;
if ( index = = - 1 )
return ;
ref var state = ref GetPointerStateForIndex ( index ) ;
bool wasPressed = state . middleButton . isPressed ;
state . middleButton . isPressed = context . ReadValueAsButton ( ) ;
state . changedThisFrame = true ;
if ( IgnoreNextClick ( ref context , wasPressed ) )
state . middleButton . ignoreNextClick = true ;
2023-05-07 18:43:11 -04:00
#if UNITY_2023_1_OR_NEWER
state . eventData . displayIndex = GetDisplayIndexFor ( context . control ) ;
#endif
2023-03-28 13:24:16 -04:00
}
private bool CheckForRemovedDevice ( ref InputAction . CallbackContext context )
{
// When a device is removed, we want to simply cancel ongoing pointer
// operations. Most importantly, we want to prevent GetPointerStateFor()
// doing ReadValue() on touch ID controls when a touchscreen has already
// been removed.
if ( context . canceled & & ! context . control . device . added )
{
m_NeedToPurgeStalePointers = true ;
return true ;
}
return false ;
}
internal const float kPixelPerLine = 20 ;
private void OnScrollCallback ( InputAction . CallbackContext context )
{
var index = GetPointerStateIndexFor ( ref context ) ;
if ( index = = - 1 )
return ;
ref var state = ref GetPointerStateForIndex ( index ) ;
// 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 < Vector2 > ( ) * ( 1 / kPixelPerLine ) ;
2023-05-07 18:43:11 -04:00
#if UNITY_2023_1_OR_NEWER
state . eventData . displayIndex = GetDisplayIndexFor ( context . control ) ;
#endif
2023-03-28 13:24:16 -04:00
}
private void OnMoveCallback ( InputAction . CallbackContext context )
{
////REVIEW: should we poll this? or set the action to not be pass-through? (ps4 controller is spamming this action)
m_NavigationState . move = context . ReadValue < Vector2 > ( ) ;
}
private void OnTrackedDeviceOrientationCallback ( InputAction . CallbackContext context )
{
var index = GetPointerStateIndexFor ( ref context ) ;
if ( index = = - 1 )
return ;
ref var state = ref GetPointerStateForIndex ( index ) ;
state . worldOrientation = context . ReadValue < Quaternion > ( ) ;
2023-05-07 18:43:11 -04:00
#if UNITY_2023_1_OR_NEWER
state . eventData . displayIndex = GetDisplayIndexFor ( context . control ) ;
#endif
2023-03-28 13:24:16 -04:00
}
private void OnTrackedDevicePositionCallback ( InputAction . CallbackContext context )
{
var index = GetPointerStateIndexFor ( ref context ) ;
if ( index = = - 1 )
return ;
ref var state = ref GetPointerStateForIndex ( index ) ;
state . worldPosition = context . ReadValue < Vector3 > ( ) ;
2023-05-07 18:43:11 -04:00
#if UNITY_2023_1_OR_NEWER
state . eventData . displayIndex = GetDisplayIndexFor ( context . control ) ;
#endif
2023-03-28 13:24:16 -04:00
}
private void OnControlsChanged ( object obj )
{
m_NeedToPurgeStalePointers = true ;
}
public override void Process ( )
{
if ( m_NeedToPurgeStalePointers )
PurgeStalePointers ( ) ;
// Reset devices of changes since we don't want to spool up changes once we gain focus.
if ( ! eventSystem . isFocused & & ! shouldIgnoreFocus )
{
for ( var i = 0 ; i < m_PointerStates . length ; + + i )
m_PointerStates [ i ] . OnFrameFinished ( ) ;
}
else
{
// Navigation input.
ProcessNavigation ( ref m_NavigationState ) ;
// Pointer input.
for ( var i = 0 ; i < m_PointerStates . length ; i + + )
{
ref var state = ref GetPointerStateForIndex ( i ) ;
state . eventData . ReadDeviceState ( ) ;
state . CopyTouchOrPenStateFrom ( state . eventData ) ;
ProcessPointer ( ref state ) ;
// If it's a touch and the touch has ended, release the pointer state.
// NOTE: We defer this by one frame such that OnPointerUp happens in the frame of release
// and OnPointerExit happens one frame later. This is so that IsPointerOverGameObject()
// stays true for the touch in the frame of release (see UI_TouchPointersAreKeptForOneFrameAfterRelease).
if ( state . pointerType = = UIPointerType . Touch & & ! state . leftButton . isPressed & & ! state . leftButton . wasReleasedThisFrame )
{
RemovePointerAtIndex ( i ) ;
- - i ;
continue ;
}
state . OnFrameFinished ( ) ;
}
}
}
#if UNITY_2021_1_OR_NEWER
public override int ConvertUIToolkitPointerId ( PointerEventData sourcePointerData )
{
// Case 1369081: when using SingleUnifiedPointer, the same (default) pointerId should be sent to UIToolkit
// regardless of pointer type or finger id.
if ( m_PointerBehavior = = UIPointerBehavior . SingleUnifiedPointer )
return UIElements . PointerId . mousePointerId ;
return sourcePointerData is ExtendedPointerEventData ep
? ep . uiToolkitPointerId
: base . ConvertUIToolkitPointerId ( sourcePointerData ) ;
}
#endif
private void HookActions ( )
{
if ( m_ActionsHooked )
return ;
if ( m_OnPointDelegate = = null )
m_OnPointDelegate = OnPointCallback ;
if ( m_OnLeftClickDelegate = = null )
m_OnLeftClickDelegate = OnLeftClickCallback ;
if ( m_OnRightClickDelegate = = null )
m_OnRightClickDelegate = OnRightClickCallback ;
if ( m_OnMiddleClickDelegate = = null )
m_OnMiddleClickDelegate = OnMiddleClickCallback ;
if ( m_OnScrollWheelDelegate = = null )
m_OnScrollWheelDelegate = OnScrollCallback ;
if ( m_OnMoveDelegate = = null )
m_OnMoveDelegate = OnMoveCallback ;
if ( m_OnTrackedDeviceOrientationDelegate = = null )
m_OnTrackedDeviceOrientationDelegate = OnTrackedDeviceOrientationCallback ;
if ( m_OnTrackedDevicePositionDelegate = = null )
m_OnTrackedDevicePositionDelegate = OnTrackedDevicePositionCallback ;
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_ScrollWheelAction , m_OnScrollWheelDelegate , install ) ;
SetActionCallback ( m_TrackedDeviceOrientationAction , m_OnTrackedDeviceOrientationDelegate , install ) ;
SetActionCallback ( m_TrackedDevicePositionAction , m_OnTrackedDevicePositionDelegate , install ) ;
}
private static void SetActionCallback ( InputActionReference actionReference , Action < InputAction . CallbackContext > 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 )
{
UnhookActions ( ) ;
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 ) ;
trackedDeviceOrientation = UpdateReferenceForNewAsset ( trackedDeviceOrientation ) ;
trackedDevicePosition = UpdateReferenceForNewAsset ( trackedDevicePosition ) ;
HookActions ( ) ;
}
}
}
[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 ;
[SerializeField, HideInInspector] internal CursorLockBehavior m_CursorLockBehavior = CursorLockBehavior . OutsideScreen ;
private static Dictionary < InputAction , InputActionReferenceState > s_InputActionReferenceCounts = new Dictionary < InputAction , InputActionReferenceState > ( ) ;
private struct InputActionReferenceState
{
public int refCount ;
public bool enabledByInputModule ;
}
[NonSerialized] private bool m_ActionsHooked ;
[NonSerialized] private bool m_NeedToPurgeStalePointers ;
private Action < InputAction . CallbackContext > m_OnPointDelegate ;
private Action < InputAction . CallbackContext > m_OnMoveDelegate ;
private Action < InputAction . CallbackContext > m_OnLeftClickDelegate ;
private Action < InputAction . CallbackContext > m_OnRightClickDelegate ;
private Action < InputAction . CallbackContext > m_OnMiddleClickDelegate ;
private Action < InputAction . CallbackContext > m_OnScrollWheelDelegate ;
private Action < InputAction . CallbackContext > m_OnTrackedDevicePositionDelegate ;
private Action < InputAction . CallbackContext > m_OnTrackedDeviceOrientationDelegate ;
private Action < object > m_OnControlsChangedDelegate ;
// Pointer-type input (also tracking-type).
[NonSerialized] private int m_CurrentPointerId = - 1 ; // Keeping track of the current pointer avoids searches in most cases.
[NonSerialized] private int m_CurrentPointerIndex = - 1 ;
[NonSerialized] internal UIPointerType m_CurrentPointerType = UIPointerType . None ;
internal InlinedArray < int > m_PointerIds ; // Index in this array maps to index in m_PointerStates. Separated out to make searching more efficient (we do a linear search).
internal InlinedArray < InputControl > m_PointerTouchControls ;
internal InlinedArray < PointerModel > m_PointerStates ;
// Navigation-type input.
private NavigationModel m_NavigationState ;
[NonSerialized] private GameObject m_LocalMultiPlayerRoot ;
/// <summary>
/// Controls the origin point of raycasts when the cursor is locked.
/// </summary>
public enum CursorLockBehavior
{
/// <summary>
/// The internal pointer position will be set to -1, -1. This short-circuits the raycasting
/// logic so no objects will be intersected. This is the default setting.
/// </summary>
OutsideScreen ,
/// <summary>
/// Raycasts will originate from the center of the screen. This mode can be useful for
/// example to check in pointer-driven FPS games if the player is looking at some world-space
/// object that implements the <see cref="IPointerEnterHandler"/> and <see cref="IPointerExitHandler"/>
/// interfaces.
/// </summary>
ScreenCenter
}
}
}
#endif