A game framework written with osu! in mind.
at master 8.4 kB view raw
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}