#if UNITY_2023_2_OR_NEWER // UnityEngine.InputForUI Module unavailable in earlier releases using System.Collections.Generic; using Unity.IntegerTime; using UnityEngine.InputSystem.Controls; using UnityEngine.InputForUI; namespace UnityEngine.InputSystem.Plugins.InputForUI { using Event = UnityEngine.InputForUI.Event; using EventModifiers = UnityEngine.InputForUI.EventModifiers; using EventProvider = UnityEngine.InputForUI.EventProvider; internal class InputSystemProvider : IEventProviderImpl { InputEventPartialProvider m_InputEventPartialProvider; Configuration m_Cfg; InputActionAsset m_InputActionAsset; InputActionReference m_PointAction; InputActionReference m_MoveAction; InputActionReference m_SubmitAction; InputActionReference m_CancelAction; InputActionReference m_LeftClickAction; InputActionReference m_MiddleClickAction; InputActionReference m_RightClickAction; InputActionReference m_ScrollWheelAction; InputAction m_NextPreviousAction; List m_Events = new List(); PointerState m_MouseState; PointerState m_PenState; bool m_SeenPenEvents; PointerState m_TouchState; bool m_SeenTouchEvents; const float k_SmallestReportedMovementSqrDist = 0.01f; NavigationEventRepeatHelper m_RepeatHelper = new(); bool m_ResetSeenEventsOnUpdate; const float kScrollUGUIScaleFactor = 3.0f; static InputSystemProvider() { // Only if InputSystem is enabled in the PlayerSettings do we set it as the provider. // This includes situations where both InputManager and InputSystem are enabled. #if ENABLE_INPUT_SYSTEM EventProvider.SetInputSystemProvider(new InputSystemProvider()); #endif } [RuntimeInitializeOnLoadMethod(loadType: RuntimeInitializeLoadType.SubsystemRegistration)] static void Bootstrap() {} // Empty function. Exists only to invoke the static class constructor in Runtime Players EventModifiers m_EventModifiers => m_InputEventPartialProvider._eventModifiers; DiscreteTime m_CurrentTime => (DiscreteTime)Time.timeAsRational; const uint k_DefaultPlayerId = 0u; public void Initialize() { m_InputEventPartialProvider ??= new InputEventPartialProvider(); m_InputEventPartialProvider.Initialize(); m_Events.Clear(); m_MouseState.Reset(); m_PenState.Reset(); m_SeenPenEvents = false; m_TouchState.Reset(); m_SeenTouchEvents = false; // TODO should UITK somehow override this? m_Cfg = Configuration.GetDefaultConfiguration(); RegisterActions(m_Cfg); } public void Shutdown() { UnregisterActions(m_Cfg); m_InputEventPartialProvider.Shutdown(); m_InputEventPartialProvider = null; } public void Update() { #if UNITY_EDITOR // Ensure we are in a good (initialized) state before running updates. // This could be in a bad state for a duration while the build pipeline is running // when building tests to run in the Standalone Player. if (m_InputActionAsset == null) return; #endif m_InputEventPartialProvider.Update(); // Sort events added by input actions callbacks, based on type. // This is necessary to ensure that events are dispatched in the correct order. // If all events are of the PointerEvents type, sorting is based on reverse order of the EventSource enum. // Touch -> Pen -> Mouse. m_Events.Sort(SortEvents); var currentTime = (DiscreteTime)Time.timeAsRational; DirectionNavigation(currentTime); foreach (var ev in m_Events) { // We need to ignore some pointer events based on priority (Touch->Pen->Mouse) // This is mostly used to filter out simulated input, e.g. when pen is active it also generates mouse input if (m_SeenTouchEvents && ev.type == Event.Type.PointerEvent && ev.eventSource == EventSource.Pen) m_PenState.Reset(); else if ((m_SeenTouchEvents || m_SeenPenEvents) && ev.type == Event.Type.PointerEvent && (ev.eventSource == EventSource.Mouse || ev.eventSource == EventSource.Unspecified)) m_MouseState.Reset(); else EventProvider.Dispatch(ev); } // Sometimes single lower priority events can be received when using Touch or Pen, on a different frame. // To avoid dispatching them, the seen event flags aren't reset in between calls to OnPointerPerformed. // Essentially, if we're moving with Touch or Pen, lower priority events aren't dispatch as well. // Once OnClickPerformed is called, the seen flags are reset if (m_ResetSeenEventsOnUpdate) { ResetSeenEvents(); m_ResetSeenEventsOnUpdate = false; } m_Events.Clear(); } void ResetSeenEvents() { m_SeenTouchEvents = false; m_SeenPenEvents = false; } //TODO: Refactor as there is no need for having almost the same implementation in the IM and ISX? void DirectionNavigation(DiscreteTime currentTime) { var(move, axesButtonWerePressed) = ReadCurrentNavigationMoveVector(); var direction = NavigationEvent.DetermineMoveDirection(move); // Checks for next/previous directions if no movement was detected if (direction == NavigationEvent.Direction.None) { direction = ReadNextPreviousDirection(); axesButtonWerePressed = m_NextPreviousAction.WasPressedThisFrame(); } if (direction == NavigationEvent.Direction.None) { m_RepeatHelper.Reset(); } else { if (m_RepeatHelper.ShouldSendMoveEvent(currentTime, direction, axesButtonWerePressed)) { EventProvider.Dispatch(Event.From(new NavigationEvent { type = NavigationEvent.Type.Move, direction = direction, timestamp = currentTime, eventSource = GetEventSource(GetActiveDeviceFromDirection(direction)), playerId = k_DefaultPlayerId, eventModifiers = m_EventModifiers })); } } } InputDevice GetActiveDeviceFromDirection(NavigationEvent.Direction direction) { switch (direction) { case NavigationEvent.Direction.Left: case NavigationEvent.Direction.Up: case NavigationEvent.Direction.Right: case NavigationEvent.Direction.Down: return m_MoveAction.action.activeControl.device; case NavigationEvent.Direction.Next: case NavigationEvent.Direction.Previous: return m_NextPreviousAction.activeControl.device; case NavigationEvent.Direction.None: default: return Keyboard.current; } } (Vector2, bool) ReadCurrentNavigationMoveVector() { var move = m_MoveAction.action.ReadValue(); // Check if the action was "pressed" this frame to deal with repeating events var axisWasPressed = m_MoveAction.action.WasPressedThisFrame(); return (move, axisWasPressed); } NavigationEvent.Direction ReadNextPreviousDirection() { if (m_NextPreviousAction.IsPressed()) { //TODO: For now it only deals with Keyboard, needs to deal with other devices if we can add bindings // for Gamepad, etc //TODO: An alternative could be to have an action for next and for previous since shortcut support does // not work properly if (m_NextPreviousAction.activeControl.device is Keyboard) { var keyboard = m_NextPreviousAction.activeControl.device as Keyboard; // Return direction based on whether shift is pressed or not return keyboard.shiftKey.isPressed ? NavigationEvent.Direction.Previous : NavigationEvent.Direction.Next; } } return NavigationEvent.Direction.None; } static int SortEvents(Event a, Event b) { return Event.CompareType(a, b); } public void OnFocusChanged(bool focus) { m_InputEventPartialProvider.OnFocusChanged(focus); } public bool RequestCurrentState(Event.Type type) { if (m_InputEventPartialProvider.RequestCurrentState(type)) return true; switch (type) { case Event.Type.PointerEvent: { if (m_TouchState.LastPositionValid) EventProvider.Dispatch(Event.From(ToPointerStateEvent(m_CurrentTime, m_TouchState, EventSource.Touch))); if (m_PenState.LastPositionValid) EventProvider.Dispatch(Event.From(ToPointerStateEvent(m_CurrentTime, m_PenState, EventSource.Pen))); if (m_MouseState.LastPositionValid) EventProvider.Dispatch(Event.From(ToPointerStateEvent(m_CurrentTime, m_MouseState, EventSource.Mouse))); else { // TODO maybe it's reasonable to poll and dispatch mouse state here anyway? } return m_TouchState.LastPositionValid || m_PenState.LastPositionValid || m_MouseState.LastPositionValid; } // TODO case Event.Type.IMECompositionEvent: default: return false; } } public uint playerCount => 1; // TODO // copied from UIElementsRuntimeUtility.cs static Vector2 ScreenBottomLeftToPanelPosition(Vector2 position, int targetDisplay) { // Flip positions Y axis between input and UITK var screenHeight = Screen.height; if (targetDisplay > 0 && targetDisplay < Display.displays.Length) screenHeight = Display.displays[targetDisplay].systemHeight; position.y = screenHeight - position.y; return position; } PointerEvent ToPointerStateEvent(DiscreteTime currentTime, in PointerState state, EventSource eventSource) { return new PointerEvent { type = PointerEvent.Type.State, pointerIndex = 0, position = state.LastPosition, deltaPosition = Vector2.zero, scroll = Vector2.zero, displayIndex = state.LastDisplayIndex, // TODO // tilt = eventSource == EventSource.Pen ? _lastPenData.tilt : Vector2.zero, // twist = eventSource == EventSource.Pen ? _lastPenData.twist : 0.0f, // pressure = eventSource == EventSource.Pen ? _lastPenData.pressure : 0.0f, // isInverted = eventSource == EventSource.Pen && ((_lastPenData.penStatus & PenStatus.Inverted) != 0), button = 0, buttonsState = state.ButtonsState, clickCount = 0, timestamp = currentTime, eventSource = eventSource, playerId = k_DefaultPlayerId, eventModifiers = m_EventModifiers }; } EventSource GetEventSource(InputAction.CallbackContext ctx) { var device = ctx.control.device; return GetEventSource(device); } EventSource GetEventSource(InputDevice device) { if (device is Touchscreen) return EventSource.Touch; if (device is Pen) return EventSource.Pen; if (device is Mouse) return EventSource.Mouse; if (device is Keyboard) return EventSource.Keyboard; if (device is Gamepad) return EventSource.Gamepad; return EventSource.Unspecified; } ref PointerState GetPointerStateForSource(EventSource eventSource) { switch (eventSource) { case EventSource.Touch: return ref m_TouchState; case EventSource.Pen: return ref m_PenState; default: return ref m_MouseState; } } void DispatchFromCallback(in Event ev) { m_Events.Add(ev); } static int FindTouchFingerIndex(Touchscreen touchscreen, InputAction.CallbackContext ctx) { if (touchscreen == null) return 0; var asVector2Control = ctx.control is Vector2Control ? (Vector2Control)ctx.control : null; var asTouchPressControl = ctx.control is TouchPressControl ? (TouchPressControl)ctx.control : null; var asTouchControl = ctx.control is TouchControl ? (TouchControl)ctx.control : null; // Finds the index of the matching control type in the Touchscreen device lost of touch controls (touches) for (var i = 0; i < touchscreen.touches.Count; ++i) { if (asVector2Control != null && asVector2Control == touchscreen.touches[i].position) return i; if (asTouchPressControl != null && asTouchPressControl == touchscreen.touches[i].press) return i; if (asTouchControl != null && asTouchControl == touchscreen.touches[i]) return i; } return 0; } void OnPointerPerformed(InputAction.CallbackContext ctx) { var eventSource = GetEventSource(ctx); ref var pointerState = ref GetPointerStateForSource(eventSource); // Overall I'm not happy how leaky this is, we're using input actions to have flexibility to bind to different controls, // but instead we just kinda abuse it to bind to different devices ... var asPointerDevice = ctx.control.device is Pointer ? (Pointer)ctx.control.device : null; var asPenDevice = ctx.control.device is Pen ? (Pen)ctx.control.device : null; var asTouchscreenDevice = ctx.control.device is Touchscreen ? (Touchscreen)ctx.control.device : null; var asTouchControl = ctx.control is TouchControl ? (TouchControl)ctx.control : null; var pointerIndex = FindTouchFingerIndex(asTouchscreenDevice, ctx); m_ResetSeenEventsOnUpdate = false; if (asTouchControl != null || asTouchscreenDevice != null) m_SeenTouchEvents = true; else if (asPenDevice != null) m_SeenPenEvents = true; var positionISX = ctx.ReadValue(); var targetDisplay = asPointerDevice != null ? asPointerDevice.displayIndex.ReadValue() : (asTouchscreenDevice != null ? asTouchscreenDevice.displayIndex.ReadValue() : 0); var position = ScreenBottomLeftToPanelPosition(positionISX, targetDisplay); var delta = pointerState.LastPositionValid ? position - pointerState.LastPosition : Vector2.zero; var tilt = asPenDevice != null ? asPenDevice.tilt.ReadValue() : Vector2.zero; var twist = asPenDevice != null ? asPenDevice.twist.ReadValue() : 0.0f; var pressure = asPenDevice != null ? asPenDevice.pressure.ReadValue() : (asTouchControl != null ? asTouchControl.pressure.ReadValue() : 0.0f); var isInverted = asPenDevice != null ? asPenDevice.eraser.isPressed : false; // TODO any way to detect that pen is inverted but not touching? if (delta.sqrMagnitude >= k_SmallestReportedMovementSqrDist) { DispatchFromCallback(Event.From(new PointerEvent { type = PointerEvent.Type.PointerMoved, pointerIndex = pointerIndex, position = position, deltaPosition = delta, scroll = Vector2.zero, displayIndex = targetDisplay, tilt = tilt, twist = twist, pressure = pressure, isInverted = isInverted, button = 0, buttonsState = pointerState.ButtonsState, clickCount = 0, timestamp = m_CurrentTime, eventSource = eventSource, playerId = k_DefaultPlayerId, eventModifiers = m_EventModifiers })); // only record if we send an event pointerState.OnMove(m_CurrentTime, position, targetDisplay); } else if (!pointerState.LastPositionValid) pointerState.OnMove(m_CurrentTime, position, targetDisplay); } void OnSubmitPerformed(InputAction.CallbackContext ctx) { DispatchFromCallback(Event.From(new NavigationEvent { type = NavigationEvent.Type.Submit, direction = NavigationEvent.Direction.None, timestamp = m_CurrentTime, eventSource = GetEventSource(ctx), playerId = k_DefaultPlayerId, eventModifiers = m_EventModifiers })); } void OnCancelPerformed(InputAction.CallbackContext ctx) { DispatchFromCallback(Event.From(new NavigationEvent { type = NavigationEvent.Type.Cancel, direction = NavigationEvent.Direction.None, timestamp = m_CurrentTime, eventSource = GetEventSource(ctx), playerId = k_DefaultPlayerId, eventModifiers = m_EventModifiers })); } void OnClickPerformed(InputAction.CallbackContext ctx, EventSource eventSource, PointerEvent.Button button) { ref var state = ref GetPointerStateForSource(eventSource); var asTouchscreenDevice = ctx.control.device is Touchscreen ? (Touchscreen)ctx.control.device : null; var asTouchControl = ctx.control is TouchControl ? (TouchControl)ctx.control : null; var pointerIndex = FindTouchFingerIndex(asTouchscreenDevice, ctx); m_ResetSeenEventsOnUpdate = true; if (asTouchControl != null || asTouchscreenDevice != null) m_SeenTouchEvents = true; var wasPressed = state.ButtonsState.Get(button); var isPressed = ctx.ReadValueAsButton(); state.OnButtonChange(m_CurrentTime, button, wasPressed, isPressed); DispatchFromCallback(Event.From(new PointerEvent { type = isPressed ? PointerEvent.Type.ButtonPressed : PointerEvent.Type.ButtonReleased, pointerIndex = pointerIndex, position = state.LastPosition, deltaPosition = Vector2.zero, scroll = Vector2.zero, displayIndex = state.LastDisplayIndex, tilt = Vector2.zero, twist = 0.0f, pressure = 0.0f, isInverted = false, button = button, buttonsState = state.ButtonsState, clickCount = state.ClickCount, timestamp = m_CurrentTime, eventSource = eventSource, playerId = k_DefaultPlayerId, eventModifiers = m_EventModifiers })); } void OnLeftClickPerformed(InputAction.CallbackContext ctx) => OnClickPerformed(ctx, GetEventSource(ctx), PointerEvent.Button.MouseLeft); void OnMiddleClickPerformed(InputAction.CallbackContext ctx) => OnClickPerformed(ctx, GetEventSource(ctx), PointerEvent.Button.MouseMiddle); void OnRightClickPerformed(InputAction.CallbackContext ctx) => OnClickPerformed(ctx, GetEventSource(ctx), PointerEvent.Button.MouseRight); void OnScrollWheelPerformed(InputAction.CallbackContext ctx) { var scrollDelta = ctx.ReadValue(); if (scrollDelta.sqrMagnitude < k_SmallestReportedMovementSqrDist) return; var eventSource = GetEventSource(ctx); ref var state = ref GetPointerStateForSource(eventSource); var position = Vector2.zero; var targetDisplay = 0; if (state.LastPositionValid) { position = state.LastPosition; targetDisplay = state.LastDisplayIndex; } else if (eventSource == EventSource.Mouse && Mouse.current != null) { position = Mouse.current.position.ReadValue(); targetDisplay = Mouse.current.displayIndex.ReadValue(); } // Make it look similar to IMGUI event scroll values. scrollDelta.x *= kScrollUGUIScaleFactor; scrollDelta.y *= -kScrollUGUIScaleFactor; DispatchFromCallback(Event.From(new PointerEvent { type = PointerEvent.Type.Scroll, pointerIndex = 0, position = position, deltaPosition = Vector2.zero, scroll = scrollDelta, displayIndex = targetDisplay, tilt = Vector2.zero, twist = 0.0f, pressure = 0.0f, isInverted = false, button = 0, buttonsState = state.ButtonsState, clickCount = 0, timestamp = m_CurrentTime, eventSource = EventSource.Mouse, playerId = k_DefaultPlayerId, eventModifiers = m_EventModifiers })); } void RegisterNextPreviousAction() { m_NextPreviousAction = new InputAction(name: "nextPreviousAction", type: InputActionType.Button); // TODO add more default bindings, or make them configurable m_NextPreviousAction.AddBinding("/tab"); m_NextPreviousAction.Enable(); } void UnregisterNextPreviousAction() { if (m_NextPreviousAction != null) { m_NextPreviousAction.Disable(); m_NextPreviousAction = null; } } void RegisterActions(Configuration cfg) { m_InputActionAsset = InputActionAsset.FromJson(cfg.InputActionAssetAsJson); m_PointAction = InputActionReference.Create(m_InputActionAsset.FindAction(m_Cfg.PointAction)); m_MoveAction = InputActionReference.Create(m_InputActionAsset.FindAction(m_Cfg.MoveAction)); m_SubmitAction = InputActionReference.Create(m_InputActionAsset.FindAction(m_Cfg.SubmitAction)); m_CancelAction = InputActionReference.Create(m_InputActionAsset.FindAction(m_Cfg.CancelAction)); m_LeftClickAction = InputActionReference.Create(m_InputActionAsset.FindAction(m_Cfg.LeftClickAction)); m_MiddleClickAction = InputActionReference.Create(m_InputActionAsset.FindAction(m_Cfg.MiddleClickAction)); m_RightClickAction = InputActionReference.Create(m_InputActionAsset.FindAction(m_Cfg.RightClickAction)); m_ScrollWheelAction = InputActionReference.Create(m_InputActionAsset.FindAction(m_Cfg.ScrollWheelAction)); if (m_PointAction.action != null) m_PointAction.action.performed += OnPointerPerformed; if (m_SubmitAction.action != null) m_SubmitAction.action.performed += OnSubmitPerformed; if (m_CancelAction.action != null) m_CancelAction.action.performed += OnCancelPerformed; if (m_LeftClickAction.action != null) m_LeftClickAction.action.performed += OnLeftClickPerformed; if (m_MiddleClickAction.action != null) m_MiddleClickAction.action.performed += OnMiddleClickPerformed; if (m_RightClickAction.action != null) m_RightClickAction.action.performed += OnRightClickPerformed; if (m_ScrollWheelAction.action != null) m_ScrollWheelAction.action.performed += OnScrollWheelPerformed; // When adding new one's don't forget to add them to UnregisterActions m_InputActionAsset.Enable(); // TODO make it configurable as it is not part of default config // The Next/Previous action is not part of the input actions asset RegisterNextPreviousAction(); } void UnregisterActions(Configuration cfg) { if (m_PointAction.action != null) m_PointAction.action.performed -= OnPointerPerformed; if (m_SubmitAction.action != null) m_SubmitAction.action.performed -= OnSubmitPerformed; if (m_CancelAction.action != null) m_CancelAction.action.performed -= OnCancelPerformed; if (m_LeftClickAction.action != null) m_LeftClickAction.action.performed -= OnLeftClickPerformed; if (m_MiddleClickAction.action != null) m_MiddleClickAction.action.performed -= OnMiddleClickPerformed; if (m_RightClickAction.action != null) m_RightClickAction.action.performed -= OnRightClickPerformed; if (m_ScrollWheelAction.action != null) m_ScrollWheelAction.action.performed -= OnScrollWheelPerformed; m_PointAction = null; m_MoveAction = null; m_SubmitAction = null; m_CancelAction = null; m_LeftClickAction = null; m_MiddleClickAction = null; m_RightClickAction = null; m_ScrollWheelAction = null; m_InputActionAsset.Disable(); // The Next/Previous action is not part of the input actions asset UnregisterNextPreviousAction(); UnityEngine.Object.Destroy(m_InputActionAsset); // TODO check if this is ok } public struct Configuration { public string InputActionAssetAsJson; public string PointAction; public string MoveAction; public string SubmitAction; public string CancelAction; public string LeftClickAction; public string MiddleClickAction; public string RightClickAction; public string ScrollWheelAction; public static Configuration GetDefaultConfiguration() { // TODO this is a weird way of doing that, is there an easier way? var asset = new DefaultInputActions(); var json = asset.asset.ToJson(); UnityEngine.Object.DestroyImmediate(asset.asset); // TODO just Dispose doesn't work in edit mode return new Configuration { InputActionAssetAsJson = json, PointAction = "UI/Point", MoveAction = "UI/Navigate", SubmitAction = "UI/Submit", CancelAction = "UI/Cancel", LeftClickAction = "UI/Click", MiddleClickAction = "UI/MiddleClick", RightClickAction = "UI/RightClick", ScrollWheelAction = "UI/ScrollWheel", }; } } } } #endif // UNITY_2023_2_OR_NEWER