A game about forced loneliness, made by TACStudios
at master 121 kB view raw
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&lt;InputActionAsset&gt;(); 991 /// var map = asset.AddActionMap("UI"); 992 /// var pointAction = map.AddAction("Point"); 993 /// 994 /// pointAction.AddBinding("&lt;Mouse&gt;/position"); 995 /// pointAction.AddBinding("&lt;Touchscreen&gt;/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&lt;InputActionAsset&gt;(); 1031 /// var map = asset.AddActionMap("UI"); 1032 /// var pointAction = map.AddAction("scroll"); 1033 /// var scrollAction = map.AddAction("scroll"); 1034 /// 1035 /// pointAction.AddBinding("&lt;Mouse&gt;/position"); 1036 /// pointAction.AddBinding("&lt;Touchscreen&gt;/touch*/position"); 1037 /// 1038 /// scrollAction.AddBinding("&lt;Mouse&gt;/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&lt;InputActionAsset&gt;(); 1076 /// var map = asset.AddActionMap("UI"); 1077 /// var pointAction = map.AddAction("scroll"); 1078 /// var clickAction = map.AddAction("click"); 1079 /// 1080 /// pointAction.AddBinding("&lt;Mouse&gt;/position"); 1081 /// pointAction.AddBinding("&lt;Touchscreen&gt;/touch*/position"); 1082 /// 1083 /// clickAction.AddBinding("&lt;Mouse&gt;/leftButton"); 1084 /// clickAction.AddBinding("&lt;Touchscreen&gt;/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&lt;InputActionAsset&gt;(); 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("&lt;Mouse&gt;/position"); 1128 /// pointAction.AddBinding("&lt;Touchscreen&gt;/touch*/position"); 1129 /// 1130 /// leftClickAction.AddBinding("&lt;Mouse&gt;/leftButton"); 1131 /// leftClickAction.AddBinding("&lt;Touchscreen&gt;/touch*/press"); 1132 /// 1133 /// middleClickAction.AddBinding("&lt;Mouse&gt;/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&lt;InputActionAsset&gt;(); 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("&lt;Mouse&gt;/position"); 1179 /// pointAction.AddBinding("&lt;Touchscreen&gt;/touch*/position"); 1180 /// 1181 /// leftClickAction.AddBinding("&lt;Mouse&gt;/leftButton"); 1182 /// leftClickAction.AddBinding("&lt;Touchscreen&gt;/touch*/press"); 1183 /// 1184 /// rightClickAction.AddBinding("&lt;Mouse&gt;/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&lt;InputActionAsset&gt;(); 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("&lt;Gamepad&gt;/*stick"); 1227 /// moveAction.AddBinding("&lt;Gamepad&gt;/dpad"); 1228 /// submitAction.AddBinding("&lt;Gamepad&gt;/buttonSouth"); 1229 /// cancelAction.AddBinding("&lt;Gamepad&gt;/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&lt;InputActionAsset&gt;(); 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("&lt;Gamepad&gt;/*stick"); 1269 /// moveAction.AddBinding("&lt;Gamepad&gt;/dpad"); 1270 /// submitAction.AddBinding("&lt;Gamepad&gt;/buttonSouth"); 1271 /// cancelAction.AddBinding("&lt;Gamepad&gt;/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&lt;InputActionAsset&gt;(); 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("&lt;Gamepad&gt;/*stick"); 1311 /// moveAction.AddBinding("&lt;Gamepad&gt;/dpad"); 1312 /// submitAction.AddBinding("&lt;Gamepad&gt;/buttonSouth"); 1313 /// cancelAction.AddBinding("&lt;Gamepad&gt;/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&lt;InputActionAsset&gt;(); 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("&lt;TrackedDevice&gt;/devicePosition"); 1355 /// orientationAction.AddBinding("&lt;TrackedDevice&gt;/deviceRotation"); 1356 /// clickAction.AddBinding("&lt;TrackedDevice&gt;/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&lt;InputActionAsset&gt;(); 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("&lt;TrackedDevice&gt;/devicePosition"); 1397 /// orientationAction.AddBinding("&lt;TrackedDevice&gt;/deviceRotation"); 1398 /// clickAction.AddBinding("&lt;TrackedDevice&gt;/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&lt;EventSystem&gt;(); 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&lt;InputSystemUIInputModule&gt;(); 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