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;
5using System.Collections.Generic;
6using System.Drawing;
7using System.Linq;
8using System.Threading.Tasks;
9using CoreAnimation;
10using Foundation;
11using ObjCRuntime;
12using OpenGLES;
13using osu.Framework.Graphics.OpenGL;
14using osuTK.Graphics.ES30;
15using osuTK.iOS;
16using UIKit;
17
18namespace osu.Framework.iOS
19{
20 [Register("iOSGameView")]
21 public class IOSGameView : iOSGameView
22 {
23 public event Action<NSSet, UIEvent> HandleTouches;
24
25 public HiddenTextField KeyboardTextField { get; }
26
27 [Export("layerClass")]
28 public static Class LayerClass() => GetLayerClass();
29
30 [Export("initWithFrame:")]
31 public IOSGameView(RectangleF frame)
32 : base(frame)
33 {
34 Scale = (float)UIScreen.MainScreen.Scale;
35 ContentScaleFactor = UIScreen.MainScreen.Scale;
36 LayerColorFormat = EAGLColorFormat.RGBA8;
37 ContextRenderingApi = EAGLRenderingAPI.OpenGLES3;
38 LayerRetainsBacking = false;
39
40 AddSubview(KeyboardTextField = new HiddenTextField());
41 }
42
43 protected override void ConfigureLayer(CAEAGLLayer eaglLayer)
44 {
45 eaglLayer.Opaque = true;
46 ExclusiveTouch = true;
47 MultipleTouchEnabled = true;
48 UserInteractionEnabled = true;
49 }
50
51 public float Scale { get; }
52
53 // SafeAreaInsets is cached to prevent access outside the main thread
54 private UIEdgeInsets safeArea = UIEdgeInsets.Zero;
55
56 internal UIEdgeInsets SafeArea
57 {
58 get => safeArea;
59 set
60 {
61 if (value.Equals(safeArea))
62 return;
63
64 safeArea = value;
65 OnResize(EventArgs.Empty);
66 }
67 }
68
69 public override void TouchesBegan(NSSet touches, UIEvent evt) => HandleTouches?.Invoke(touches, evt);
70 public override void TouchesCancelled(NSSet touches, UIEvent evt) => HandleTouches?.Invoke(touches, evt);
71 public override void TouchesEnded(NSSet touches, UIEvent evt) => HandleTouches?.Invoke(touches, evt);
72 public override void TouchesMoved(NSSet touches, UIEvent evt) => HandleTouches?.Invoke(touches, evt);
73
74 protected override void CreateFrameBuffer()
75 {
76 base.CreateFrameBuffer();
77 GLWrapper.DefaultFrameBuffer = Framebuffer;
78 }
79
80 private bool needsResizeFrameBuffer;
81 public void RequestResizeFrameBuffer() => needsResizeFrameBuffer = true;
82
83 public override void LayoutSubviews()
84 {
85 base.LayoutSubviews();
86 SafeArea = SafeAreaInsets;
87 }
88
89 public override void SwapBuffers()
90 {
91 base.SwapBuffers();
92
93 // ResizeFrameBuffer needs to run on the main thread, but triggered in such a way that it blocks our draw thread until done
94 if (needsResizeFrameBuffer)
95 {
96 needsResizeFrameBuffer = false;
97 GL.Finish();
98 InvokeOnMainThread(ResizeFrameBuffer);
99 }
100 }
101
102 protected override bool ShouldCallOnRender => false;
103
104 public class HiddenTextField : UITextField
105 {
106 public event Action<NSRange, string> HandleShouldChangeCharacters;
107 public event Action HandleShouldReturn;
108 public event Action<UIKeyCommand> HandleKeyCommand;
109
110 /// <summary>
111 /// Placeholder text that the <see cref="HiddenTextField"/> will be populated with after every keystroke.
112 /// </summary>
113 private const string placeholder_text = "aaaaaa";
114
115 /// <summary>
116 /// The approximate midpoint of <see cref="placeholder_text"/> that the cursor will be reset to after every keystroke.
117 /// </summary>
118 public const int CURSOR_POSITION = 3;
119
120 private int responderSemaphore;
121
122 private readonly IEnumerable<Selector> softwareBlockedActions = new[]
123 {
124 new Selector("cut:"),
125 new Selector("copy:"),
126 new Selector("select:"),
127 new Selector("selectAll:"),
128 };
129
130 private readonly IEnumerable<Selector> rawBlockedActions = new[]
131 {
132 new Selector("cut:"),
133 new Selector("copy:"),
134 new Selector("paste:"),
135 new Selector("select:"),
136 new Selector("selectAll:"),
137 };
138
139 public override UITextSmartDashesType SmartDashesType => UITextSmartDashesType.No;
140 public override UITextSmartInsertDeleteType SmartInsertDeleteType => UITextSmartInsertDeleteType.No;
141 public override UITextSmartQuotesType SmartQuotesType => UITextSmartQuotesType.No;
142
143 private bool softwareKeyboard = true;
144
145 internal bool SoftwareKeyboard
146 {
147 get => softwareKeyboard;
148 set
149 {
150 softwareKeyboard = value;
151 resetText();
152 }
153 }
154
155 public HiddenTextField()
156 {
157 AutocapitalizationType = UITextAutocapitalizationType.None;
158 AutocorrectionType = UITextAutocorrectionType.No;
159 KeyboardType = UIKeyboardType.Default;
160 KeyboardAppearance = UIKeyboardAppearance.Default;
161
162 resetText();
163
164 ShouldChangeCharacters = (textField, range, replacementString) =>
165 {
166 resetText();
167 HandleShouldChangeCharacters?.Invoke(range, replacementString);
168 return false;
169 };
170
171 ShouldReturn = textField =>
172 {
173 resetText();
174 HandleShouldReturn?.Invoke();
175 return false;
176 };
177 }
178
179 public override UIKeyCommand[] KeyCommands => new[]
180 {
181 UIKeyCommand.Create(UIKeyCommand.LeftArrow, 0, new Selector("keyPressed:")),
182 UIKeyCommand.Create(UIKeyCommand.RightArrow, 0, new Selector("keyPressed:")),
183 UIKeyCommand.Create(UIKeyCommand.UpArrow, 0, new Selector("keyPressed:")),
184 UIKeyCommand.Create(UIKeyCommand.DownArrow, 0, new Selector("keyPressed:"))
185 };
186
187 public override bool CanPerform(Selector action, NSObject withSender)
188 {
189 if ((!softwareKeyboard && rawBlockedActions.Contains(action)) || (softwareKeyboard && softwareBlockedActions.Contains(action)))
190 return false;
191
192 return base.CanPerform(action, withSender);
193 }
194
195 [Export("keyPressed:")]
196 private void keyPressed(UIKeyCommand cmd) => HandleKeyCommand?.Invoke(cmd);
197
198 private void resetText()
199 {
200 if (SoftwareKeyboard)
201 {
202 // we put in some dummy text and move the cursor to the middle so that backspace (and potentially delete or cursor keys) will be detected
203 Text = placeholder_text;
204 var newPosition = GetPosition(BeginningOfDocument, CURSOR_POSITION);
205 SelectedTextRange = GetTextRange(newPosition, newPosition);
206 }
207 else
208 {
209 Text = "";
210 SelectedTextRange = GetTextRange(BeginningOfDocument, BeginningOfDocument);
211 }
212 }
213
214 public void UpdateFirstResponder(bool become)
215 {
216 if (become)
217 {
218 responderSemaphore = Math.Max(responderSemaphore + 1, 1);
219 InvokeOnMainThread(() => BecomeFirstResponder());
220 }
221 else
222 {
223 responderSemaphore = Math.Max(responderSemaphore - 1, 0);
224 Task.Delay(200).ContinueWith(task =>
225 {
226 if (responderSemaphore <= 0)
227 InvokeOnMainThread(() => ResignFirstResponder());
228 });
229 }
230 }
231 }
232 }
233}