A game about forced loneliness, made by TACStudios
1#if PACKAGE_DOCS_GENERATION || UNITY_INPUT_SYSTEM_ENABLE_UI
2using System;
3using System.Collections.Generic;
4using UnityEngine.EventSystems;
5using UnityEngine.InputSystem.Controls;
6using UnityEngine.InputSystem.LowLevel;
7using UnityEngine.InputSystem.Utilities;
8using UnityEngine.Serialization;
9using UnityEngine.UI;
10#if UNITY_EDITOR
11using UnityEditor;
12#endif
13
14////FIXME: The UI is currently not reacting to pointers until they are moved after the UI module has been enabled. What needs to
15//// happen is that point, trackedDevicePosition, and trackedDeviceOrientation have initial state checks. However, for touch,
16//// we do *not* want to react to the initial value as then we also get presses (unlike with other pointers). Argh.
17
18////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)
19//// I.e. there'd be a PointerInput, a NavigationInput, and a TrackedInput composite. This would solve several problems in one go and make
20//// it much more obvious which inputs go together.
21//// NOTE: This does not actually solve the problem. Even if, for example, we have a PointerInput value struct and a PointerInputComposite
22//// 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
23//// touchscreens and thus multiple touches coming in through the same composite. This leads back to the same situation.
24
25////REVIEW: The current input model has too much complexity for pointer input; find a way to simplify this.
26
27////REVIEW: how does this/uGUI support drag-scrolls on touch? [GESTURES]
28
29////REVIEW: how does this/uGUI support two-finger right-clicks with touch? [GESTURES]
30
31////TODO: add ability to query which device was last used with any of the actions
32////REVIEW: also give access to the last/current UI event?
33
34////TODO: ToString() method a la PointerInputModule
35
36namespace UnityEngine.InputSystem.UI
37{
38 /// <summary>
39 /// Input module that takes its input from <see cref="InputAction">input actions</see>.
40 /// </summary>
41 /// <remarks>
42 /// This UI input module has the advantage over other such modules that it doesn't have to know
43 /// what devices and types of devices input is coming from. Instead, the actions hide the actual
44 /// sources of input from the module.
45 ///
46 /// When adding this component from code (such as through <c>GameObject.AddComponent</c>), the
47 /// resulting module will automatically have a set of default input actions assigned to it
48 /// (see <see cref="AssignDefaultActions"/>).
49 /// </remarks>
50 [HelpURL(InputSystem.kDocUrl + "/manual/UISupport.html#setting-up-ui-input")]
51 public class InputSystemUIInputModule : BaseInputModule
52 {
53 /// <summary>
54 /// Whether to clear the current selection when a click happens that does not hit any <c>GameObject</c>.
55 /// </summary>
56 /// <value>If true (default), clicking outside of any GameObject will reset the current selection.</value>
57 /// <remarks>
58 /// By toggling this behavior off, background clicks will keep the current selection. I.e.
59 /// <c>EventSystem.currentSelectedGameObject</c> will not be changed.
60 /// </remarks>
61 public bool deselectOnBackgroundClick
62 {
63 get => m_DeselectOnBackgroundClick;
64 set => m_DeselectOnBackgroundClick = value;
65 }
66
67 /// <summary>
68 /// How to deal with the presence of pointer-type input from multiple devices.
69 /// </summary>
70 /// <remarks>
71 /// By default, this is set to <see cref="UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack"/> which will
72 /// treat input from <see cref="Mouse"/> and <see cref="Pen"/> devices as coming from a single on-screen pointer
73 /// but will treat input from devices such as <see cref="XR.XRController"/> and <see cref="Touchscreen"/> as
74 /// their own discrete pointers.
75 ///
76 /// The primary effect of this setting is to determine whether the user can concurrently point at more than
77 /// a single UI element or not. Whenever multiple pointers are allowed, more than one element may have a pointer
78 /// over it at any one point and thus several elements can be interacted with concurrently.
79 /// </remarks>
80 public UIPointerBehavior pointerBehavior
81 {
82 get => m_PointerBehavior;
83 set => m_PointerBehavior = value;
84 }
85
86 /// <summary>
87 /// Where to position the pointer when the cursor is locked.
88 /// </summary>
89 /// <remarks>
90 /// By default, the pointer is positioned at -1, -1 in screen space when the cursor is locked. This has implications
91 /// for using ray casters like <see cref="PhysicsRaycaster"/> because the raycasts will be sent from the pointer
92 /// position. By setting the value of <see cref="cursorLockBehavior"/> to <see cref="CursorLockBehavior.ScreenCenter"/>,
93 /// the raycasts will be sent from the center of the screen. This is useful when trying to interact with world space UI
94 /// using the <see cref="IPointerEnterHandler"/> and <see cref="IPointerExitHandler"/> interfaces when the cursor
95 /// is locked.
96 /// </remarks>
97 /// <see cref="Cursor.lockState"/>
98 public CursorLockBehavior cursorLockBehavior
99 {
100 get => m_CursorLockBehavior;
101 set => m_CursorLockBehavior = value;
102 }
103
104 /// <summary>
105 /// A root game object to support correct navigation in local multi-player UIs.
106 /// <remarks>
107 /// In local multi-player games where each player has their own UI, players should not be able to navigate into
108 /// another player's UI. Each player should have their own instance of an InputSystemUIInputModule, and this property
109 /// should be set to the root game object containing all UI objects for that player. If set, navigation using the
110 /// <see cref="InputSystemUIInputModule.move"/> action will be constrained to UI objects under that root.
111 /// </remarks>
112 /// </summary>
113 internal GameObject localMultiPlayerRoot
114 {
115 get => m_LocalMultiPlayerRoot;
116 set => m_LocalMultiPlayerRoot = value;
117 }
118
119 /// <summary>
120 /// A multiplier value that allows you to adjust the scroll wheel speed sent to uGUI (Unity UI) components.
121 /// </summary>
122 /// <remarks>
123 /// This value controls the magnitude of the PointerEventData.scrollDelta value, when the scroll wheel is rotated one tick. It acts as a multiplier, so a value of 1 passes through the original value and behaves the same as the legacy Standalone Input Module.
124 ///
125 /// A value larger than one increases the scrolling speed per tick, and a value less than one decreases the speed.
126 ///
127 /// You can set this to a negative value to invert the scroll direction. A value of zero prevents mousewheel scrolling from working at all.
128 ///
129 /// Note: this has no effect on UI Toolkit content, only uGUI components.
130 /// </remarks>
131 public float scrollDeltaPerTick
132 {
133 get => m_ScrollDeltaPerTick;
134 set => m_ScrollDeltaPerTick = value;
135 }
136
137 /// <summary>
138 /// Called by <c>EventSystem</c> when the input module is made current.
139 /// </summary>
140 public override void ActivateModule()
141 {
142 base.ActivateModule();
143
144 // Select firstSelectedGameObject if nothing is selected ATM.
145 var toSelect = eventSystem.currentSelectedGameObject;
146 if (toSelect == null)
147 toSelect = eventSystem.firstSelectedGameObject;
148 eventSystem.SetSelectedGameObject(toSelect, GetBaseEventData());
149 }
150
151 /// <summary>
152 /// Check whether the given pointer or touch is currently hovering over a <c>GameObject</c>.
153 /// </summary>
154 /// <param name="pointerOrTouchId">ID of the pointer or touch. Meaning this should correspond to either
155 /// <c>PointerEventData.pointerId</c> or <see cref="ExtendedPointerEventData.touchId"/>. The pointer ID
156 /// generally corresponds to the <see cref="InputDevice.deviceId"/> of the pointer device. An exception
157 /// to this are touches as a <see cref="Touchscreen"/> may have multiple pointers (one for each active
158 /// finger). For touch, you can use the <see cref="TouchControl.touchId"/> of the touch.
159 ///
160 /// Note that for touch, a pointer will stay valid for one frame before being removed. In other words,
161 /// when <see cref="TouchPhase.Ended"/> or <see cref="TouchPhase.Canceled"/> is received for a touch
162 /// and the touch was over a <c>GameObject</c>, the associated pointer is still considered over that
163 /// object for the frame in which the touch ended.
164 ///
165 /// To check whether any pointer is over a <c>GameObject</c>, simply pass a negative value such as -1.</param>
166 /// <returns>True if the given pointer is currently hovering over a <c>GameObject</c>.</returns>
167 /// <remarks>
168 /// The result is true if the given pointer has caused an <c>IPointerEnter</c> event to be sent to a
169 /// <c>GameObject</c>.
170 ///
171 /// This method can be invoked via <c>EventSystem.current.IsPointerOverGameObject</c>.
172 ///
173 /// Be aware that this method relies on state set up during UI event processing that happens in <c>EventSystem.Update</c>,
174 /// that is, as part of <c>MonoBehaviour</c> updates. This step happens <em>after</em> input processing.
175 /// Thus, calling this method earlier than that in the frame will make it poll state from <em>last</em> frame.
176 ///
177 /// Calling this method from within an <see cref="InputAction"/> callback (such as <see cref="InputAction.performed"/>)
178 /// will result in a warning. See the "UI vs Game Input" sample shipped with the Input System package for
179 /// how to deal with this fact.
180 ///
181 /// <example>
182 /// <code>
183 /// // In general, the pointer ID corresponds to the device ID:
184 /// EventSystem.current.IsPointerOverGameObject(XRController.leftHand.deviceId);
185 /// EventSystem.current.IsPointerOverGameObject(Mouse.current.deviceId);
186 ///
187 /// // For touch input, pass the ID of a touch:
188 /// EventSystem.current.IsPointerOverGameObject(Touchscreen.primaryTouch.touchId.ReadValue());
189 ///
190 /// // But can also pass the ID of the entire Touchscreen in which case the result
191 /// // is true if any touch is over a GameObject:
192 /// EventSystem.current.IsPointerOverGameObject(Touchscreen.current.deviceId);
193 ///
194 /// // Finally, any negative value will be interpreted as "any pointer" and will
195 /// // return true if any one pointer is currently over a GameObject:
196 /// EventSystem.current.IsPointerOverGameObject(-1);
197 /// EventSystem.current.IsPointerOverGameObject(); // Equivalent.
198 /// </code>
199 /// </example>
200 /// </remarks>
201 /// <seealso cref="ExtendedPointerEventData.touchId"/>
202 /// <seealso cref="InputDevice.deviceId"/>
203 public override bool IsPointerOverGameObject(int pointerOrTouchId)
204 {
205 if (InputSystem.isProcessingEvents)
206 Debug.LogWarning(
207 "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");
208
209 var stateIndex = -1;
210
211 if (pointerOrTouchId < 0)
212 {
213 if (m_CurrentPointerId != -1)
214 {
215 stateIndex = m_CurrentPointerIndex;
216 }
217 else
218 {
219 // No current pointer. Can happen, for example, when a touch just ended and its pointer record
220 // was removed as a result. If we still have some active pointer, use it.
221 if (m_PointerStates.length > 0)
222 stateIndex = 0;
223 }
224 }
225 else
226 {
227 stateIndex = GetPointerStateIndexFor(pointerOrTouchId);
228 }
229
230 if (stateIndex == -1)
231 return false;
232
233 return m_PointerStates[stateIndex].eventData.pointerEnter != null;
234 }
235
236 /// <summary>
237 /// Returns the most recent raycast information for a given pointer or touch.
238 /// </summary>
239 /// <param name="pointerOrTouchId">ID of the pointer or touch. Meaning this should correspond to either
240 /// <c>PointerEventData.pointerId</c> or <see cref="ExtendedPointerEventData.touchId"/>. The pointer ID
241 /// generally corresponds to the <see cref="InputDevice.deviceId"/> of the pointer device. An exception
242 /// to this are touches as a <see cref="Touchscreen"/> may have multiple pointers (one for each active
243 /// finger). For touch, you can use the <see cref="TouchControl.touchId"/> of the touch.
244 ///
245 /// Negative values will return an invalid <see cref="RaycastResult"/>.</param>
246 /// <returns>The most recent raycast information.</returns>
247 /// <remarks>
248 /// This method is for the most recent raycast, but depending on when it's called is not guaranteed to be for the current frame.
249 /// This method can be used to determine raycast distances and hit information for visualization.
250 /// <br />
251 /// Use <see cref="RaycastResult.isValid"/> to determine if pointer hit anything.
252 /// </remarks>
253 /// <seealso cref="ExtendedPointerEventData.touchId"/>
254 /// <seealso cref="InputDevice.deviceId"/>
255 public RaycastResult GetLastRaycastResult(int pointerOrTouchId)
256 {
257 var stateIndex = GetPointerStateIndexFor(pointerOrTouchId);
258 if (stateIndex == -1)
259 return default;
260
261 return m_PointerStates[stateIndex].eventData.pointerCurrentRaycast;
262 }
263
264 private RaycastResult PerformRaycast(ExtendedPointerEventData eventData)
265 {
266 if (eventData == null)
267 throw new ArgumentNullException(nameof(eventData));
268
269 // If it's an event from a tracked device, see if we have a TrackedDeviceRaycaster and give it
270 // the first shot.
271 if (eventData.pointerType == UIPointerType.Tracked && TrackedDeviceRaycaster.s_Instances.length > 0)
272 {
273 for (var i = 0; i < TrackedDeviceRaycaster.s_Instances.length; ++i)
274 {
275 var trackedDeviceRaycaster = TrackedDeviceRaycaster.s_Instances[i];
276 m_RaycastResultCache.Clear();
277 trackedDeviceRaycaster.PerformRaycast(eventData, m_RaycastResultCache);
278 if (m_RaycastResultCache.Count > 0)
279 {
280 var raycastResult = m_RaycastResultCache[0];
281 m_RaycastResultCache.Clear();
282 return raycastResult;
283 }
284 }
285 return default;
286 }
287
288 // Otherwise pass it along to the normal raycasting logic.
289 eventSystem.RaycastAll(eventData, m_RaycastResultCache);
290 var result = FindFirstRaycast(m_RaycastResultCache);
291 m_RaycastResultCache.Clear();
292 return result;
293 }
294
295 // Mouse, pen, touch, and tracked device pointer input all go through here.
296 private void ProcessPointer(ref PointerModel state)
297 {
298 var eventData = state.eventData;
299
300 // Sync position.
301 var pointerType = eventData.pointerType;
302 if (pointerType == UIPointerType.MouseOrPen && Cursor.lockState == CursorLockMode.Locked)
303 {
304 eventData.position = m_CursorLockBehavior == CursorLockBehavior.OutsideScreen ?
305 new Vector2(-1, -1) :
306 new Vector2(Screen.width / 2f, Screen.height / 2f);
307 ////REVIEW: This is consistent with StandaloneInputModule but having no deltas in locked mode seems wrong
308 eventData.delta = default;
309 }
310 else if (pointerType == UIPointerType.Tracked)
311 {
312 var position = state.worldPosition;
313 var rotation = state.worldOrientation;
314 if (m_XRTrackingOrigin != null)
315 {
316 position = m_XRTrackingOrigin.TransformPoint(position);
317 rotation = m_XRTrackingOrigin.rotation * rotation;
318 }
319
320 eventData.trackedDeviceOrientation = rotation;
321 eventData.trackedDevicePosition = position;
322 }
323 else
324 {
325 eventData.delta = state.screenPosition - eventData.position;
326 eventData.position = state.screenPosition;
327 }
328
329 // Clear the 'used' flag.
330 eventData.Reset();
331
332 // Raycast from current position.
333 eventData.pointerCurrentRaycast = PerformRaycast(eventData);
334
335 // Sync position for tracking devices. For those, we can only do this
336 // after the raycast as the screen-space position is a byproduct of the raycast.
337 if (pointerType == UIPointerType.Tracked && eventData.pointerCurrentRaycast.isValid)
338 {
339 var screenPos = eventData.pointerCurrentRaycast.screenPosition;
340 eventData.delta = screenPos - eventData.position;
341 eventData.position = eventData.pointerCurrentRaycast.screenPosition;
342 }
343
344 ////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
345
346 // Left mouse button. Movement and scrolling is processed with event set left button.
347 eventData.button = PointerEventData.InputButton.Left;
348 state.leftButton.CopyPressStateTo(eventData);
349
350 // Unlike StandaloneInputModule, we process moves before processing buttons. This way
351 // UI elements get pointer enters/exits before they get button ups/downs and clicks.
352 ProcessPointerMovement(ref state, eventData);
353
354 // We always need to process move-related events in order to get PointerEnter and Exit events
355 // when we change UI state (e.g. show/hide objects) without moving the pointer. This unfortunately
356 // also means that we will invariably raycast on every update.
357 // However, after that, early out at this point when there's no changes to the pointer state (except
358 // for tracked pointers as the tracking origin may have moved).
359 if (!state.changedThisFrame && (xrTrackingOrigin == null || state.pointerType != UIPointerType.Tracked))
360 return;
361
362 ProcessPointerButton(ref state.leftButton, eventData);
363 ProcessPointerButtonDrag(ref state.leftButton, eventData);
364 ProcessPointerScroll(ref state, eventData);
365
366 // Right mouse button.
367 eventData.button = PointerEventData.InputButton.Right;
368 state.rightButton.CopyPressStateTo(eventData);
369
370 ProcessPointerButton(ref state.rightButton, eventData);
371 ProcessPointerButtonDrag(ref state.rightButton, eventData);
372
373 // Middle mouse button.
374 eventData.button = PointerEventData.InputButton.Middle;
375 state.middleButton.CopyPressStateTo(eventData);
376
377 ProcessPointerButton(ref state.middleButton, eventData);
378 ProcessPointerButtonDrag(ref state.middleButton, eventData);
379 }
380
381 // if we are using a MultiplayerEventSystem, ignore any transforms
382 // not under the current MultiplayerEventSystem's root.
383 private bool PointerShouldIgnoreTransform(Transform t)
384 {
385 if (eventSystem is MultiplayerEventSystem multiplayerEventSystem && multiplayerEventSystem.playerRoot != null)
386 {
387 if (!t.IsChildOf(multiplayerEventSystem.playerRoot.transform))
388 return true;
389 }
390 return false;
391 }
392
393 private void ProcessPointerMovement(ref PointerModel pointer, ExtendedPointerEventData eventData)
394 {
395 var currentPointerTarget =
396 // If the pointer is a touch that was released the *previous* frame, we generate pointer-exit events
397 // and then later remove the pointer.
398 eventData.pointerType == UIPointerType.Touch && !pointer.leftButton.isPressed && !pointer.leftButton.wasReleasedThisFrame
399 ? null
400 : eventData.pointerCurrentRaycast.gameObject;
401
402 ProcessPointerMovement(eventData, currentPointerTarget);
403 }
404
405 private void ProcessPointerMovement(ExtendedPointerEventData eventData, GameObject currentPointerTarget)
406 {
407#if UNITY_2021_1_OR_NEWER
408 // If the pointer moved, send move events to all UI elements the pointer is
409 // currently over.
410 var wasMoved = eventData.IsPointerMoving();
411 if (wasMoved)
412 {
413 for (var i = 0; i < eventData.hovered.Count; ++i)
414 ExecuteEvents.Execute(eventData.hovered[i], eventData, ExecuteEvents.pointerMoveHandler);
415 }
416#endif
417
418 // If we have no target or pointerEnter has been deleted,
419 // we just send exit events to anything we are tracking
420 // and then exit.
421 if (currentPointerTarget == null || eventData.pointerEnter == null)
422 {
423 for (var i = 0; i < eventData.hovered.Count; ++i)
424 ExecuteEvents.Execute(eventData.hovered[i], eventData, ExecuteEvents.pointerExitHandler);
425
426 eventData.hovered.Clear();
427
428 if (currentPointerTarget == null)
429 {
430 eventData.pointerEnter = null;
431 return;
432 }
433 }
434
435 if (eventData.pointerEnter == currentPointerTarget && currentPointerTarget)
436 return;
437
438 Transform commonRoot = FindCommonRoot(eventData.pointerEnter, currentPointerTarget)?.transform;
439 Transform pointerParent = ((Component)currentPointerTarget.GetComponentInParent<IPointerExitHandler>())?.transform;
440
441 // We walk up the tree until a common root and the last entered and current entered object is found.
442 // Then send exit and enter events up to, but not including, the common root.
443 // ** or when !m_SendPointerEnterToParent, stop when meeting a gameobject with an exit event handler
444 if (eventData.pointerEnter != null)
445 {
446 var current = eventData.pointerEnter.transform;
447 while (current != null)
448 {
449 // if we reach the common root break out!
450 if (sendPointerHoverToParent && current == commonRoot)
451 break;
452
453 // if we reach a PointerExitEvent break out!
454 if (!sendPointerHoverToParent && current == pointerParent)
455 break;
456
457#if UNITY_2021_3_OR_NEWER
458 eventData.fullyExited = current != commonRoot && eventData.pointerEnter != currentPointerTarget;
459#endif
460 ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerExitHandler);
461 eventData.hovered.Remove(current.gameObject);
462
463 if (sendPointerHoverToParent)
464 current = current.parent;
465
466 // if we reach the common root break out!
467 if (current == commonRoot)
468 break;
469
470 if (!sendPointerHoverToParent)
471 current = current.parent;
472 }
473 }
474
475 // now issue the enter call up to but not including the common root
476 Transform oldPointerEnter = eventData.pointerEnter ? eventData.pointerEnter.transform : null;
477 eventData.pointerEnter = currentPointerTarget;
478 if (currentPointerTarget != null)
479 {
480 Transform current = currentPointerTarget.transform;
481 while (current != null && !PointerShouldIgnoreTransform(current))
482 {
483#if UNITY_2021_3_OR_NEWER
484 eventData.reentered = current == commonRoot && current != oldPointerEnter;
485 // if we are sending the event to parent, they are already in hover mode at that point. No need to bubble up the event.
486 if (sendPointerHoverToParent && eventData.reentered)
487 break;
488#endif
489
490 ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerEnterHandler);
491#if UNITY_2021_1_OR_NEWER
492 if (wasMoved)
493 ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerMoveHandler);
494#endif
495 eventData.hovered.Add(current.gameObject);
496
497 // stop when encountering an object with the pointerEnterHandler
498 if (!sendPointerHoverToParent && current.GetComponent<IPointerEnterHandler>() != null)
499 break;
500
501 if (sendPointerHoverToParent)
502 current = current.parent;
503
504 // if we reach the common root break out!
505 if (current == commonRoot)
506 break;
507
508 if (!sendPointerHoverToParent)
509 current = current.parent;
510 }
511 }
512 }
513
514 private const float kClickSpeed = 0.3f;
515
516 private void ProcessPointerButton(ref PointerModel.ButtonState button, PointerEventData eventData)
517 {
518 var currentOverGo = eventData.pointerCurrentRaycast.gameObject;
519
520 if (currentOverGo != null && PointerShouldIgnoreTransform(currentOverGo.transform))
521 return;
522
523 // Button press.
524 if (button.wasPressedThisFrame)
525 {
526 button.pressTime = InputRuntime.s_Instance.unscaledGameTime;
527
528 eventData.delta = Vector2.zero;
529 eventData.dragging = false;
530 eventData.pressPosition = eventData.position;
531 eventData.pointerPressRaycast = eventData.pointerCurrentRaycast;
532 eventData.eligibleForClick = true;
533 eventData.useDragThreshold = true;
534
535 var selectHandler = ExecuteEvents.GetEventHandler<ISelectHandler>(currentOverGo);
536
537 // If we have clicked something new, deselect the old thing and leave 'selection handling' up
538 // to the press event (except if there's none and we're told to not deselect in that case).
539 if (selectHandler != eventSystem.currentSelectedGameObject && (selectHandler != null || m_DeselectOnBackgroundClick))
540 eventSystem.SetSelectedGameObject(null, eventData);
541
542 // Invoke OnPointerDown, if present.
543 var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, eventData, ExecuteEvents.pointerDownHandler);
544
545 var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
546
547 // If no GO responded to OnPointerDown, look for one that responds to OnPointerClick.
548 // NOTE: This only looks up the handler. We don't invoke OnPointerClick here.
549 if (newPressed == null)
550 newPressed = pointerClickHandler;
551
552 // Reset click state if delay to last release was too long or if we didn't
553 // press on the same object as last time. The latter part we don't know until
554 // we've actually run the press handler.
555 button.clickedOnSameGameObject = newPressed == eventData.lastPress && button.pressTime - eventData.clickTime <= kClickSpeed;
556 if (eventData.clickCount > 0 && !button.clickedOnSameGameObject)
557 {
558 eventData.clickCount = default;
559 eventData.clickTime = default;
560 }
561
562 // Set pointerPress. This nukes lastPress. Meaning that after OnPointerDown, lastPress will
563 // become null.
564 eventData.pointerPress = newPressed;
565#if UNITY_2020_1_OR_NEWER // pointerClick doesn't exist before this.
566 eventData.pointerClick = pointerClickHandler;
567#endif
568 eventData.rawPointerPress = currentOverGo;
569
570 // Save the drag handler for drag events during this mouse down.
571 eventData.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);
572
573 if (eventData.pointerDrag != null)
574 ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.initializePotentialDrag);
575 }
576
577 // Button release.
578 if (button.wasReleasedThisFrame)
579 {
580 // Check for click. Release must be on same GO that we pressed on and we must not
581 // have moved beyond our move tolerance (doing so will set eligibleForClick to false).
582 // NOTE: There's two difference to click handling here compared to StandaloneInputModule.
583 // 1) StandaloneInputModule counts clicks entirely on press meaning that clickCount is increased
584 // before a click has actually happened.
585 // 2) StandaloneInputModule increases click counts even if something is eventually not deemed a
586 // click and OnPointerClick is thus never invoked.
587 var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
588#if UNITY_2020_1_OR_NEWER
589 var isClick = eventData.pointerClick != null && eventData.pointerClick == pointerClickHandler && eventData.eligibleForClick;
590#else
591 var isClick = eventData.pointerPress != null && eventData.pointerPress == pointerClickHandler && eventData.eligibleForClick;
592#endif
593 if (isClick)
594 {
595 // Count clicks.
596 if (button.clickedOnSameGameObject)
597 {
598 // We re-clicked on the same UI element within 0.3 seconds so count
599 // it as a repeat click.
600 ++eventData.clickCount;
601 }
602 else
603 {
604 // First click on this object.
605 eventData.clickCount = 1;
606 }
607 eventData.clickTime = InputRuntime.s_Instance.unscaledGameTime;
608 }
609
610 // Invoke OnPointerUp.
611 ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerUpHandler);
612
613 // Invoke OnPointerClick or OnDrop.
614 if (isClick)
615 {
616#if UNITY_2020_1_OR_NEWER
617 ExecuteEvents.Execute(eventData.pointerClick, eventData, ExecuteEvents.pointerClickHandler);
618#else
619 ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerClickHandler);
620#endif
621 }
622 else if (eventData.dragging && eventData.pointerDrag != null)
623 ExecuteEvents.ExecuteHierarchy(currentOverGo, eventData, ExecuteEvents.dropHandler);
624
625 eventData.eligibleForClick = false;
626 eventData.pointerPress = null;
627 eventData.rawPointerPress = null;
628
629 if (eventData.dragging && eventData.pointerDrag != null)
630 ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.endDragHandler);
631
632 eventData.dragging = false;
633 eventData.pointerDrag = null;
634
635 button.ignoreNextClick = false;
636 }
637
638 button.CopyPressStateFrom(eventData);
639 }
640
641 private void ProcessPointerButtonDrag(ref PointerModel.ButtonState button, ExtendedPointerEventData eventData)
642 {
643 if (!eventData.IsPointerMoving() ||
644 (eventData.pointerType == UIPointerType.MouseOrPen && Cursor.lockState == CursorLockMode.Locked) ||
645 eventData.pointerDrag == null)
646 return;
647
648 // Detect drags.
649 if (!eventData.dragging)
650 {
651 if (!eventData.useDragThreshold || (eventData.pressPosition - eventData.position).sqrMagnitude >=
652 (double)eventSystem.pixelDragThreshold * eventSystem.pixelDragThreshold * (eventData.pointerType == UIPointerType.Tracked
653 ? m_TrackedDeviceDragThresholdMultiplier
654 : 1))
655 {
656 // Started dragging. Invoke OnBeginDrag.
657 ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.beginDragHandler);
658 eventData.dragging = true;
659 }
660 }
661
662 if (eventData.dragging)
663 {
664 // If we moved from our initial press object, process an up for that object.
665 if (eventData.pointerPress != eventData.pointerDrag)
666 {
667 ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerUpHandler);
668
669 eventData.eligibleForClick = false;
670 eventData.pointerPress = null;
671 eventData.rawPointerPress = null;
672 }
673
674 ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.dragHandler);
675 button.CopyPressStateFrom(eventData);
676 }
677 }
678
679 private static void ProcessPointerScroll(ref PointerModel pointer, PointerEventData eventData)
680 {
681 var scrollDelta = pointer.scrollDelta;
682 if (!Mathf.Approximately(scrollDelta.sqrMagnitude, 0.0f))
683 {
684 eventData.scrollDelta = scrollDelta;
685 var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(eventData.pointerEnter);
686 ExecuteEvents.ExecuteHierarchy(scrollHandler, eventData, ExecuteEvents.scrollHandler);
687 }
688 }
689
690 internal void ProcessNavigation(ref NavigationModel navigationState)
691 {
692 var usedSelectionChange = false;
693 if (eventSystem.currentSelectedGameObject != null)
694 {
695 var data = GetBaseEventData();
696 ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.updateSelectedHandler);
697 usedSelectionChange = data.used;
698 }
699
700 // Don't send move events if disabled in the EventSystem.
701 if (!eventSystem.sendNavigationEvents)
702 return;
703
704 // Process move.
705 var movement = navigationState.move;
706 if (!usedSelectionChange && (!Mathf.Approximately(movement.x, 0f) || !Mathf.Approximately(movement.y, 0f)))
707 {
708 var time = InputRuntime.s_Instance.unscaledGameTime;
709 var moveVector = navigationState.move;
710
711 var moveDirection = MoveDirection.None;
712 if (moveVector.sqrMagnitude > 0)
713 {
714 if (Mathf.Abs(moveVector.x) > Mathf.Abs(moveVector.y))
715 moveDirection = moveVector.x > 0 ? MoveDirection.Right : MoveDirection.Left;
716 else
717 moveDirection = moveVector.y > 0 ? MoveDirection.Up : MoveDirection.Down;
718 }
719
720 ////REVIEW: is resetting move repeats when direction changes really useful behavior?
721 if (moveDirection != m_NavigationState.lastMoveDirection)
722 m_NavigationState.consecutiveMoveCount = 0;
723
724 if (moveDirection != MoveDirection.None)
725 {
726 var allow = true;
727 if (m_NavigationState.consecutiveMoveCount != 0)
728 {
729 if (m_NavigationState.consecutiveMoveCount > 1)
730 allow = time > m_NavigationState.lastMoveTime + moveRepeatRate;
731 else
732 allow = time > m_NavigationState.lastMoveTime + moveRepeatDelay;
733 }
734
735 if (allow)
736 {
737 var eventData = m_NavigationState.eventData;
738 if (eventData == null)
739 {
740 eventData = new ExtendedAxisEventData(eventSystem);
741 m_NavigationState.eventData = eventData;
742 }
743 eventData.Reset();
744
745 eventData.moveVector = moveVector;
746 eventData.moveDir = moveDirection;
747
748 if (IsMoveAllowed(eventData))
749 {
750 ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, eventData, ExecuteEvents.moveHandler);
751 usedSelectionChange = eventData.used;
752
753 m_NavigationState.consecutiveMoveCount = m_NavigationState.consecutiveMoveCount + 1;
754 m_NavigationState.lastMoveTime = time;
755 m_NavigationState.lastMoveDirection = moveDirection;
756 }
757 }
758 }
759 else
760 m_NavigationState.consecutiveMoveCount = 0;
761 }
762 else
763 {
764 m_NavigationState.consecutiveMoveCount = 0;
765 }
766
767 // Process submit and cancel events.
768 if (!usedSelectionChange && eventSystem.currentSelectedGameObject != null)
769 {
770 // NOTE: Whereas we use callbacks for the other actions, we rely on WasPressedThisFrame() for
771 // submit and cancel. This makes their behavior inconsistent with pointer click behavior where
772 // a click will register on button *up*, but consistent with how other UI systems work where
773 // click occurs on key press. This nuance in behavior becomes important in combination with
774 // action enable/disable changes in response to submit or cancel. We react to button *down*
775 // instead of *up*, so button *up* will come in *after* we have applied the state change.
776 var submitAction = m_SubmitAction?.action;
777 var cancelAction = m_CancelAction?.action;
778
779 var data = GetBaseEventData();
780 if (cancelAction != null && cancelAction.WasPerformedThisFrame())
781 ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.cancelHandler);
782 if (!data.used && submitAction != null && submitAction.WasPerformedThisFrame())
783 ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.submitHandler);
784 }
785 }
786
787 private bool IsMoveAllowed(AxisEventData eventData)
788 {
789 if (m_LocalMultiPlayerRoot == null)
790 return true;
791
792 if (eventSystem.currentSelectedGameObject == null)
793 return true;
794
795 var selectable = eventSystem.currentSelectedGameObject.GetComponent<Selectable>();
796
797 if (selectable == null)
798 return true;
799
800 Selectable navigationTarget = null;
801 switch (eventData.moveDir)
802 {
803 case MoveDirection.Right:
804 navigationTarget = selectable.FindSelectableOnRight();
805 break;
806
807 case MoveDirection.Up:
808 navigationTarget = selectable.FindSelectableOnUp();
809 break;
810
811 case MoveDirection.Left:
812 navigationTarget = selectable.FindSelectableOnLeft();
813 break;
814
815 case MoveDirection.Down:
816 navigationTarget = selectable.FindSelectableOnDown();
817 break;
818 }
819
820 if (navigationTarget == null)
821 return true;
822
823 return navigationTarget.transform.IsChildOf(m_LocalMultiPlayerRoot.transform);
824 }
825
826 [FormerlySerializedAs("m_RepeatDelay")]
827 [Tooltip("The Initial delay (in seconds) between an initial move action and a repeated move action.")]
828 [SerializeField]
829 private float m_MoveRepeatDelay = 0.5f;
830
831 [FormerlySerializedAs("m_RepeatRate")]
832 [Tooltip("The speed (in seconds) that the move action repeats itself once repeating (max 1 per frame).")]
833 [SerializeField]
834 private float m_MoveRepeatRate = 0.1f;
835
836 [Tooltip("Scales the Eventsystem.DragThreshold, for tracked devices, to make selection easier.")]
837 // Hide this while we still have to figure out what to do with this.
838 private float m_TrackedDeviceDragThresholdMultiplier = 2.0f;
839
840 [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.")]
841 [SerializeField]
842 private Transform m_XRTrackingOrigin;
843
844 /// <summary>
845 /// Delay in seconds between an initial move action and a repeated move action while <see cref="move"/> is actuated.
846 /// </summary>
847 /// <remarks>
848 /// While <see cref="move"/> is being held down, the input module will first wait for <see cref="moveRepeatDelay"/> seconds
849 /// after the first actuation of <see cref="move"/> and then trigger a move event every <see cref="moveRepeatRate"/> seconds.
850 /// </remarks>
851 /// <seealso cref="moveRepeatRate"/>
852 /// <seealso cref="AxisEventData"/>
853 /// <see cref="move"/>
854 public float moveRepeatDelay
855 {
856 get => m_MoveRepeatDelay;
857 set => m_MoveRepeatDelay = value;
858 }
859
860 /// <summary>
861 /// Delay in seconds between repeated move actions while <see cref="move"/> is actuated.
862 /// </summary>
863 /// <remarks>
864 /// While <see cref="move"/> is being held down, the input module will first wait for <see cref="moveRepeatDelay"/> seconds
865 /// after the first actuation of <see cref="move"/> and then trigger a move event every <see cref="moveRepeatRate"/> seconds.
866 ///
867 /// Note that a maximum of one <see cref="AxisEventData"/> will be sent per frame. This means that even if multiple time
868 /// increments of the repeat delay have passed since the last update, only one move repeat event will be generated.
869 /// </remarks>
870 /// <seealso cref="moveRepeatDelay"/>
871 /// <seealso cref="AxisEventData"/>
872 /// <see cref="move"/>
873 public float moveRepeatRate
874 {
875 get => m_MoveRepeatRate;
876 set => m_MoveRepeatRate = value;
877 }
878
879 private bool explictlyIgnoreFocus => InputSystem.settings.backgroundBehavior == InputSettings.BackgroundBehavior.IgnoreFocus;
880
881 private bool shouldIgnoreFocus
882 {
883 // By default, key this on whether running the background is enabled or not. Rationale is that
884 // if running in the background is enabled, we already have rules in place what kind of input
885 // is allowed through and what isn't. And for the input that *IS* allowed through, the UI should
886 // react.
887 get => explictlyIgnoreFocus || InputRuntime.s_Instance.runInBackground;
888 }
889
890 [Obsolete("'repeatRate' has been obsoleted; use 'moveRepeatRate' instead. (UnityUpgradable) -> moveRepeatRate", false)]
891 public float repeatRate
892 {
893 get => moveRepeatRate;
894 set => moveRepeatRate = value;
895 }
896
897 [Obsolete("'repeatDelay' has been obsoleted; use 'moveRepeatDelay' instead. (UnityUpgradable) -> moveRepeatDelay", false)]
898 public float repeatDelay
899 {
900 get => moveRepeatDelay;
901 set => moveRepeatDelay = value;
902 }
903
904 /// <summary>
905 /// A <see cref="Transform"/> representing the real world origin for tracking devices.
906 /// This is used to convert real world positions and rotations for <see cref="UIPointerType.Tracked"/> pointers into Unity's global space.
907 /// When using the XR Interaction Toolkit, this should be pointing to the XR Rig's Transform.
908 /// </summary>
909 /// <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>
910 public Transform xrTrackingOrigin
911 {
912 get => m_XRTrackingOrigin;
913 set => m_XRTrackingOrigin = value;
914 }
915
916 /// <summary>
917 /// Scales the drag threshold of <c>EventSystem</c> for tracked devices to make selection easier.
918 /// </summary>
919 public float trackedDeviceDragThresholdMultiplier
920 {
921 get => m_TrackedDeviceDragThresholdMultiplier;
922 set => m_TrackedDeviceDragThresholdMultiplier = value;
923 }
924
925 private void SwapAction(ref InputActionReference property, InputActionReference newValue, bool actionsHooked, Action<InputAction.CallbackContext> actionCallback)
926 {
927 if (property == newValue || (property != null && newValue != null && property.action == newValue.action))
928 return;
929
930 if (property != null && actionCallback != null && actionsHooked)
931 {
932 property.action.performed -= actionCallback;
933 property.action.canceled -= actionCallback;
934 }
935
936 var oldActionNull = property?.action == null;
937 var oldActionEnabled = property?.action != null && property.action.enabled;
938
939 TryDisableInputAction(property);
940 property = newValue;
941
942 #if DEBUG
943 // We source inputs from arbitrary pointers through a set of pointer-related actions (point, click, etc). This means that in any frame,
944 // multiple pointers may pipe input through to the same action and we do not want the disambiguation code in InputActionState.ShouldIgnoreControlStateChange()
945 // to prevent input from getting to us. Thus, these actions should generally be set to InputActionType.PassThrough.
946 //
947 // We treat navigation actions differently as there is only a single NavigationModel for the UI that all navigation input feeds into.
948 // Thus, those actions should be configured with disambiguation active (i.e. Move should be a Value action and Submit and Cancel should
949 // be Button actions). This is especially important for Submit and Cancel as we get proper press and release action this way.
950 if (newValue != null && newValue.action != null && newValue.action.type != InputActionType.PassThrough && !IsNavigationAction(newValue))
951 {
952 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 "
953 + $"input from multiple pointers (action {newValue.action} is set to {newValue.action.type})", this);
954 }
955 #endif
956
957 if (newValue?.action != null && actionCallback != null && actionsHooked)
958 {
959 property.action.performed += actionCallback;
960 property.action.canceled += actionCallback;
961 }
962
963 if (isActiveAndEnabled && newValue?.action != null && (oldActionEnabled || oldActionNull))
964 EnableInputAction(property);
965 }
966
967 #if DEBUG
968 private bool IsNavigationAction(InputActionReference reference)
969 {
970 return reference == m_SubmitAction || reference == m_CancelAction || reference == m_MoveAction;
971 }
972
973 #endif
974
975 /// <summary>
976 /// An <see cref="InputAction"/> delivering a <see cref="Vector2"/> 2D screen position
977 /// used as a cursor for pointing at UI elements.
978 /// </summary>
979 /// <remarks>
980 /// The values read from this action determine <see cref="PointerEventData.position"/> and <see cref="PointerEventData.delta"/>.
981 ///
982 /// Together with <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and
983 /// <see cref="scrollWheel"/>, this forms the basis for pointer-type UI input.
984 ///
985 /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
986 /// <see cref="InputAction.expectedControlType"/> set to <c>"Vector2"</c>.
987 ///
988 /// <example>
989 /// <code>
990 /// var asset = ScriptableObject.Create<InputActionAsset>();
991 /// var map = asset.AddActionMap("UI");
992 /// var pointAction = map.AddAction("Point");
993 ///
994 /// pointAction.AddBinding("<Mouse>/position");
995 /// pointAction.AddBinding("<Touchscreen>/touch*/position");
996 ///
997 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point =
998 /// InputActionReference.Create(pointAction);
999 /// </code>
1000 /// </example>
1001 /// </remarks>
1002 /// <seealso cref="leftClick"/>
1003 /// <seealso cref="rightClick"/>
1004 /// <seealso cref="middleClick"/>
1005 /// <seealso cref="scrollWheel"/>
1006 public InputActionReference point
1007 {
1008 get => m_PointAction;
1009 set => SwapAction(ref m_PointAction, value, m_ActionsHooked, m_OnPointDelegate);
1010 }
1011
1012 /// <summary>
1013 /// An <see cref="InputAction"/> delivering a <c>Vector2</c> scroll wheel value
1014 /// used for sending <see cref="PointerEventData"/> events.
1015 /// </summary>
1016 /// <remarks>
1017 /// The values read from this action determine <see cref="PointerEventData.scrollDelta"/>.
1018 ///
1019 /// Together with <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and
1020 /// <see cref="point"/>, this forms the basis for pointer-type UI input.
1021 ///
1022 /// Note that the action is optional. A pointer is fully functional with just <see cref="point"/>
1023 /// and <see cref="leftClick"/> alone.
1024 ///
1025 /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
1026 /// <see cref="InputAction.expectedControlType"/> set to <c>"Vector2"</c>.
1027 ///
1028 /// <example>
1029 /// <code>
1030 /// var asset = ScriptableObject.Create<InputActionAsset>();
1031 /// var map = asset.AddActionMap("UI");
1032 /// var pointAction = map.AddAction("scroll");
1033 /// var scrollAction = map.AddAction("scroll");
1034 ///
1035 /// pointAction.AddBinding("<Mouse>/position");
1036 /// pointAction.AddBinding("<Touchscreen>/touch*/position");
1037 ///
1038 /// scrollAction.AddBinding("<Mouse>/scroll");
1039 ///
1040 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point =
1041 /// InputActionReference.Create(pointAction);
1042 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).scrollWheel =
1043 /// InputActionReference.Create(scrollAction);
1044 /// </code>
1045 /// </example>
1046 /// </remarks>
1047 /// <seealso cref="leftClick"/>
1048 /// <seealso cref="rightClick"/>
1049 /// <seealso cref="middleClick"/>
1050 /// <seealso cref="point"/>
1051 public InputActionReference scrollWheel
1052 {
1053 get => m_ScrollWheelAction;
1054 set => SwapAction(ref m_ScrollWheelAction, value, m_ActionsHooked, m_OnScrollWheelDelegate);
1055 }
1056
1057 /// <summary>
1058 /// An <see cref="InputAction"/> delivering a <c>float</c> button value that determines
1059 /// whether the left button of a pointer is pressed.
1060 /// </summary>
1061 /// <remarks>
1062 /// Clicks on this button will use <see cref="PointerEventData.InputButton.Left"/> for <see cref="PointerEventData.button"/>.
1063 ///
1064 /// Together with <see cref="point"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and
1065 /// <see cref="scrollWheel"/>, this forms the basis for pointer-type UI input.
1066 ///
1067 /// Note that together with <see cref="point"/>, this action is necessary for a pointer to be functional. The other clicks
1068 /// and <see cref="scrollWheel"/> are optional, however.
1069 ///
1070 /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
1071 /// <see cref="InputAction.expectedControlType"/> set to <c>"Button"</c>.
1072 ///
1073 /// <example>
1074 /// <code>
1075 /// var asset = ScriptableObject.Create<InputActionAsset>();
1076 /// var map = asset.AddActionMap("UI");
1077 /// var pointAction = map.AddAction("scroll");
1078 /// var clickAction = map.AddAction("click");
1079 ///
1080 /// pointAction.AddBinding("<Mouse>/position");
1081 /// pointAction.AddBinding("<Touchscreen>/touch*/position");
1082 ///
1083 /// clickAction.AddBinding("<Mouse>/leftButton");
1084 /// clickAction.AddBinding("<Touchscreen>/touch*/press");
1085 ///
1086 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point =
1087 /// InputActionReference.Create(pointAction);
1088 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick =
1089 /// InputActionReference.Create(clickAction);
1090 /// </code>
1091 /// </example>
1092 /// </remarks>
1093 /// <seealso cref="rightClick"/>
1094 /// <seealso cref="middleClick"/>
1095 /// <seealso cref="scrollWheel"/>
1096 /// <seealso cref="point"/>
1097 public InputActionReference leftClick
1098 {
1099 get => m_LeftClickAction;
1100 set => SwapAction(ref m_LeftClickAction, value, m_ActionsHooked, m_OnLeftClickDelegate);
1101 }
1102
1103 /// <summary>
1104 /// An <see cref="InputAction"/> delivering a <c>float</c> button value that determines
1105 /// whether the middle button of a pointer is pressed.
1106 /// </summary>
1107 /// <remarks>
1108 /// Clicks on this button will use <see cref="PointerEventData.InputButton.Middle"/> for <see cref="PointerEventData.button"/>.
1109 ///
1110 /// Together with <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="scrollWheel"/>, and
1111 /// <see cref="point"/>, this forms the basis for pointer-type UI input.
1112 ///
1113 /// Note that the action is optional. A pointer is fully functional with just <see cref="point"/>
1114 /// and <see cref="leftClick"/> alone.
1115 ///
1116 /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
1117 /// <see cref="InputAction.expectedControlType"/> set to <c>"Button"</c>.
1118 ///
1119 /// <example>
1120 /// <code>
1121 /// var asset = ScriptableObject.Create<InputActionAsset>();
1122 /// var map = asset.AddActionMap("UI");
1123 /// var pointAction = map.AddAction("scroll");
1124 /// var leftClickAction = map.AddAction("leftClick");
1125 /// var middleClickAction = map.AddAction("middleClick");
1126 ///
1127 /// pointAction.AddBinding("<Mouse>/position");
1128 /// pointAction.AddBinding("<Touchscreen>/touch*/position");
1129 ///
1130 /// leftClickAction.AddBinding("<Mouse>/leftButton");
1131 /// leftClickAction.AddBinding("<Touchscreen>/touch*/press");
1132 ///
1133 /// middleClickAction.AddBinding("<Mouse>/middleButton");
1134 ///
1135 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point =
1136 /// InputActionReference.Create(pointAction);
1137 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick =
1138 /// InputActionReference.Create(leftClickAction);
1139 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).middleClick =
1140 /// InputActionReference.Create(middleClickAction);
1141 /// </code>
1142 /// </example>
1143 /// </remarks>
1144 /// <seealso cref="leftClick"/>
1145 /// <seealso cref="rightClick"/>
1146 /// <seealso cref="scrollWheel"/>
1147 /// <seealso cref="point"/>
1148 public InputActionReference middleClick
1149 {
1150 get => m_MiddleClickAction;
1151 set => SwapAction(ref m_MiddleClickAction, value, m_ActionsHooked, m_OnMiddleClickDelegate);
1152 }
1153
1154 /// <summary>
1155 /// An <see cref="InputAction"/> delivering a <c>float"</c> button value that determines
1156 /// whether the right button of a pointer is pressed.
1157 /// </summary>
1158 /// <remarks>
1159 /// Clicks on this button will use <see cref="PointerEventData.InputButton.Right"/> for <see cref="PointerEventData.button"/>.
1160 ///
1161 /// Together with <see cref="leftClick"/>, <see cref="middleClick"/>, <see cref="scrollWheel"/>, and
1162 /// <see cref="point"/>, this forms the basis for pointer-type UI input.
1163 ///
1164 /// Note that the action is optional. A pointer is fully functional with just <see cref="point"/>
1165 /// and <see cref="leftClick"/> alone.
1166 ///
1167 /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
1168 /// <see cref="InputAction.expectedControlType"/> set to <c>"Button"</c>.
1169 ///
1170 /// <example>
1171 /// <code>
1172 /// var asset = ScriptableObject.Create<InputActionAsset>();
1173 /// var map = asset.AddActionMap("UI");
1174 /// var pointAction = map.AddAction("scroll");
1175 /// var leftClickAction = map.AddAction("leftClick");
1176 /// var rightClickAction = map.AddAction("rightClick");
1177 ///
1178 /// pointAction.AddBinding("<Mouse>/position");
1179 /// pointAction.AddBinding("<Touchscreen>/touch*/position");
1180 ///
1181 /// leftClickAction.AddBinding("<Mouse>/leftButton");
1182 /// leftClickAction.AddBinding("<Touchscreen>/touch*/press");
1183 ///
1184 /// rightClickAction.AddBinding("<Mouse>/rightButton");
1185 ///
1186 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point =
1187 /// InputActionReference.Create(pointAction);
1188 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick =
1189 /// InputActionReference.Create(leftClickAction);
1190 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).rightClick =
1191 /// InputActionReference.Create(rightClickAction);
1192 /// </code>
1193 /// </example>
1194 /// </remarks>
1195 /// <seealso cref="leftClick"/>
1196 /// <seealso cref="middleClick"/>
1197 /// <seealso cref="scrollWheel"/>
1198 /// <seealso cref="point"/>
1199 public InputActionReference rightClick
1200 {
1201 get => m_RightClickAction;
1202 set => SwapAction(ref m_RightClickAction, value, m_ActionsHooked, m_OnRightClickDelegate);
1203 }
1204
1205 /// <summary>
1206 /// An <see cref="InputAction"/> delivering a <c>Vector2</c> 2D motion vector
1207 /// used for sending <see cref="AxisEventData"/> navigation events.
1208 /// </summary>
1209 /// <remarks>
1210 /// The events generated from this input will be received by <see cref="IMoveHandler.OnMove"/>.
1211 ///
1212 /// This action together with <see cref="submit"/> and <see cref="cancel"/> form the sources for navigation-style
1213 /// UI input.
1214 ///
1215 /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
1216 /// <see cref="InputAction.expectedControlType"/> set to <c>"Vector2"</c>.
1217 ///
1218 /// <example>
1219 /// <code>
1220 /// var asset = ScriptableObject.Create<InputActionAsset>();
1221 /// var map = asset.AddActionMap("UI");
1222 /// var pointAction = map.AddAction("move");
1223 /// var submitAction = map.AddAction("submit");
1224 /// var cancelAction = map.AddAction("cancel");
1225 ///
1226 /// moveAction.AddBinding("<Gamepad>/*stick");
1227 /// moveAction.AddBinding("<Gamepad>/dpad");
1228 /// submitAction.AddBinding("<Gamepad>/buttonSouth");
1229 /// cancelAction.AddBinding("<Gamepad>/buttonEast");
1230 ///
1231 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).move =
1232 /// InputActionReference.Create(moveAction);
1233 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).submit =
1234 /// InputActionReference.Create(submitAction);
1235 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).cancelAction =
1236 /// InputActionReference.Create(cancelAction);
1237 /// </code>
1238 /// </example>
1239 /// </remarks>
1240 /// <seealso cref="submit"/>
1241 /// <seealso cref="cancel"/>
1242 public InputActionReference move
1243 {
1244 get => m_MoveAction;
1245 set => SwapAction(ref m_MoveAction, value, m_ActionsHooked, m_OnMoveDelegate);
1246 }
1247
1248 /// <summary>
1249 /// An <see cref="InputAction"/> delivering a <c>float</c> button value that determines when <c>ISubmitHandler</c>
1250 /// is triggered.
1251 /// </summary>
1252 /// <remarks>
1253 /// The events generated from this input will be received by <see cref="ISubmitHandler"/>.
1254 ///
1255 /// This action together with <see cref="move"/> and <see cref="cancel"/> form the sources for navigation-style
1256 /// UI input.
1257 ///
1258 /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.Button"/>.
1259 ///
1260 /// <example>
1261 /// <code>
1262 /// var asset = ScriptableObject.Create<InputActionAsset>();
1263 /// var map = asset.AddActionMap("UI");
1264 /// var pointAction = map.AddAction("move");
1265 /// var submitAction = map.AddAction("submit");
1266 /// var cancelAction = map.AddAction("cancel");
1267 ///
1268 /// moveAction.AddBinding("<Gamepad>/*stick");
1269 /// moveAction.AddBinding("<Gamepad>/dpad");
1270 /// submitAction.AddBinding("<Gamepad>/buttonSouth");
1271 /// cancelAction.AddBinding("<Gamepad>/buttonEast");
1272 ///
1273 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).move =
1274 /// InputActionReference.Create(moveAction);
1275 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).submit =
1276 /// InputActionReference.Create(submitAction);
1277 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).cancelAction =
1278 /// InputActionReference.Create(cancelAction);
1279 /// </code>
1280 /// </example>
1281 /// </remarks>
1282 /// <seealso cref="move"/>
1283 /// <seealso cref="cancel"/>
1284 public InputActionReference submit
1285 {
1286 get => m_SubmitAction;
1287 set => SwapAction(ref m_SubmitAction, value, m_ActionsHooked, null);
1288 }
1289
1290 /// <summary>
1291 /// An <see cref="InputAction"/> delivering a <c>float</c> button value that determines when <c>ICancelHandler</c>
1292 /// is triggered.
1293 /// </summary>
1294 /// <remarks>
1295 /// The events generated from this input will be received by <see cref="ICancelHandler"/>.
1296 ///
1297 /// This action together with <see cref="move"/> and <see cref="submit"/> form the sources for navigation-style
1298 /// UI input.
1299 ///
1300 /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.Button"/>.
1301 ///
1302 /// <example>
1303 /// <code>
1304 /// var asset = ScriptableObject.Create<InputActionAsset>();
1305 /// var map = asset.AddActionMap("UI");
1306 /// var pointAction = map.AddAction("move");
1307 /// var submitAction = map.AddAction("submit");
1308 /// var cancelAction = map.AddAction("cancel");
1309 ///
1310 /// moveAction.AddBinding("<Gamepad>/*stick");
1311 /// moveAction.AddBinding("<Gamepad>/dpad");
1312 /// submitAction.AddBinding("<Gamepad>/buttonSouth");
1313 /// cancelAction.AddBinding("<Gamepad>/buttonEast");
1314 ///
1315 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).move =
1316 /// InputActionReference.Create(moveAction);
1317 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).submit =
1318 /// InputActionReference.Create(submitAction);
1319 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).cancelAction =
1320 /// InputActionReference.Create(cancelAction);
1321 /// </code>
1322 /// </example>
1323 /// </remarks>
1324 /// <seealso cref="move"/>
1325 /// <seealso cref="submit"/>
1326 public InputActionReference cancel
1327 {
1328 get => m_CancelAction;
1329 set => SwapAction(ref m_CancelAction, value, m_ActionsHooked, null);
1330 }
1331
1332 /// <summary>
1333 /// An <see cref="InputAction"/> delivering a <c>Quaternion</c> value reflecting the orientation of <see cref="TrackedDevice"/>s.
1334 /// In combination with <see cref="trackedDevicePosition"/>, this is used to determine the transform of tracked devices from which
1335 /// to raycast into the UI scene.
1336 /// </summary>
1337 /// <remarks>
1338 /// <see cref="trackedDeviceOrientation"/> and <see cref="trackedDevicePosition"/> together replace <see cref="point"/> for
1339 /// UI input from <see cref="TrackedDevice"/>. Other than that, UI input for tracked devices is no different from "normal"
1340 /// pointer-type input. This means that <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and
1341 /// <see cref="scrollWheel"/> can all be used for tracked device input like for regular pointer input.
1342 ///
1343 /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
1344 /// <see cref="InputAction.expectedControlType"/> set to <c>"Quaternion"</c>.
1345 ///
1346 /// <example>
1347 /// <code>
1348 /// var asset = ScriptableObject.Create<InputActionAsset>();
1349 /// var map = asset.AddActionMap("UI");
1350 /// var positionAction = map.AddAction("position");
1351 /// var orientationAction = map.AddAction("orientation");
1352 /// var clickAction = map.AddAction("click");
1353 ///
1354 /// positionAction.AddBinding("<TrackedDevice>/devicePosition");
1355 /// orientationAction.AddBinding("<TrackedDevice>/deviceRotation");
1356 /// clickAction.AddBinding("<TrackedDevice>/trigger");
1357 ///
1358 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).trackedDevicePosition =
1359 /// InputActionReference.Create(positionAction);
1360 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).trackedDeviceOrientation =
1361 /// InputActionReference.Create(orientationAction);
1362 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick =
1363 /// InputActionReference.Create(clickAction);
1364 /// </code>
1365 /// </example>
1366 /// </remarks>
1367 /// <seealso cref="trackedDevicePosition"/>
1368 public InputActionReference trackedDeviceOrientation
1369 {
1370 get => m_TrackedDeviceOrientationAction;
1371 set => SwapAction(ref m_TrackedDeviceOrientationAction, value, m_ActionsHooked, m_OnTrackedDeviceOrientationDelegate);
1372 }
1373
1374 /// <summary>
1375 /// An <see cref="InputAction"/> delivering a <c>Vector3</c> value reflecting the position of <see cref="TrackedDevice"/>s.
1376 /// In combination with <see cref="trackedDeviceOrientation"/>, this is used to determine the transform of tracked devices from which
1377 /// to raycast into the UI scene.
1378 /// </summary>
1379 /// <remarks>
1380 /// <see cref="trackedDeviceOrientation"/> and <see cref="trackedDevicePosition"/> together replace <see cref="point"/> for
1381 /// UI input from <see cref="TrackedDevice"/>. Other than that, UI input for tracked devices is no different from "normal"
1382 /// pointer-type input. This means that <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and
1383 /// <see cref="scrollWheel"/> can all be used for tracked device input like for regular pointer input.
1384 ///
1385 /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its
1386 /// <see cref="InputAction.expectedControlType"/> set to <c>"Vector3"</c>.
1387 ///
1388 /// <example>
1389 /// <code>
1390 /// var asset = ScriptableObject.Create<InputActionAsset>();
1391 /// var map = asset.AddActionMap("UI");
1392 /// var positionAction = map.AddAction("position");
1393 /// var orientationAction = map.AddAction("orientation");
1394 /// var clickAction = map.AddAction("click");
1395 ///
1396 /// positionAction.AddBinding("<TrackedDevice>/devicePosition");
1397 /// orientationAction.AddBinding("<TrackedDevice>/deviceRotation");
1398 /// clickAction.AddBinding("<TrackedDevice>/trigger");
1399 ///
1400 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).trackedDevicePosition =
1401 /// InputActionReference.Create(positionAction);
1402 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).trackedDeviceOrientation =
1403 /// InputActionReference.Create(orientationAction);
1404 /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick =
1405 /// InputActionReference.Create(clickAction);
1406 /// </code>
1407 /// </example>
1408 /// </remarks>
1409 /// <seealso cref="trackedDeviceOrientation"/>
1410 public InputActionReference trackedDevicePosition
1411 {
1412 get => m_TrackedDevicePositionAction;
1413 set => SwapAction(ref m_TrackedDevicePositionAction, value, m_ActionsHooked, m_OnTrackedDevicePositionDelegate);
1414 }
1415
1416 /// <summary>
1417 /// Assigns default input actions asset and input actions, similar to how defaults are assigned when creating UI module in editor.
1418 /// Useful for creating <see cref="InputSystemUIInputModule"/> at runtime.
1419 /// </summary>
1420 /// <remarks>
1421 /// This instantiates <see cref="DefaultInputActions"/> and assigns it to <see cref="actionsAsset"/>. It also
1422 /// assigns all the various individual actions such as <see cref="point"/> and <see cref="leftClick"/>.
1423 ///
1424 /// Note that if an <c>InputSystemUIInputModule</c> component is programmatically added to a <c>GameObject</c>,
1425 /// it will automatically receive the default actions as part of its <c>OnEnable</c> method. Use <see cref="UnassignActions"/>
1426 /// to remove these assignments.
1427 ///
1428 /// <example>
1429 /// <code>
1430 /// var go = new GameObject();
1431 /// go.AddComponent<EventSystem>();
1432 ///
1433 /// // Adding the UI module like this will implicitly enable it and thus lead to
1434 /// // automatic assignment of the default input actions.
1435 /// var uiModule = go.AddComponent<InputSystemUIInputModule>();
1436 ///
1437 /// // Manually remove the default input actions.
1438 /// uiModule.UnassignActions();
1439 /// </code>
1440 /// </example>
1441 /// </remarks>
1442 /// <seealso cref="actionsAsset"/>
1443 /// <seealso cref="DefaultInputActions"/>
1444
1445 private static DefaultInputActions defaultActions;
1446
1447 public void AssignDefaultActions()
1448 {
1449 if (defaultActions == null)
1450 {
1451 defaultActions = new DefaultInputActions();
1452 }
1453 actionsAsset = defaultActions.asset;
1454 cancel = InputActionReference.Create(defaultActions.UI.Cancel);
1455 submit = InputActionReference.Create(defaultActions.UI.Submit);
1456 move = InputActionReference.Create(defaultActions.UI.Navigate);
1457 leftClick = InputActionReference.Create(defaultActions.UI.Click);
1458 rightClick = InputActionReference.Create(defaultActions.UI.RightClick);
1459 middleClick = InputActionReference.Create(defaultActions.UI.MiddleClick);
1460 point = InputActionReference.Create(defaultActions.UI.Point);
1461 scrollWheel = InputActionReference.Create(defaultActions.UI.ScrollWheel);
1462 trackedDeviceOrientation = InputActionReference.Create(defaultActions.UI.TrackedDeviceOrientation);
1463 trackedDevicePosition = InputActionReference.Create(defaultActions.UI.TrackedDevicePosition);
1464 }
1465
1466 /// <summary>
1467 /// Remove all action assignments, that is <see cref="actionsAsset"/> as well as all individual
1468 /// actions such as <see cref="leftClick"/>.
1469 /// </summary>
1470 /// <remarks>
1471 /// If the current actions were enabled by the UI input module, they will be disabled in the process.
1472 /// </remarks>
1473 /// <seealso cref="AssignDefaultActions"/>
1474 public void UnassignActions()
1475 {
1476 defaultActions?.Dispose();
1477 defaultActions = default;
1478 actionsAsset = default;
1479 cancel = default;
1480 submit = default;
1481 move = default;
1482 leftClick = default;
1483 rightClick = default;
1484 middleClick = default;
1485 point = default;
1486 scrollWheel = default;
1487 trackedDeviceOrientation = default;
1488 trackedDevicePosition = default;
1489 }
1490
1491 [Obsolete("'trackedDeviceSelect' has been obsoleted; use 'leftClick' instead.", true)]
1492 public InputActionReference trackedDeviceSelect
1493 {
1494 get => throw new InvalidOperationException();
1495 set => throw new InvalidOperationException();
1496 }
1497
1498#if UNITY_EDITOR
1499 protected override void Reset()
1500 {
1501 base.Reset();
1502
1503 var asset = (InputActionAsset)AssetDatabase.LoadAssetAtPath(
1504 UnityEngine.InputSystem.Editor.PlayerInputEditor.kDefaultInputActionsAssetPath,
1505 typeof(InputActionAsset));
1506 // Setting default asset and actions when creating via inspector
1507 Editor.InputSystemUIInputModuleEditor.ReassignActions(this, asset);
1508 }
1509
1510#endif
1511
1512 protected override void Awake()
1513 {
1514 base.Awake();
1515
1516 m_NavigationState.Reset();
1517 }
1518
1519 protected override void OnDestroy()
1520 {
1521 base.OnDestroy();
1522
1523 UnhookActions();
1524 }
1525
1526 protected override void OnEnable()
1527 {
1528 base.OnEnable();
1529
1530 if (m_OnControlsChangedDelegate == null)
1531 m_OnControlsChangedDelegate = OnControlsChanged;
1532 InputActionState.s_GlobalState.onActionControlsChanged.AddCallback(m_OnControlsChangedDelegate);
1533
1534 if (HasNoActions())
1535 AssignDefaultActions();
1536
1537 ResetPointers();
1538
1539 HookActions();
1540 EnableAllActions();
1541 }
1542
1543 protected override void OnDisable()
1544 {
1545 ResetPointers();
1546
1547 InputActionState.s_GlobalState.onActionControlsChanged.RemoveCallback(m_OnControlsChangedDelegate);
1548
1549 DisableAllActions();
1550 UnhookActions();
1551
1552 base.OnDisable();
1553 }
1554
1555 private void ResetPointers()
1556 {
1557 var numPointers = m_PointerStates.length;
1558 for (var i = 0; i < numPointers; ++i)
1559 SendPointerExitEventsAndRemovePointer(0);
1560
1561 m_CurrentPointerId = -1;
1562 m_CurrentPointerIndex = -1;
1563 m_CurrentPointerType = UIPointerType.None;
1564 }
1565
1566 private bool HasNoActions()
1567 {
1568 if (m_ActionsAsset != null)
1569 return false;
1570
1571 return m_PointAction?.action == null
1572 && m_LeftClickAction?.action == null
1573 && m_RightClickAction?.action == null
1574 && m_MiddleClickAction?.action == null
1575 && m_SubmitAction?.action == null
1576 && m_CancelAction?.action == null
1577 && m_ScrollWheelAction?.action == null
1578 && m_TrackedDeviceOrientationAction?.action == null
1579 && m_TrackedDevicePositionAction?.action == null;
1580 }
1581
1582 private void EnableAllActions()
1583 {
1584 EnableInputAction(m_PointAction);
1585 EnableInputAction(m_LeftClickAction);
1586 EnableInputAction(m_RightClickAction);
1587 EnableInputAction(m_MiddleClickAction);
1588 EnableInputAction(m_MoveAction);
1589 EnableInputAction(m_SubmitAction);
1590 EnableInputAction(m_CancelAction);
1591 EnableInputAction(m_ScrollWheelAction);
1592 EnableInputAction(m_TrackedDeviceOrientationAction);
1593 EnableInputAction(m_TrackedDevicePositionAction);
1594 }
1595
1596 private void DisableAllActions()
1597 {
1598 TryDisableInputAction(m_PointAction, true);
1599 TryDisableInputAction(m_LeftClickAction, true);
1600 TryDisableInputAction(m_RightClickAction, true);
1601 TryDisableInputAction(m_MiddleClickAction, true);
1602 TryDisableInputAction(m_MoveAction, true);
1603 TryDisableInputAction(m_SubmitAction, true);
1604 TryDisableInputAction(m_CancelAction, true);
1605 TryDisableInputAction(m_ScrollWheelAction, true);
1606 TryDisableInputAction(m_TrackedDeviceOrientationAction, true);
1607 TryDisableInputAction(m_TrackedDevicePositionAction, true);
1608 }
1609
1610 private void EnableInputAction(InputActionReference inputActionReference)
1611 {
1612 var action = inputActionReference?.action;
1613 if (action == null)
1614 return;
1615
1616 if (s_InputActionReferenceCounts.TryGetValue(action, out var referenceState))
1617 {
1618 referenceState.refCount++;
1619 s_InputActionReferenceCounts[action] = referenceState;
1620 }
1621 else
1622 {
1623 // if the action is already enabled but its reference count is zero then it was enabled by
1624 // something outside the input module and the input module should never disable it.
1625 referenceState = new InputActionReferenceState {refCount = 1, enabledByInputModule = !action.enabled};
1626 s_InputActionReferenceCounts.Add(action, referenceState);
1627 }
1628
1629 action.Enable();
1630 }
1631
1632 private void TryDisableInputAction(InputActionReference inputActionReference, bool isComponentDisabling = false)
1633 {
1634 var action = inputActionReference?.action;
1635 if (action == null)
1636 return;
1637
1638 // Don't decrement refCount when we were not responsible for incrementing it.
1639 // I.e. when we were not enabled yet. When OnDisabled is called, isActiveAndEnabled will
1640 // already have been set to false. In that case we pass isComponentDisabling to check if we
1641 // came from OnDisabled and therefore need to allow disabling.
1642 if (!isActiveAndEnabled && !isComponentDisabling)
1643 return;
1644
1645 if (!s_InputActionReferenceCounts.TryGetValue(action, out var referenceState))
1646 return;
1647
1648 if (referenceState.refCount - 1 == 0 && referenceState.enabledByInputModule)
1649 {
1650 action.Disable();
1651 s_InputActionReferenceCounts.Remove(action);
1652 return;
1653 }
1654
1655 referenceState.refCount--;
1656 s_InputActionReferenceCounts[action] = referenceState;
1657 }
1658
1659 private int GetPointerStateIndexFor(int pointerOrTouchId)
1660 {
1661 if (pointerOrTouchId == m_CurrentPointerId)
1662 return m_CurrentPointerIndex;
1663
1664 for (var i = 0; i < m_PointerIds.length; ++i)
1665 if (m_PointerIds[i] == pointerOrTouchId)
1666 return i;
1667
1668 // Search for Device or Touch Ids as a fallback
1669 for (var i = 0; i < m_PointerStates.length; ++i)
1670 {
1671 var eventData = m_PointerStates[i].eventData;
1672 if (eventData.touchId == pointerOrTouchId || (eventData.touchId != 0 && eventData.device.deviceId == pointerOrTouchId))
1673 return i;
1674 }
1675
1676 return -1;
1677 }
1678
1679 private ref PointerModel GetPointerStateForIndex(int index)
1680 {
1681 if (index == 0)
1682 return ref m_PointerStates.firstValue;
1683 return ref m_PointerStates.additionalValues[index - 1];
1684 }
1685
1686 private int GetDisplayIndexFor(InputControl control)
1687 {
1688 int displayIndex = 0;
1689 if (control.device is Pointer pointerCast)
1690 {
1691 displayIndex = pointerCast.displayIndex.ReadValue();
1692 Debug.Assert(displayIndex <= byte.MaxValue, "Display index was larger than expected");
1693 }
1694 return displayIndex;
1695 }
1696
1697 private int GetPointerStateIndexFor(ref InputAction.CallbackContext context)
1698 {
1699 if (CheckForRemovedDevice(ref context))
1700 return -1;
1701
1702 var phase = context.phase;
1703 return GetPointerStateIndexFor(context.control, createIfNotExists: phase != InputActionPhase.Canceled);
1704 }
1705
1706 // This is the key method for determining which pointer a particular input is associated with.
1707 // The principal determinant is the device that is sending the input which, in general, is expected
1708 // to be a Pointer (Mouse, Pen, Touchscreen) or TrackedDevice.
1709 //
1710 // Note, however, that the input is not guaranteed to even come from a pointer-like device. One can
1711 // bind the space key to a left click, for example. As long as we have an active pointer that can
1712 // deliver position input, we accept that setup and treat pressing the space key the same as pressing
1713 // the left button input on the respective pointer.
1714 //
1715 // Quite a lot going on in this method but we're dealing with three different UI interaction paradigms
1716 // here which we all support from a single input path and allow seamless switching between.
1717 private int GetPointerStateIndexFor(InputControl control, bool createIfNotExists = true)
1718 {
1719 Debug.Assert(control != null, "Control must not be null");
1720
1721 ////REVIEW: Any way we can cut down on the hops all over memory that we're doing here?
1722 var device = control.device;
1723
1724 ////TODO: We're repeatedly inspecting the control setup here. Do this once and only redo it if the control setup changes.
1725
1726 ////REVIEW: It seems wrong that we are picking up an input here that is *NOT* reflected in our actions. We just end
1727 //// up reading a touchId control implicitly instead of allowing actions to deliver IDs to us. On the other hand,
1728 //// making that setup explicit in actions may be quite awkward and not nearly as robust.
1729 // Determine the pointer (and touch) ID. We default the pointer ID to the device
1730 // ID of the InputDevice.
1731 var controlParent = control.parent;
1732 var touchControlIndex = m_PointerTouchControls.IndexOfReference(controlParent);
1733 if (touchControlIndex != -1)
1734 {
1735 // For touches, we cache a reference to the control of a pointer so that we don't
1736 // have to continuously do ReadValue() on the touch ID control.
1737 m_CurrentPointerId = m_PointerIds[touchControlIndex];
1738 m_CurrentPointerIndex = touchControlIndex;
1739 m_CurrentPointerType = UIPointerType.Touch;
1740
1741 return touchControlIndex;
1742 }
1743
1744 var pointerId = device.deviceId;
1745 var touchId = 0;
1746 var touchPosition = Vector2.zero;
1747
1748 // Need to check if it's a touch so that we get a correct pointerId.
1749 if (controlParent is TouchControl touchControl)
1750 {
1751 touchId = touchControl.touchId.value;
1752 touchPosition = touchControl.position.value;
1753 }
1754 // Could be it's a toplevel control on Touchscreen (like "<Touchscreen>/position"). In that case,
1755 // read the touch ID from primaryTouch.
1756 else if (controlParent is Touchscreen touchscreen)
1757 {
1758 touchId = touchscreen.primaryTouch.touchId.value;
1759 touchPosition = touchscreen.primaryTouch.position.value;
1760 }
1761
1762 int displayIndex = GetDisplayIndexFor(control);
1763
1764 if (touchId != 0)
1765 pointerId = ExtendedPointerEventData.MakePointerIdForTouch(pointerId, touchId);
1766
1767 // Early out if it's the last used pointer.
1768 // NOTE: Can't just compare by device here because of touchscreens potentially having multiple associated pointers.
1769 if (m_CurrentPointerId == pointerId)
1770 return m_CurrentPointerIndex;
1771
1772 // Search m_PointerIds for an existing entry.
1773 // NOTE: This is a linear search but m_PointerIds is only IDs and the number of concurrent pointers
1774 // should be very low at any one point (in fact, we don't generally expect to have more than one
1775 // which is why we are using InlinedArrays).
1776 if (touchId == 0) // Not necessary for touches; see above.
1777 {
1778 for (var i = 0; i < m_PointerIds.length; i++)
1779 {
1780 if (m_PointerIds[i] == pointerId)
1781 {
1782 // Existing entry found. Make it the current pointer.
1783 m_CurrentPointerId = pointerId;
1784 m_CurrentPointerIndex = i;
1785 m_CurrentPointerType = m_PointerStates[i].pointerType;
1786 return i;
1787 }
1788 }
1789 }
1790
1791 if (!createIfNotExists)
1792 return -1;
1793
1794 // Determine pointer type.
1795 var pointerType = UIPointerType.None;
1796 if (touchId != 0)
1797 pointerType = UIPointerType.Touch;
1798 else if (HaveControlForDevice(device, point))
1799 pointerType = UIPointerType.MouseOrPen;
1800 else if (HaveControlForDevice(device, trackedDevicePosition))
1801 pointerType = UIPointerType.Tracked;
1802
1803 ////REVIEW: For touch, probably makes sense to force-ignore any input other than from primaryTouch.
1804 // If the behavior is SingleUnifiedPointer, we only ever create a single pointer state
1805 // and use that for all pointer input that is coming in.
1806 if ((m_PointerBehavior == UIPointerBehavior.SingleUnifiedPointer && pointerType != UIPointerType.None) ||
1807 (m_PointerBehavior == UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack && pointerType == UIPointerType.MouseOrPen))
1808 {
1809 if (m_CurrentPointerIndex == -1)
1810 {
1811 m_CurrentPointerIndex = AllocatePointer(pointerId, displayIndex, touchId, pointerType, control, device, touchId != 0 ? controlParent : null);
1812 }
1813 else
1814 {
1815 // Update pointer record to reflect current device. We know they're different because we checked
1816 // m_CurrentPointerId earlier in the method.
1817 // NOTE: This path may repeatedly switch the pointer type and ID on the same single event instance.
1818
1819 ref var pointer = ref GetPointerStateForIndex(m_CurrentPointerIndex);
1820
1821 var eventData = pointer.eventData;
1822 eventData.control = control;
1823 eventData.device = device;
1824 eventData.pointerType = pointerType;
1825 eventData.pointerId = pointerId;
1826 eventData.touchId = touchId;
1827#if UNITY_2022_3_OR_NEWER
1828 eventData.displayIndex = displayIndex;
1829#endif
1830
1831 // Make sure these don't linger around when we switch to a different kind of pointer.
1832 eventData.trackedDeviceOrientation = default;
1833 eventData.trackedDevicePosition = default;
1834 }
1835
1836 if (pointerType == UIPointerType.Touch)
1837 GetPointerStateForIndex(m_CurrentPointerIndex).screenPosition = touchPosition;
1838
1839 m_CurrentPointerId = pointerId;
1840 m_CurrentPointerType = pointerType;
1841
1842 return m_CurrentPointerIndex;
1843 }
1844
1845 // No existing record for the device. Find out if the device has the ability to point at all.
1846 // If not, we need to use a pointer state from a different device (if present).
1847 var index = -1;
1848 if (pointerType != UIPointerType.None)
1849 {
1850 // Device has an associated position input. Create a new pointer record.
1851 index = AllocatePointer(pointerId, displayIndex, touchId, pointerType, control, device, touchId != 0 ? controlParent : null);
1852 }
1853 else
1854 {
1855 // Device has no associated position input. Find a pointer device to route the change into.
1856 // As a last resort, create a pointer without a position input.
1857
1858 // If we have a current pointer, route the input into that. The majority of times we end
1859 // up in this branch, this should settle things.
1860 if (m_CurrentPointerId != -1)
1861 return m_CurrentPointerIndex;
1862
1863 // NOTE: In most cases, we end up here when there is input on a non-pointer device bound to one of the pointer-related
1864 // actions before there is input from a pointer device. In this scenario, we don't have a pointer state allocated
1865 // for the device yet.
1866
1867 // If we have anything bound to the `point` action, create a pointer for it.
1868 var pointControls = point?.action?.controls;
1869 var pointerDevice = pointControls.HasValue && pointControls.Value.Count > 0 ? pointControls.Value[0].device : null;
1870 if (pointerDevice != null && !(pointerDevice is Touchscreen)) // Touchscreen only temporarily allocate pointer states.
1871 {
1872 // Create MouseOrPen style pointer.
1873 index = AllocatePointer(pointerDevice.deviceId, displayIndex, 0, UIPointerType.MouseOrPen, pointControls.Value[0], pointerDevice);
1874 }
1875 else
1876 {
1877 // Do the same but look at the `position` action.
1878 var positionControls = trackedDevicePosition?.action?.controls;
1879 var trackedDevice = positionControls.HasValue && positionControls.Value.Count > 0
1880 ? positionControls.Value[0].device
1881 : null;
1882 if (trackedDevice != null)
1883 {
1884 // Create a Tracked style pointer.
1885 index = AllocatePointer(trackedDevice.deviceId, displayIndex, 0, UIPointerType.Tracked, positionControls.Value[0], trackedDevice);
1886 }
1887 else
1888 {
1889 // We got input from a non-pointer device and apparently there's no pointer we can route the
1890 // input into. Just create a pointer state for the device and leave it at that.
1891 index = AllocatePointer(pointerId, displayIndex, 0, UIPointerType.None, control, device);
1892 }
1893 }
1894 }
1895
1896 if (pointerType == UIPointerType.Touch)
1897 GetPointerStateForIndex(index).screenPosition = touchPosition;
1898
1899 m_CurrentPointerId = pointerId;
1900 m_CurrentPointerIndex = index;
1901 m_CurrentPointerType = pointerType;
1902
1903 return index;
1904 }
1905
1906 private int AllocatePointer(int pointerId, int displayIndex, int touchId, UIPointerType pointerType, InputControl control, InputDevice device, InputControl touchControl = null)
1907 {
1908 // Recover event instance from previous record.
1909 var eventData = default(ExtendedPointerEventData);
1910 if (m_PointerStates.Capacity > m_PointerStates.length)
1911 {
1912 if (m_PointerStates.length == 0)
1913 eventData = m_PointerStates.firstValue.eventData;
1914 else
1915 eventData = m_PointerStates.additionalValues[m_PointerStates.length - 1].eventData;
1916 }
1917
1918 // Or allocate event.
1919 if (eventData == null)
1920 eventData = new ExtendedPointerEventData(eventSystem);
1921
1922 eventData.pointerId = pointerId;
1923#if UNITY_2022_3_OR_NEWER
1924 eventData.displayIndex = displayIndex;
1925#endif
1926 eventData.touchId = touchId;
1927 eventData.pointerType = pointerType;
1928 eventData.control = control;
1929 eventData.device = device;
1930
1931 // Allocate state.
1932 m_PointerIds.AppendWithCapacity(pointerId);
1933 m_PointerTouchControls.AppendWithCapacity(touchControl);
1934 return m_PointerStates.AppendWithCapacity(new PointerModel(eventData));
1935 }
1936
1937 private void SendPointerExitEventsAndRemovePointer(int index)
1938 {
1939 var eventData = m_PointerStates[index].eventData;
1940 if (eventData.pointerEnter != null)
1941 ProcessPointerMovement(eventData, null);
1942
1943 RemovePointerAtIndex(index);
1944 }
1945
1946 private void RemovePointerAtIndex(int index)
1947 {
1948 Debug.Assert(m_PointerStates[index].eventData.pointerEnter == null, "Pointer should have exited all objects before being removed");
1949
1950 // We don't want to release touch pointers on the same frame they are released (unpressed). They get cleaned up one frame later in Process()
1951 ref var state = ref GetPointerStateForIndex(index);
1952 if (state.pointerType == UIPointerType.Touch && (state.leftButton.isPressed || state.leftButton.wasReleasedThisFrame))
1953 {
1954 return;
1955 }
1956
1957 // Retain event data so that we can reuse the event the next time we allocate a PointerModel record.
1958 var eventData = m_PointerStates[index].eventData;
1959 Debug.Assert(eventData != null, "Pointer state should have an event instance!");
1960
1961 // Update current pointer, if necessary.
1962 if (index == m_CurrentPointerIndex)
1963 {
1964 m_CurrentPointerId = -1;
1965 m_CurrentPointerIndex = -1;
1966 m_CurrentPointerType = default;
1967 }
1968 else if (m_CurrentPointerIndex == m_PointerIds.length - 1)
1969 {
1970 // We're about to move the last entry so update the index it will
1971 // be at.
1972 m_CurrentPointerIndex = index;
1973 }
1974
1975 // Remove. Note that we may change the order of pointers here. This can save us needless copying
1976 // and m_CurrentPointerIndex should be the only index we get around for longer.
1977 m_PointerIds.RemoveAtByMovingTailWithCapacity(index);
1978 m_PointerTouchControls.RemoveAtByMovingTailWithCapacity(index);
1979 m_PointerStates.RemoveAtByMovingTailWithCapacity(index);
1980 Debug.Assert(m_PointerIds.length == m_PointerStates.length, "Pointer ID array should match state array in length");
1981
1982 // Put event instance back in place at one past last entry of array (which we know we have
1983 // as we just erased one entry). This entry will be the next one that will be used when we
1984 // allocate a new entry.
1985
1986 // Wipe the event.
1987 // NOTE: We only wipe properties here that contain reference data. The rest we rely on
1988 // the event handling code to initialize when using the event.
1989 eventData.hovered.Clear();
1990 eventData.device = null;
1991 eventData.pointerCurrentRaycast = default;
1992 eventData.pointerPressRaycast = default;
1993 eventData.pointerPress = default; // Twice to wipe lastPress, too.
1994 eventData.pointerPress = default;
1995 eventData.pointerDrag = default;
1996 eventData.pointerEnter = default;
1997 eventData.rawPointerPress = default;
1998
1999 if (m_PointerStates.length == 0)
2000 m_PointerStates.firstValue.eventData = eventData;
2001 else
2002 m_PointerStates.additionalValues[m_PointerStates.length - 1].eventData = eventData;
2003 }
2004
2005 // Remove any pointer that no longer has the ability to point.
2006 private void PurgeStalePointers()
2007 {
2008 for (var i = 0; i < m_PointerStates.length; ++i)
2009 {
2010 ref var state = ref GetPointerStateForIndex(i);
2011 var device = state.eventData.device;
2012 if (!device.added || // Check if device was removed altogether.
2013 (!HaveControlForDevice(device, point) &&
2014 !HaveControlForDevice(device, trackedDevicePosition) &&
2015 !HaveControlForDevice(device, trackedDeviceOrientation)))
2016 {
2017 SendPointerExitEventsAndRemovePointer(i);
2018 --i;
2019 }
2020 }
2021
2022 m_NeedToPurgeStalePointers = false;
2023 }
2024
2025 private static bool HaveControlForDevice(InputDevice device, InputActionReference actionReference)
2026 {
2027 var action = actionReference?.action;
2028 if (action == null)
2029 return false;
2030
2031 var controls = action.controls;
2032 for (var i = 0; i < controls.Count; ++i)
2033 if (controls[i].device == device)
2034 return true;
2035
2036 return false;
2037 }
2038
2039 // The pointer actions we unfortunately cannot poll as we may be sourcing input from multiple pointers.
2040
2041 private void OnPointCallback(InputAction.CallbackContext context)
2042 {
2043 // When a pointer is removed, there's like a non-zero coordinate on the position control and thus
2044 // we will see cancellations on the "Point" action. Ignore these as they provide no useful values
2045 // and we want to avoid doing a read of touch IDs in GetPointerStateFor() on an already removed
2046 // touchscreen.
2047 if (CheckForRemovedDevice(ref context) || context.canceled)
2048 return;
2049
2050 var index = GetPointerStateIndexFor(context.control);
2051 if (index == -1)
2052 return;
2053
2054 ref var state = ref GetPointerStateForIndex(index);
2055 state.screenPosition = context.ReadValue<Vector2>();
2056#if UNITY_2022_3_OR_NEWER
2057 state.eventData.displayIndex = GetDisplayIndexFor(context.control);
2058#endif
2059 }
2060
2061 // NOTE: In the click events, we specifically react to the Canceled phase to make sure we do NOT perform
2062 // button *clicks* when an action resets. However, we still need to send pointer ups.
2063
2064 private bool IgnoreNextClick(ref InputAction.CallbackContext context, bool wasPressed)
2065 {
2066 // If explicitly ignoring focus due to setting, never ignore clicks
2067 if (explictlyIgnoreFocus)
2068 return false;
2069 // If a currently active click is cancelled (by focus change), ignore next click if device cannot run in background.
2070 // This prevents the cancelled click event being registered when focus is returned i.e. if
2071 // the button was released while another window was focused.
2072 return context.canceled && !InputRuntime.s_Instance.isPlayerFocused && !context.control.device.canRunInBackground && wasPressed;
2073 }
2074
2075 private void OnLeftClickCallback(InputAction.CallbackContext context)
2076 {
2077 var index = GetPointerStateIndexFor(ref context);
2078 if (index == -1)
2079 return;
2080
2081 ref var state = ref GetPointerStateForIndex(index);
2082 bool wasPressed = state.leftButton.isPressed;
2083 state.leftButton.isPressed = context.ReadValueAsButton();
2084 state.changedThisFrame = true;
2085 if (IgnoreNextClick(ref context, wasPressed))
2086 state.leftButton.ignoreNextClick = true;
2087#if UNITY_2022_3_OR_NEWER
2088 state.eventData.displayIndex = GetDisplayIndexFor(context.control);
2089#endif
2090 }
2091
2092 private void OnRightClickCallback(InputAction.CallbackContext context)
2093 {
2094 var index = GetPointerStateIndexFor(ref context);
2095 if (index == -1)
2096 return;
2097
2098 ref var state = ref GetPointerStateForIndex(index);
2099 bool wasPressed = state.rightButton.isPressed;
2100 state.rightButton.isPressed = context.ReadValueAsButton();
2101 state.changedThisFrame = true;
2102 if (IgnoreNextClick(ref context, wasPressed))
2103 state.rightButton.ignoreNextClick = true;
2104#if UNITY_2022_3_OR_NEWER
2105 state.eventData.displayIndex = GetDisplayIndexFor(context.control);
2106#endif
2107 }
2108
2109 private void OnMiddleClickCallback(InputAction.CallbackContext context)
2110 {
2111 var index = GetPointerStateIndexFor(ref context);
2112 if (index == -1)
2113 return;
2114
2115 ref var state = ref GetPointerStateForIndex(index);
2116 bool wasPressed = state.middleButton.isPressed;
2117 state.middleButton.isPressed = context.ReadValueAsButton();
2118 state.changedThisFrame = true;
2119 if (IgnoreNextClick(ref context, wasPressed))
2120 state.middleButton.ignoreNextClick = true;
2121#if UNITY_2022_3_OR_NEWER
2122 state.eventData.displayIndex = GetDisplayIndexFor(context.control);
2123#endif
2124 }
2125
2126 private bool CheckForRemovedDevice(ref InputAction.CallbackContext context)
2127 {
2128 // When a device is removed, we want to simply cancel ongoing pointer
2129 // operations. Most importantly, we want to prevent GetPointerStateFor()
2130 // doing ReadValue() on touch ID controls when a touchscreen has already
2131 // been removed.
2132 if (context.canceled && !context.control.device.added)
2133 {
2134 m_NeedToPurgeStalePointers = true;
2135 return true;
2136 }
2137 return false;
2138 }
2139
2140 private void OnScrollCallback(InputAction.CallbackContext context)
2141 {
2142 var index = GetPointerStateIndexFor(ref context);
2143 if (index == -1)
2144 return;
2145
2146 ref var state = ref GetPointerStateForIndex(index);
2147
2148 var scrollDelta = context.ReadValue<Vector2>();
2149
2150 // ISXB-704: convert input value to BaseInputModule convention.
2151 state.scrollDelta = (scrollDelta / InputSystem.scrollWheelDeltaPerTick) * scrollDeltaPerTick;
2152
2153#if UNITY_2022_3_OR_NEWER
2154 state.eventData.displayIndex = GetDisplayIndexFor(context.control);
2155#endif
2156 }
2157
2158 private void OnMoveCallback(InputAction.CallbackContext context)
2159 {
2160 ////REVIEW: should we poll this? or set the action to not be pass-through? (ps4 controller is spamming this action)
2161 m_NavigationState.move = context.ReadValue<Vector2>();
2162 }
2163
2164 private void OnTrackedDeviceOrientationCallback(InputAction.CallbackContext context)
2165 {
2166 var index = GetPointerStateIndexFor(ref context);
2167 if (index == -1)
2168 return;
2169
2170 ref var state = ref GetPointerStateForIndex(index);
2171 state.worldOrientation = context.ReadValue<Quaternion>();
2172#if UNITY_2022_3_OR_NEWER
2173 state.eventData.displayIndex = GetDisplayIndexFor(context.control);
2174#endif
2175 }
2176
2177 private void OnTrackedDevicePositionCallback(InputAction.CallbackContext context)
2178 {
2179 var index = GetPointerStateIndexFor(ref context);
2180 if (index == -1)
2181 return;
2182
2183 ref var state = ref GetPointerStateForIndex(index);
2184 state.worldPosition = context.ReadValue<Vector3>();
2185#if UNITY_2022_3_OR_NEWER
2186 state.eventData.displayIndex = GetDisplayIndexFor(context.control);
2187#endif
2188 }
2189
2190 private void OnControlsChanged(object obj)
2191 {
2192 m_NeedToPurgeStalePointers = true;
2193 }
2194
2195 private void FilterPointerStatesByType()
2196 {
2197 var pointerTypeToProcess = UIPointerType.None;
2198 // Read all pointers device states
2199 // Find first pointer that has changed this frame to be processed later
2200 for (var i = 0; i < m_PointerStates.length; ++i)
2201 {
2202 ref var state = ref GetPointerStateForIndex(i);
2203 state.eventData.ReadDeviceState();
2204 state.CopyTouchOrPenStateFrom(state.eventData);
2205 if (state.changedThisFrame && pointerTypeToProcess == UIPointerType.None)
2206 pointerTypeToProcess = state.pointerType;
2207 }
2208
2209 // For SingleMouseOrPenButMultiTouchAndTrack, we keep a single pointer for mouse and pen but only for as
2210 // long as there is no touch or tracked input. If we get that kind, we remove the mouse/pen pointer.
2211 if (m_PointerBehavior == UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack && pointerTypeToProcess != UIPointerType.None)
2212 {
2213 // var pointerTypeToProcess = m_PointerStates.firstValue.pointerType;
2214 if (pointerTypeToProcess == UIPointerType.MouseOrPen)
2215 {
2216 // We have input on a mouse or pen. Kill all touch and tracked pointers we may have.
2217 for (var i = 0; i < m_PointerStates.length; ++i)
2218 {
2219 ref var state = ref GetPointerStateForIndex(i);
2220 // Touch pointers need to get forced to no longer be pressed otherwise they will not get released in subsequent frames.
2221 if (m_PointerStates[i].pointerType == UIPointerType.Touch)
2222 {
2223 state.leftButton.isPressed = false;
2224 }
2225 if (m_PointerStates[i].pointerType != UIPointerType.MouseOrPen && m_PointerStates[i].pointerType != UIPointerType.Touch || (m_PointerStates[i].pointerType == UIPointerType.Touch && !state.leftButton.isPressed && !state.leftButton.wasReleasedThisFrame))
2226 {
2227 SendPointerExitEventsAndRemovePointer(i);
2228 --i;
2229 }
2230 }
2231 }
2232 else
2233 {
2234 // We have touch or tracked input. Kill mouse/pen pointer, if we have it.
2235 for (var i = 0; i < m_PointerStates.length; ++i)
2236 {
2237 if (m_PointerStates[i].pointerType == UIPointerType.MouseOrPen)
2238 {
2239 SendPointerExitEventsAndRemovePointer(i);
2240 --i;
2241 }
2242 }
2243 }
2244 }
2245 }
2246
2247 public override void Process()
2248 {
2249 if (m_NeedToPurgeStalePointers)
2250 PurgeStalePointers();
2251
2252 // Reset devices of changes since we don't want to spool up changes once we gain focus.
2253 if (!eventSystem.isFocused && !shouldIgnoreFocus)
2254 {
2255 for (var i = 0; i < m_PointerStates.length; ++i)
2256 m_PointerStates[i].OnFrameFinished();
2257 }
2258 else
2259 {
2260 // Navigation input.
2261 ProcessNavigation(ref m_NavigationState);
2262
2263 FilterPointerStatesByType();
2264
2265 // Pointer input.
2266 for (var i = 0; i < m_PointerStates.length; i++)
2267 {
2268 ref var state = ref GetPointerStateForIndex(i);
2269
2270 ProcessPointer(ref state);
2271
2272 // If it's a touch and the touch has ended, release the pointer state.
2273 // NOTE: We defer this by one frame such that OnPointerUp happens in the frame of release
2274 // and OnPointerExit happens one frame later. This is so that IsPointerOverGameObject()
2275 // stays true for the touch in the frame of release (see UI_TouchPointersAreKeptForOneFrameAfterRelease).
2276 if (state.pointerType == UIPointerType.Touch && !state.leftButton.isPressed && !state.leftButton.wasReleasedThisFrame)
2277 {
2278 RemovePointerAtIndex(i);
2279 --i;
2280 continue;
2281 }
2282
2283 state.OnFrameFinished();
2284 }
2285 }
2286 }
2287
2288#if UNITY_2021_1_OR_NEWER
2289 public override int ConvertUIToolkitPointerId(PointerEventData sourcePointerData)
2290 {
2291 // Case 1369081: when using SingleUnifiedPointer, the same (default) pointerId should be sent to UIToolkit
2292 // regardless of pointer type or finger id.
2293 if (m_PointerBehavior == UIPointerBehavior.SingleUnifiedPointer)
2294 return UIElements.PointerId.mousePointerId;
2295
2296 return sourcePointerData is ExtendedPointerEventData ep
2297 ? ep.uiToolkitPointerId
2298 : base.ConvertUIToolkitPointerId(sourcePointerData);
2299 }
2300
2301#endif
2302
2303#if UNITY_INPUT_SYSTEM_INPUT_MODULE_SCROLL_DELTA
2304 const float kSmallestScrollDeltaPerTick = 0.00001f;
2305 public override Vector2 ConvertPointerEventScrollDeltaToTicks(Vector2 scrollDelta)
2306 {
2307 if (Mathf.Abs(scrollDeltaPerTick) < kSmallestScrollDeltaPerTick)
2308 return Vector2.zero;
2309
2310 return scrollDelta / scrollDeltaPerTick;
2311 }
2312
2313#endif
2314
2315 private void HookActions()
2316 {
2317 if (m_ActionsHooked)
2318 return;
2319
2320 if (m_OnPointDelegate == null)
2321 m_OnPointDelegate = OnPointCallback;
2322 if (m_OnLeftClickDelegate == null)
2323 m_OnLeftClickDelegate = OnLeftClickCallback;
2324 if (m_OnRightClickDelegate == null)
2325 m_OnRightClickDelegate = OnRightClickCallback;
2326 if (m_OnMiddleClickDelegate == null)
2327 m_OnMiddleClickDelegate = OnMiddleClickCallback;
2328 if (m_OnScrollWheelDelegate == null)
2329 m_OnScrollWheelDelegate = OnScrollCallback;
2330 if (m_OnMoveDelegate == null)
2331 m_OnMoveDelegate = OnMoveCallback;
2332 if (m_OnTrackedDeviceOrientationDelegate == null)
2333 m_OnTrackedDeviceOrientationDelegate = OnTrackedDeviceOrientationCallback;
2334 if (m_OnTrackedDevicePositionDelegate == null)
2335 m_OnTrackedDevicePositionDelegate = OnTrackedDevicePositionCallback;
2336
2337 SetActionCallbacks(true);
2338 }
2339
2340 private void UnhookActions()
2341 {
2342 if (!m_ActionsHooked)
2343 return;
2344
2345 SetActionCallbacks(false);
2346 }
2347
2348 private void SetActionCallbacks(bool install)
2349 {
2350 m_ActionsHooked = install;
2351 SetActionCallback(m_PointAction, m_OnPointDelegate, install);
2352 SetActionCallback(m_MoveAction, m_OnMoveDelegate, install);
2353 SetActionCallback(m_LeftClickAction, m_OnLeftClickDelegate, install);
2354 SetActionCallback(m_RightClickAction, m_OnRightClickDelegate, install);
2355 SetActionCallback(m_MiddleClickAction, m_OnMiddleClickDelegate, install);
2356 SetActionCallback(m_ScrollWheelAction, m_OnScrollWheelDelegate, install);
2357 SetActionCallback(m_TrackedDeviceOrientationAction, m_OnTrackedDeviceOrientationDelegate, install);
2358 SetActionCallback(m_TrackedDevicePositionAction, m_OnTrackedDevicePositionDelegate, install);
2359 }
2360
2361 private static void SetActionCallback(InputActionReference actionReference, Action<InputAction.CallbackContext> callback, bool install)
2362 {
2363 if (!install && callback == null)
2364 return;
2365
2366 if (actionReference == null)
2367 return;
2368
2369 var action = actionReference.action;
2370 if (action == null)
2371 return;
2372
2373 if (install)
2374 {
2375 action.performed += callback;
2376 action.canceled += callback;
2377 }
2378 else
2379 {
2380 action.performed -= callback;
2381 action.canceled -= callback;
2382 }
2383 }
2384
2385 private InputActionReference UpdateReferenceForNewAsset(InputActionReference actionReference)
2386 {
2387 var oldAction = actionReference?.action;
2388 if (oldAction == null)
2389 return null;
2390
2391 var oldActionMap = oldAction.actionMap;
2392 Debug.Assert(oldActionMap != null, "Not expected to end up with a singleton action here");
2393
2394 var newActionMap = m_ActionsAsset?.FindActionMap(oldActionMap.name);
2395 if (newActionMap == null)
2396 return null;
2397
2398 var newAction = newActionMap.FindAction(oldAction.name);
2399 if (newAction == null)
2400 return null;
2401
2402 return InputActionReference.Create(newAction);
2403 }
2404
2405 public InputActionAsset actionsAsset
2406 {
2407 get => m_ActionsAsset;
2408 set
2409 {
2410 if (value != m_ActionsAsset)
2411 {
2412 UnhookActions();
2413
2414 m_ActionsAsset = value;
2415
2416 point = UpdateReferenceForNewAsset(point);
2417 move = UpdateReferenceForNewAsset(move);
2418 leftClick = UpdateReferenceForNewAsset(leftClick);
2419 rightClick = UpdateReferenceForNewAsset(rightClick);
2420 middleClick = UpdateReferenceForNewAsset(middleClick);
2421 scrollWheel = UpdateReferenceForNewAsset(scrollWheel);
2422 submit = UpdateReferenceForNewAsset(submit);
2423 cancel = UpdateReferenceForNewAsset(cancel);
2424 trackedDeviceOrientation = UpdateReferenceForNewAsset(trackedDeviceOrientation);
2425 trackedDevicePosition = UpdateReferenceForNewAsset(trackedDevicePosition);
2426
2427 HookActions();
2428 }
2429 }
2430 }
2431
2432 [SerializeField, HideInInspector] private InputActionAsset m_ActionsAsset;
2433 [SerializeField, HideInInspector] private InputActionReference m_PointAction;
2434 [SerializeField, HideInInspector] private InputActionReference m_MoveAction;
2435 [SerializeField, HideInInspector] private InputActionReference m_SubmitAction;
2436 [SerializeField, HideInInspector] private InputActionReference m_CancelAction;
2437 [SerializeField, HideInInspector] private InputActionReference m_LeftClickAction;
2438 [SerializeField, HideInInspector] private InputActionReference m_MiddleClickAction;
2439 [SerializeField, HideInInspector] private InputActionReference m_RightClickAction;
2440 [SerializeField, HideInInspector] private InputActionReference m_ScrollWheelAction;
2441 [SerializeField, HideInInspector] private InputActionReference m_TrackedDevicePositionAction;
2442 [SerializeField, HideInInspector] private InputActionReference m_TrackedDeviceOrientationAction;
2443
2444 [SerializeField] private bool m_DeselectOnBackgroundClick = true;
2445 [SerializeField] private UIPointerBehavior m_PointerBehavior = UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack;
2446 [SerializeField, HideInInspector] internal CursorLockBehavior m_CursorLockBehavior = CursorLockBehavior.OutsideScreen;
2447
2448 // See ISXB-766 for a history of where the 6.0f value comes from
2449 // (we used to have 120 per tick on Windows and divided it by 20.)
2450 [SerializeField] private float m_ScrollDeltaPerTick = 6.0f;
2451
2452 private static Dictionary<InputAction, InputActionReferenceState> s_InputActionReferenceCounts = new Dictionary<InputAction, InputActionReferenceState>();
2453
2454 private struct InputActionReferenceState
2455 {
2456 public int refCount;
2457 public bool enabledByInputModule;
2458 }
2459
2460 [NonSerialized] private bool m_ActionsHooked;
2461 [NonSerialized] private bool m_NeedToPurgeStalePointers;
2462
2463 private Action<InputAction.CallbackContext> m_OnPointDelegate;
2464 private Action<InputAction.CallbackContext> m_OnMoveDelegate;
2465 private Action<InputAction.CallbackContext> m_OnLeftClickDelegate;
2466 private Action<InputAction.CallbackContext> m_OnRightClickDelegate;
2467 private Action<InputAction.CallbackContext> m_OnMiddleClickDelegate;
2468 private Action<InputAction.CallbackContext> m_OnScrollWheelDelegate;
2469 private Action<InputAction.CallbackContext> m_OnTrackedDevicePositionDelegate;
2470 private Action<InputAction.CallbackContext> m_OnTrackedDeviceOrientationDelegate;
2471 private Action<object> m_OnControlsChangedDelegate;
2472
2473 // Pointer-type input (also tracking-type).
2474 [NonSerialized] private int m_CurrentPointerId = -1; // Keeping track of the current pointer avoids searches in most cases.
2475 [NonSerialized] private int m_CurrentPointerIndex = -1;
2476 [NonSerialized] internal UIPointerType m_CurrentPointerType = UIPointerType.None;
2477 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).
2478 internal InlinedArray<InputControl> m_PointerTouchControls;
2479 internal InlinedArray<PointerModel> m_PointerStates;
2480
2481 // Navigation-type input.
2482 private NavigationModel m_NavigationState;
2483
2484 [NonSerialized] private GameObject m_LocalMultiPlayerRoot;
2485
2486#if UNITY_INPUT_SYSTEM_SENDPOINTERHOVERTOPARENT
2487 // Needed for testing.
2488 internal new bool sendPointerHoverToParent
2489 {
2490 get => base.sendPointerHoverToParent;
2491 set => base.sendPointerHoverToParent = value;
2492 }
2493#else
2494 private bool sendPointerHoverToParent => true;
2495#endif
2496
2497 /// <summary>
2498 /// Controls the origin point of raycasts when the cursor is locked.
2499 /// </summary>
2500 public enum CursorLockBehavior
2501 {
2502 /// <summary>
2503 /// The internal pointer position will be set to -1, -1. This short-circuits the raycasting
2504 /// logic so no objects will be intersected. This is the default setting.
2505 /// </summary>
2506 OutsideScreen,
2507
2508 /// <summary>
2509 /// Raycasts will originate from the center of the screen. This mode can be useful for
2510 /// example to check in pointer-driven FPS games if the player is looking at some world-space
2511 /// object that implements the <see cref="IPointerEnterHandler"/> and <see cref="IPointerExitHandler"/>
2512 /// interfaces.
2513 /// </summary>
2514 ScreenCenter
2515 }
2516 }
2517}
2518#endif