using UnityEngine.EventSystems; namespace UnityEngine.UIElements { #if PACKAGE_UITOOLKIT /// /// Use this class to handle input and send events to UI Toolkit runtime panels. /// [AddComponentMenu("UI Toolkit/Panel Event Handler (UI Toolkit)")] public class PanelEventHandler : UIBehaviour, IPointerMoveHandler, IPointerUpHandler, IPointerDownHandler, ISubmitHandler, ICancelHandler, IMoveHandler, IScrollHandler, ISelectHandler, IDeselectHandler, IPointerExitHandler, IPointerEnterHandler, IRuntimePanelComponent { private BaseRuntimePanel m_Panel; /// /// The panel that this component relates to. If panel is null, this component will have no effect. /// Will be set to null automatically if panel is Disposed from an external source. /// public IPanel panel { get => m_Panel; set { var newPanel = (BaseRuntimePanel)value; if (m_Panel != newPanel) { UnregisterCallbacks(); m_Panel = newPanel; RegisterCallbacks(); } } } private GameObject selectableGameObject => m_Panel?.selectableGameObject; private EventSystem eventSystem => UIElementsRuntimeUtility.activeEventSystem as EventSystem; private readonly PointerEvent m_PointerEvent = new PointerEvent(); protected override void OnEnable() { base.OnEnable(); RegisterCallbacks(); } protected override void OnDisable() { base.OnDisable(); UnregisterCallbacks(); } void RegisterCallbacks() { if (m_Panel != null) { m_Panel.destroyed += OnPanelDestroyed; m_Panel.visualTree.RegisterCallback(OnElementFocus, TrickleDown.TrickleDown); m_Panel.visualTree.RegisterCallback(OnElementBlur, TrickleDown.TrickleDown); } } void UnregisterCallbacks() { if (m_Panel != null) { m_Panel.destroyed -= OnPanelDestroyed; m_Panel.visualTree.UnregisterCallback(OnElementFocus, TrickleDown.TrickleDown); m_Panel.visualTree.UnregisterCallback(OnElementBlur, TrickleDown.TrickleDown); } } void OnPanelDestroyed() { panel = null; } void OnElementFocus(FocusEvent e) { if (!m_Selecting && eventSystem != null) eventSystem.SetSelectedGameObject(selectableGameObject); } void OnElementBlur(BlurEvent e) { // Important: if panel discards focus entirely, it doesn't discard EventSystem selection necessarily. // Also note that if we arrive here through eventSystem.SetSelectedGameObject -> OnDeselect, // eventSystem.currentSelectedGameObject will still have its old value and we can't reaffect it immediately. } private bool m_Selecting; public void OnSelect(BaseEventData eventData) { m_Selecting = true; try { // This shouldn't conflict with EditorWindow calling Panel.Focus (only on Editor-type panels). m_Panel?.Focus(); } finally { m_Selecting = false; } } public void OnDeselect(BaseEventData eventData) { m_Panel?.Blur(); } public void OnPointerMove(PointerEventData eventData) { if (m_Panel == null || !ReadPointerData(m_PointerEvent, eventData)) return; using (var e = PointerMoveEvent.GetPooled(m_PointerEvent)) { SendEvent(e, eventData); } } public void OnPointerUp(PointerEventData eventData) { if (m_Panel == null || !ReadPointerData(m_PointerEvent, eventData, PointerEventType.Up)) return; using (var e = PointerUpEvent.GetPooled(m_PointerEvent)) { SendEvent(e, eventData); if (e.pressedButtons == 0) PointerDeviceState.SetPlayerPanelWithSoftPointerCapture(e.pointerId, null); } } public void OnPointerDown(PointerEventData eventData) { if (m_Panel == null || !ReadPointerData(m_PointerEvent, eventData, PointerEventType.Down)) return; if (eventSystem != null) eventSystem.SetSelectedGameObject(selectableGameObject); using (var e = PointerDownEvent.GetPooled(m_PointerEvent)) { SendEvent(e, eventData); PointerDeviceState.SetPlayerPanelWithSoftPointerCapture(e.pointerId, m_Panel); } } public void OnPointerExit(PointerEventData eventData) { if (m_Panel == null || !ReadPointerData(m_PointerEvent, eventData)) return; // If a pointer exit is called while the pointer is still on top of this object, it means // there's something else removing the pointer, so we might need to send a PointerCancelEvent. // This is necessary for touch pointers that are being released, because in UGUI the object // that was last hovered will not always be the one receiving the pointer up. if (eventData.pointerCurrentRaycast.gameObject == gameObject && eventData.pointerPressRaycast.gameObject != gameObject && m_PointerEvent.pointerId != PointerId.mousePointerId) { using (var e = PointerCancelEvent.GetPooled(m_PointerEvent)) { SendEvent(e, eventData); } } m_Panel.PointerLeavesPanel(m_PointerEvent.pointerId, m_PointerEvent.position); } public void OnPointerEnter(PointerEventData eventData) { if (m_Panel == null || !ReadPointerData(m_PointerEvent, eventData)) return; m_Panel.PointerEntersPanel(m_PointerEvent.pointerId, m_PointerEvent.position); } public void OnSubmit(BaseEventData eventData) { if (m_Panel == null) return; using (var e = NavigationSubmitEvent.GetPooled()) { SendEvent(e, eventData); } } public void OnCancel(BaseEventData eventData) { if (m_Panel == null) return; using (var e = NavigationCancelEvent.GetPooled()) { SendEvent(e, eventData); } } public void OnMove(AxisEventData eventData) { if (m_Panel == null) return; using (var e = NavigationMoveEvent.GetPooled(eventData.moveVector)) { SendEvent(e, eventData); } // TODO: if runtime panel has no internal navigation, switch to the next UGUI selectable element. } public void OnScroll(PointerEventData eventData) { if (m_Panel == null || !ReadPointerData(m_PointerEvent, eventData)) return; var scrollDelta = eventData.scrollDelta; scrollDelta.y = -scrollDelta.y; const float kPixelPerLine = 20; // The old input system reported scroll deltas in lines, we report pixels. // Need to scale as the UI system expects lines. scrollDelta /= kPixelPerLine; using (var e = WheelEvent.GetPooled(scrollDelta, m_PointerEvent)) { SendEvent(e, eventData); } } private void SendEvent(EventBase e, BaseEventData sourceEventData) { //e.runtimeEventData = sourceEventData; m_Panel.SendEvent(e); if (e.isPropagationStopped) sourceEventData.Use(); } private void SendEvent(EventBase e, Event sourceEvent) { m_Panel.SendEvent(e); if (e.isPropagationStopped) sourceEvent.Use(); } void Update() { if (m_Panel != null && eventSystem != null && eventSystem.currentSelectedGameObject == selectableGameObject) ProcessImguiEvents(true); } void LateUpdate() { // Empty the Event queue, look for EventModifiers. ProcessImguiEvents(false); } private Event m_Event = new Event(); private static EventModifiers s_Modifiers = EventModifiers.None; void ProcessImguiEvents(bool isSelected) { bool first = true; while (Event.PopEvent(m_Event)) { if (m_Event.type == EventType.Ignore || m_Event.type == EventType.Repaint || m_Event.type == EventType.Layout) continue; s_Modifiers = first ? m_Event.modifiers : (s_Modifiers | m_Event.modifiers); first = false; if (isSelected) { ProcessKeyboardEvent(m_Event); if (m_Event.type != EventType.Used) ProcessTabEvent(m_Event); } } } void ProcessKeyboardEvent(Event e) { if (e.type == EventType.KeyUp) { SendKeyUpEvent(e); } else if (e.type == EventType.KeyDown) { SendKeyDownEvent(e); } } // TODO: add an ITabHandler interface void ProcessTabEvent(Event e) { if (e.type == EventType.KeyDown && e.character == '\t') { SendTabEvent(e, e.shift ? -1 : 1); } } private void SendTabEvent(Event e, int direction) { using (var ev = NavigationTabEvent.GetPooled(direction)) { SendEvent(ev, e); } } private void SendKeyUpEvent(Event e) { using (var ev = KeyUpEvent.GetPooled('\0', e.keyCode, e.modifiers)) { SendEvent(ev, e); } } private void SendKeyDownEvent(Event e) { using (var ev = KeyDownEvent.GetPooled(e.character, e.keyCode, e.modifiers)) { SendEvent(ev, e); } } private bool ReadPointerData(PointerEvent pe, PointerEventData eventData, PointerEventType eventType = PointerEventType.Default) { if (eventSystem == null || eventSystem.currentInputModule == null) return false; pe.Read(this, eventData, eventType); // PointerEvents making it this far have been validated by PanelRaycaster already m_Panel.ScreenToPanel(pe.position, pe.deltaPosition, out var panelPosition, out var panelDelta, allowOutside:true); pe.SetPosition(panelPosition, panelDelta); return true; } enum PointerEventType { Default, Down, Up } class PointerEvent : IPointerEvent { public int pointerId { get; private set; } public string pointerType { get; private set; } public bool isPrimary { get; private set; } public int button { get; private set; } public int pressedButtons { get; private set; } public Vector3 position { get; private set; } public Vector3 localPosition { get; private set; } public Vector3 deltaPosition { get; private set; } public float deltaTime { get; private set; } public int clickCount { get; private set; } public float pressure { get; private set; } public float tangentialPressure { get; private set; } public float altitudeAngle { get; private set; } public float azimuthAngle { get; private set; } public float twist { get; private set; } public Vector2 radius { get; private set; } public Vector2 radiusVariance { get; private set; } public EventModifiers modifiers { get; private set; } public bool shiftKey => (modifiers & EventModifiers.Shift) != 0; public bool ctrlKey => (modifiers & EventModifiers.Control) != 0; public bool commandKey => (modifiers & EventModifiers.Command) != 0; public bool altKey => (modifiers & EventModifiers.Alt) != 0; public bool actionKey => Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer ? commandKey : ctrlKey; public void Read(PanelEventHandler self, PointerEventData eventData, PointerEventType eventType) { pointerId = self.eventSystem.currentInputModule.ConvertUIToolkitPointerId(eventData); bool InRange(int i, int start, int count) => i >= start && i < start + count; pointerType = InRange(pointerId, PointerId.touchPointerIdBase, PointerId.touchPointerCount) ? PointerType.touch : InRange(pointerId, PointerId.penPointerIdBase, PointerId.penPointerCount) ? PointerType.pen : PointerType.mouse; isPrimary = pointerId == PointerId.mousePointerId || pointerId == PointerId.touchPointerIdBase || pointerId == PointerId.penPointerIdBase; button = (int)eventData.button; clickCount = eventData.clickCount; // Flip Y axis between input and UITK var h = Screen.height; var eventPosition = Display.RelativeMouseAt(eventData.position); if (eventPosition != Vector3.zero) { // We support multiple display and display identification based on event position. int eventDisplayIndex = (int)eventPosition.z; if (eventDisplayIndex > 0 && eventDisplayIndex < Display.displays.Length) h = Display.displays[eventDisplayIndex].systemHeight; } else { eventPosition = eventData.position; } var delta = eventData.delta; eventPosition.y = h - eventPosition.y; delta.y = -delta.y; localPosition = position = eventPosition; deltaPosition = delta; deltaTime = 0; //TODO: find out what's expected here. Time since last frame? Since last sent event? pressure = eventData.pressure; tangentialPressure = eventData.tangentialPressure; altitudeAngle = eventData.altitudeAngle; azimuthAngle = eventData.azimuthAngle; twist = eventData.twist; radius = eventData.radius; radiusVariance = eventData.radiusVariance; modifiers = s_Modifiers; if (eventType == PointerEventType.Default) { button = -1; clickCount = 0; } else { button = button >= 0 ? button : 0; clickCount = Mathf.Max(1, clickCount); if (eventType == PointerEventType.Down) PointerDeviceState.PressButton(pointerId, button); else if (eventType == PointerEventType.Up) PointerDeviceState.ReleaseButton(pointerId, button); } pressedButtons = PointerDeviceState.GetPressedButtons(pointerId); } public void SetPosition(Vector3 positionOverride, Vector3 deltaOverride) { localPosition = position = positionOverride; deltaPosition = deltaOverride; } } } #endif }