IndieGame/client/Packages/com.unity.inputsystem@1.7.0/InputSystem/Plugins/InputForUI/InputSystemProvider.cs

684 lines
28 KiB
C#
Raw Normal View History

2024-10-11 10:12:15 +08:00
#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<Event> m_Events = new List<Event>();
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<Vector2>();
// 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<Vector2>();
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<Vector2>();
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("<Keyboard>/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