A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System.Diagnostics;
5using Foundation;
6using osu.Framework.Input;
7using osu.Framework.Input.Handlers;
8using osu.Framework.Input.StateChanges;
9using osu.Framework.Input.States;
10using osu.Framework.Platform;
11using osuTK;
12using osuTK.Input;
13using UIKit;
14
15namespace osu.Framework.iOS.Input
16{
17 public class IOSTouchHandler : InputHandler
18 {
19 private readonly IOSGameView view;
20
21 private UIEventButtonMask? lastButtonMask;
22
23 private readonly bool indirectPointerSupported = UIDevice.CurrentDevice.CheckSystemVersion(13, 4);
24
25 private readonly UITouch[] activeTouches = new UITouch[TouchState.MAX_TOUCH_COUNT];
26
27 public IOSTouchHandler(IOSGameView view)
28 {
29 this.view = view;
30 view.HandleTouches += handleTouches;
31 }
32
33 private void handleTouches(NSSet obj, UIEvent evt)
34 {
35 foreach (var t in obj)
36 handleUITouch((UITouch)t, evt);
37 }
38
39 private void handleUITouch(UITouch touch, UIEvent e)
40 {
41 var cgLocation = touch.LocationInView(null);
42 Vector2 location = new Vector2((float)cgLocation.X * view.Scale, (float)cgLocation.Y * view.Scale);
43
44 if (indirectPointerSupported && touch.Type == UITouchType.IndirectPointer)
45 handleIndirectPointer(touch, e.ButtonMask, location);
46 else
47 handleTouch(touch, location);
48 }
49
50 private void handleIndirectPointer(UITouch touch, UIEventButtonMask buttonMask, Vector2 location)
51 {
52 PendingInputs.Enqueue(new MousePositionAbsoluteInput { Position = location });
53
54 // Indirect pointer means the touch came from a mouse cursor, and wasn't a physical touch on the screen
55 switch (touch.Phase)
56 {
57 case UITouchPhase.Began:
58 case UITouchPhase.Moved:
59 // only one button can be in a "down" state at once. all previous buttons are automatically released.
60 // we need to handle this assumption at our end.
61 if (lastButtonMask != null && lastButtonMask != buttonMask)
62 PendingInputs.Enqueue(new MouseButtonInput(buttonFromMask(lastButtonMask.Value), false));
63
64 PendingInputs.Enqueue(new MouseButtonInput(buttonFromMask(buttonMask), true));
65 lastButtonMask = buttonMask;
66 break;
67
68 case UITouchPhase.Cancelled:
69 case UITouchPhase.Ended:
70 Debug.Assert(lastButtonMask != null);
71
72 PendingInputs.Enqueue(new MouseButtonInput(buttonFromMask(lastButtonMask.Value), false));
73 lastButtonMask = null;
74 break;
75 }
76 }
77
78 private void handleTouch(UITouch uiTouch, Vector2 location)
79 {
80 TouchSource? existingSource = getTouchSource(uiTouch);
81
82 if (uiTouch.Phase == UITouchPhase.Began)
83 {
84 // need to assign the new touch.
85 Debug.Assert(existingSource == null);
86
87 existingSource = assignNextAvailableTouchSource(uiTouch);
88 }
89
90 if (existingSource == null)
91 return;
92
93 var touch = new Touch(existingSource.Value, location);
94
95 // standard touch handling
96 switch (uiTouch.Phase)
97 {
98 case UITouchPhase.Began:
99 case UITouchPhase.Moved:
100 PendingInputs.Enqueue(new TouchInput(touch, true));
101 break;
102
103 case UITouchPhase.Cancelled:
104 case UITouchPhase.Ended:
105 PendingInputs.Enqueue(new TouchInput(touch, false));
106
107 // touch no longer valid, remove from reference array.
108 activeTouches[(int)existingSource] = null;
109 break;
110 }
111 }
112
113 private TouchSource? assignNextAvailableTouchSource(UITouch uiTouch)
114 {
115 for (int i = 0; i < activeTouches.Length; i++)
116 {
117 if (activeTouches[i] != null) continue;
118
119 activeTouches[i] = uiTouch;
120 return (TouchSource)i;
121 }
122
123 // we only handle up to TouchState.MAX_TOUCH_COUNT. Ignore any further touches for now.
124 return null;
125 }
126
127 private TouchSource? getTouchSource(UITouch touch)
128 {
129 for (int i = 0; i < activeTouches.Length; i++)
130 {
131 // The recommended (and only) way to track touches is storing and comparing references of the UITouch objects.
132 // https://stackoverflow.com/questions/39823914/how-to-track-multiple-touches
133 if (ReferenceEquals(activeTouches[i], touch))
134 return (TouchSource)i;
135 }
136
137 return null;
138 }
139
140 private MouseButton buttonFromMask(UIEventButtonMask buttonMask)
141 {
142 Debug.Assert(indirectPointerSupported);
143
144 switch (buttonMask)
145 {
146 default:
147 return MouseButton.Left;
148
149 case UIEventButtonMask.Secondary:
150 return MouseButton.Right;
151 }
152 }
153
154 protected override void Dispose(bool disposing)
155 {
156 view.HandleTouches -= handleTouches;
157 base.Dispose(disposing);
158 }
159
160 public override bool IsActive => true;
161
162 public override bool Initialize(GameHost host) => true;
163 }
164}