A game about forced loneliness, made by TACStudios
1using UnityEngine.EventSystems;
2using UnityEngine.UI;
3
4namespace UnityEngine.UIElements
5{
6 // This code is disabled unless the UI Toolkit package or the com.unity.modules.uielements module are present.
7 // The UIElements module is always present in the Editor but it can be stripped from a project build if unused.
8#if PACKAGE_UITOOLKIT
9 /// <summary>
10 /// Use this class to handle input and send events to UI Toolkit runtime panels.
11 /// </summary>
12 [AddComponentMenu("UI Toolkit/Panel Event Handler (UI Toolkit)")]
13 public class PanelEventHandler : UIBehaviour, IPointerMoveHandler, IPointerUpHandler, IPointerDownHandler,
14 ISubmitHandler, ICancelHandler, IMoveHandler, IScrollHandler, ISelectHandler, IDeselectHandler,
15 IPointerExitHandler, IPointerEnterHandler, IRuntimePanelComponent, IPointerClickHandler
16 {
17 private BaseRuntimePanel m_Panel;
18
19 /// <summary>
20 /// The panel that this component relates to. If panel is null, this component will have no effect.
21 /// Will be set to null automatically if panel is Disposed from an external source.
22 /// </summary>
23 public IPanel panel
24 {
25 get => m_Panel;
26 set
27 {
28 var newPanel = (BaseRuntimePanel)value;
29 if (m_Panel != newPanel)
30 {
31 UnregisterCallbacks();
32 m_Panel = newPanel;
33 RegisterCallbacks();
34 }
35 }
36 }
37
38 private GameObject selectableGameObject => m_Panel?.selectableGameObject;
39 private EventSystem eventSystem => UIElementsRuntimeUtility.activeEventSystem as EventSystem;
40
41 private bool isCurrentFocusedPanel => m_Panel != null && eventSystem != null &&
42 eventSystem.currentSelectedGameObject == selectableGameObject;
43
44 private Focusable currentFocusedElement => m_Panel?.focusController.GetLeafFocusedElement();
45
46 private readonly PointerEvent m_PointerEvent = new PointerEvent();
47
48 private float m_LastClickTime = 0;
49
50 protected override void OnEnable()
51 {
52 base.OnEnable();
53 RegisterCallbacks();
54 }
55
56 protected override void OnDisable()
57 {
58 base.OnDisable();
59 UnregisterCallbacks();
60 }
61
62 void RegisterCallbacks()
63 {
64 if (m_Panel != null)
65 {
66 m_Panel.destroyed += OnPanelDestroyed;
67 m_Panel.visualTree.RegisterCallback<FocusEvent>(OnElementFocus, TrickleDown.TrickleDown);
68 m_Panel.visualTree.RegisterCallback<BlurEvent>(OnElementBlur, TrickleDown.TrickleDown);
69 }
70 }
71
72 void UnregisterCallbacks()
73 {
74 if (m_Panel != null)
75 {
76 m_Panel.destroyed -= OnPanelDestroyed;
77 m_Panel.visualTree.UnregisterCallback<FocusEvent>(OnElementFocus, TrickleDown.TrickleDown);
78 m_Panel.visualTree.UnregisterCallback<BlurEvent>(OnElementBlur, TrickleDown.TrickleDown);
79 }
80 }
81
82 void OnPanelDestroyed()
83 {
84 panel = null;
85 }
86
87 void OnElementFocus(FocusEvent e)
88 {
89 if (!m_Selecting && eventSystem != null)
90 eventSystem.SetSelectedGameObject(selectableGameObject);
91 }
92
93 void OnElementBlur(BlurEvent e)
94 {
95 // Important: if panel discards focus entirely, it doesn't discard EventSystem selection necessarily.
96 // Also note that if we arrive here through eventSystem.SetSelectedGameObject -> OnDeselect,
97 // eventSystem.currentSelectedGameObject will still have its old value and we can't reaffect it immediately.
98 }
99
100 private bool m_Selecting;
101 public void OnSelect(BaseEventData eventData)
102 {
103 m_Selecting = true;
104 try
105 {
106 // This shouldn't conflict with EditorWindow calling Panel.Focus (only on Editor-type panels).
107 m_Panel?.Focus();
108 }
109 finally
110 {
111 m_Selecting = false;
112 }
113 }
114
115 public void OnDeselect(BaseEventData eventData)
116 {
117 m_Panel?.Blur();
118 }
119
120 public void OnPointerMove(PointerEventData eventData)
121 {
122 if (m_Panel == null || !ReadPointerData(m_PointerEvent, eventData))
123 return;
124
125 using (var e = PointerMoveEvent.GetPooled(m_PointerEvent))
126 {
127 SendEvent(e, eventData);
128 }
129 }
130
131 public void OnPointerUp(PointerEventData eventData)
132 {
133 if (m_Panel == null || !ReadPointerData(m_PointerEvent, eventData, PointerEventType.Up))
134 return;
135
136 using (var e = PointerUpEvent.GetPooled(m_PointerEvent))
137 {
138 SendEvent(e, eventData);
139
140 if (e.pressedButtons == 0)
141 PointerDeviceState.SetPlayerPanelWithSoftPointerCapture(e.pointerId, null);
142 }
143 }
144
145 public void OnPointerDown(PointerEventData eventData)
146 {
147 if (m_Panel == null || !ReadPointerData(m_PointerEvent, eventData, PointerEventType.Down))
148 return;
149
150 if (eventSystem != null)
151 eventSystem.SetSelectedGameObject(selectableGameObject);
152
153 using (var e = PointerDownEvent.GetPooled(m_PointerEvent))
154 {
155 SendEvent(e, eventData);
156
157 PointerDeviceState.SetPlayerPanelWithSoftPointerCapture(e.pointerId, m_Panel);
158 }
159 }
160
161 public void OnPointerExit(PointerEventData eventData)
162 {
163 if (m_Panel == null || !ReadPointerData(m_PointerEvent, eventData))
164 return;
165
166 // If a pointer exit is called while the pointer is still on top of this object, it means
167 // there's something else removing the pointer, so we might need to send a PointerCancelEvent.
168 // This is necessary for touch pointers that are being released, because in UGUI the object
169 // that was last hovered will not always be the one receiving the pointer up.
170 if (eventData.pointerCurrentRaycast.gameObject == gameObject &&
171 eventData.pointerPressRaycast.gameObject != gameObject &&
172 m_PointerEvent.pointerId != PointerId.mousePointerId)
173 {
174 using (var e = PointerCancelEvent.GetPooled(m_PointerEvent))
175 {
176 SendEvent(e, eventData);
177
178 if (e.pressedButtons == 0)
179 PointerDeviceState.SetPlayerPanelWithSoftPointerCapture(e.pointerId, null);
180 }
181 }
182
183 m_Panel.PointerLeavesPanel(m_PointerEvent.pointerId, m_PointerEvent.position);
184 }
185
186 public void OnPointerEnter(PointerEventData eventData)
187 {
188 if (m_Panel == null || !ReadPointerData(m_PointerEvent, eventData))
189 return;
190
191 m_Panel.PointerEntersPanel(m_PointerEvent.pointerId, m_PointerEvent.position);
192 }
193
194 public void OnPointerClick(PointerEventData eventData)
195 {
196 m_LastClickTime = Time.unscaledTime;
197 }
198
199 public void OnSubmit(BaseEventData eventData)
200 {
201 if (m_Panel == null)
202 return;
203
204 // Allow KeyDown/KeyUp events to be processed before navigation events.
205 var target = currentFocusedElement ?? m_Panel.visualTree;
206 ProcessImguiEvents(target);
207
208 using (var e = NavigationSubmitEvent.GetPooled(s_Modifiers))
209 {
210 e.target = target;
211 SendEvent(e, eventData);
212 }
213 }
214
215 public void OnCancel(BaseEventData eventData)
216 {
217 if (m_Panel == null)
218 return;
219
220 // Allow KeyDown/KeyUp events to be processed before navigation events.
221 var target = currentFocusedElement ?? m_Panel.visualTree;
222 ProcessImguiEvents(target);
223
224 using (var e = NavigationCancelEvent.GetPooled(s_Modifiers))
225 {
226 e.target = target;
227 SendEvent(e, eventData);
228 }
229 }
230
231 public void OnMove(AxisEventData eventData)
232 {
233 if (m_Panel == null)
234 return;
235
236 // Allow KeyDown/KeyUp events to be processed before navigation events.
237 var target = currentFocusedElement ?? m_Panel.visualTree;
238 ProcessImguiEvents(target);
239
240 using (var e = NavigationMoveEvent.GetPooled(eventData.moveVector, s_Modifiers))
241 {
242 e.target = target;
243 SendEvent(e, eventData);
244 }
245
246 // TODO: if runtime panel has no internal navigation, switch to the next UGUI selectable element.
247 }
248
249 public void OnScroll(PointerEventData eventData)
250 {
251 if (m_Panel == null || !ReadPointerData(m_PointerEvent, eventData))
252 return;
253
254 var uguiScrollDelta = eventData.scrollDelta;
255 var scrollTicks = eventSystem.currentInputModule.ConvertPointerEventScrollDeltaToTicks(uguiScrollDelta);
256
257 // ISXB-808: Scale scrollDelta to match the UIToolkit convention.
258 var uitkScrollDelta = scrollTicks * WheelEvent.scrollDeltaPerTick;
259 uitkScrollDelta.y = -uitkScrollDelta.y;
260
261 using (var e = WheelEvent.GetPooled(uitkScrollDelta, m_PointerEvent))
262 {
263 SendEvent(e, eventData);
264 }
265 }
266
267 private void SendEvent(EventBase e, BaseEventData sourceEventData)
268 {
269 //e.runtimeEventData = sourceEventData;
270 m_Panel.SendEvent(e);
271 if (e.isPropagationStopped)
272 sourceEventData.Use();
273 }
274
275 private void SendEvent(EventBase e, Event sourceEvent)
276 {
277 m_Panel.SendEvent(e);
278
279 // Don't call sourceEvent.Use() because DefaultEventSystem doesn't call it either
280 // and we want to have the same behavior as much as possible.
281 // See UGUIEventSystemTests.KeyDownStoppedDoesntPreventNavigationEvents for a test requires this.
282 }
283
284 internal void Update()
285 {
286 if (isCurrentFocusedPanel)
287 ProcessImguiEvents(currentFocusedElement ?? m_Panel.visualTree);
288 }
289
290 void LateUpdate()
291 {
292 // Empty the Event queue, look for EventModifiers.
293 ProcessImguiEvents(null);
294 }
295
296 private Event m_Event = new Event();
297 private static EventModifiers s_Modifiers = EventModifiers.None;
298
299 // Send IMGUI events to given focus-based target, if any, or simply flush the event queue if not.
300 // For uniformity of composite events (keyDown vs navigation), target should remain the same
301 // throughout the entire processing cycle.
302 void ProcessImguiEvents(Focusable target)
303 {
304 bool first = true;
305
306 while (Event.PopEvent(m_Event))
307 {
308 if (m_Event.type == EventType.Ignore || m_Event.type == EventType.Repaint ||
309 m_Event.type == EventType.Layout)
310 continue;
311
312 s_Modifiers = first ? m_Event.modifiers : (s_Modifiers | m_Event.modifiers);
313 first = false;
314
315 if (target != null)
316 {
317 ProcessKeyboardEvent(m_Event, target);
318 if (eventSystem.sendNavigationEvents)
319 ProcessTabEvent(m_Event, target);
320 }
321 }
322 }
323
324 void ProcessKeyboardEvent(Event e, Focusable target)
325 {
326 if (e.type == EventType.KeyUp)
327 {
328 SendKeyUpEvent(e, target);
329 }
330 else if (e.type == EventType.KeyDown)
331 {
332 SendKeyDownEvent(e, target);
333 }
334 }
335
336 // TODO: add an ITabHandler interface
337 void ProcessTabEvent(Event e, Focusable target)
338 {
339 if (e.ShouldSendNavigationMoveEventRuntime())
340 {
341 SendTabEvent(e, e.shift ? NavigationMoveEvent.Direction.Previous : NavigationMoveEvent.Direction.Next, target);
342 }
343 }
344
345 private void SendTabEvent(Event e, NavigationMoveEvent.Direction direction, Focusable target)
346 {
347 using (var ev = NavigationMoveEvent.GetPooled(direction, s_Modifiers))
348 {
349 ev.target = target;
350 SendEvent(ev, e);
351 }
352 }
353
354 private void SendKeyUpEvent(Event e, Focusable target)
355 {
356 // Use UIElementsRuntimeUtility.CreateEvent because DefaultEventSystem uses it too
357 // and we want to have the same behavior as much as possible.
358 using (var ev = (KeyUpEvent) UIElementsRuntimeUtility.CreateEvent(e))
359 {
360 ev.target = target;
361 SendEvent(ev, e);
362 }
363 }
364
365 private void SendKeyDownEvent(Event e, Focusable target)
366 {
367 // Use UIElementsRuntimeUtility.CreateEvent because DefaultEventSystem uses it too
368 // and we want to have the same behavior as much as possible.
369 using (var ev = (KeyDownEvent) UIElementsRuntimeUtility.CreateEvent(e))
370 {
371 ev.target = target;
372 SendEvent(ev, e);
373 }
374 }
375
376 private bool ReadPointerData(PointerEvent pe, PointerEventData eventData, PointerEventType eventType = PointerEventType.Default)
377 {
378 if (eventSystem == null || eventSystem.currentInputModule == null)
379 return false;
380
381 pe.Read(this, eventData, eventType);
382
383 // PointerEvents making it this far have been validated by PanelRaycaster already
384 m_Panel.ScreenToPanel(pe.position, pe.deltaPosition,
385 out var panelPosition, out var panelDelta, allowOutside:true);
386
387 pe.SetPosition(panelPosition, panelDelta);
388 return true;
389 }
390
391 enum PointerEventType
392 {
393 Default, Down, Up
394 }
395
396 class PointerEvent : IPointerEvent
397 {
398 public int pointerId { get; private set; }
399 public string pointerType { get; private set; }
400 public bool isPrimary { get; private set; }
401 public int button { get; private set; }
402 public int pressedButtons { get; private set; }
403 public Vector3 position { get; private set; }
404 public Vector3 localPosition { get; private set; }
405 public Vector3 deltaPosition { get; private set; }
406 public float deltaTime { get; private set; }
407 public int clickCount { get; private set; }
408 public float pressure { get; private set; }
409 public float tangentialPressure { get; private set; }
410 public float altitudeAngle { get; private set; }
411 public float azimuthAngle { get; private set; }
412 public float twist { get; private set; }
413 public Vector2 tilt { get; private set; }
414 public PenStatus penStatus { get; private set; }
415 public Vector2 radius { get; private set; }
416 public Vector2 radiusVariance { get; private set; }
417 public EventModifiers modifiers { get; private set; }
418
419 public bool shiftKey => (modifiers & EventModifiers.Shift) != 0;
420 public bool ctrlKey => (modifiers & EventModifiers.Control) != 0;
421 public bool commandKey => (modifiers & EventModifiers.Command) != 0;
422 public bool altKey => (modifiers & EventModifiers.Alt) != 0;
423
424 public bool actionKey =>
425 Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer
426 ? commandKey
427 : ctrlKey;
428
429 public void Read(PanelEventHandler self, PointerEventData eventData, PointerEventType eventType)
430 {
431 pointerId = self.eventSystem.currentInputModule.ConvertUIToolkitPointerId(eventData);
432
433 bool InRange(int i, int start, int count) => i >= start && i < start + count;
434
435 pointerType =
436 InRange(pointerId, PointerId.touchPointerIdBase, PointerId.touchPointerCount) ? PointerType.touch :
437 InRange(pointerId, PointerId.penPointerIdBase, PointerId.penPointerCount) ? PointerType.pen :
438 PointerType.mouse;
439
440 isPrimary = pointerId == PointerId.mousePointerId ||
441 pointerId == PointerId.touchPointerIdBase ||
442 pointerId == PointerId.penPointerIdBase;
443
444 // Flip Y axis between input and UITK
445 var h = Screen.height;
446
447 Vector3 eventPosition = MultipleDisplayUtilities.GetRelativeMousePositionForRaycast(eventData);
448 int eventDisplayIndex = (int)eventPosition.z;
449
450 if (eventDisplayIndex > 0 && eventDisplayIndex < Display.displays.Length)
451 h = Display.displays[eventDisplayIndex].systemHeight;
452
453 var delta = eventData.delta;
454 eventPosition.y = h - eventPosition.y;
455 delta.y = -delta.y;
456
457 localPosition = position = eventPosition;
458 deltaPosition = delta;
459
460 deltaTime = 0; //TODO: find out what's expected here. Time since last frame? Since last sent event?
461 pressure = eventData.pressure;
462 tangentialPressure = eventData.tangentialPressure;
463 altitudeAngle = eventData.altitudeAngle;
464 azimuthAngle = eventData.azimuthAngle;
465 twist = eventData.twist;
466 tilt = eventData.tilt;
467 penStatus = eventData.penStatus;
468 radius = eventData.radius;
469 radiusVariance = eventData.radiusVariance;
470
471 modifiers = s_Modifiers;
472
473 if (eventType == PointerEventType.Default)
474 {
475 button = -1;
476 clickCount = 0;
477 }
478 else
479 {
480 button = Mathf.Max(0, (int)eventData.button);
481 clickCount = eventData.clickCount;
482
483 if (eventType == PointerEventType.Down)
484 {
485 // UUM-57082: InputSystem doesn't reset clickCount on delay until after it sends PointerDown
486 // This is not perfect but it's the best we can do with incomplete information.
487 if (Time.unscaledTime > self.m_LastClickTime + ClickDetector.s_DoubleClickTime * 0.001f)
488 clickCount = 0;
489
490 // Case 1379054: UIToolkit assumes clickCount is increased before PointerDown, but UGUI does it after.
491 clickCount++;
492
493 PointerDeviceState.PressButton(pointerId, button);
494 }
495 else if (eventType == PointerEventType.Up)
496 {
497 PointerDeviceState.ReleaseButton(pointerId, button);
498 }
499
500 clickCount = Mathf.Max(1, clickCount);
501 }
502
503 pressedButtons = PointerDeviceState.GetPressedButtons(pointerId);
504 }
505
506 public void SetPosition(Vector3 positionOverride, Vector3 deltaOverride)
507 {
508 localPosition = position = positionOverride;
509 deltaPosition = deltaOverride;
510 }
511 }
512 }
513#endif
514}