#include "SDL_internal.h"
#ifdef SDL_VIDEO_DRIVER_UIKIT
#include "../SDL_sysvideo.h"
#include "../../events/SDL_events_c.h"
#include "SDL_uikitviewcontroller.h"
#include "SDL_uikitmessagebox.h"
#include "SDL_uikitevents.h"
#include "SDL_uikitvideo.h"
#include "SDL_uikitmodes.h"
#include "SDL_uikitwindow.h"
#include "SDL_uikitopengles.h"
#ifdef SDL_PLATFORM_TVOS
static void SDLCALL SDL_AppleTVControllerUIHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
{
@autoreleasepool {
SDL_uikitviewcontroller *viewcontroller = (__bridge SDL_uikitviewcontroller *)userdata;
viewcontroller.controllerUserInteractionEnabled = hint && (*hint != '0');
}
}
#endif
#ifndef SDL_PLATFORM_TVOS
static void SDLCALL SDL_HideHomeIndicatorHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
{
@autoreleasepool {
SDL_uikitviewcontroller *viewcontroller = (__bridge SDL_uikitviewcontroller *)userdata;
viewcontroller.homeIndicatorHidden = (hint && *hint) ? SDL_atoi(hint) : -1;
[viewcontroller setNeedsUpdateOfHomeIndicatorAutoHidden];
[viewcontroller setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
}
}
#endif
@implementation SDLUITextField : UITextField
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (action == @selector(paste:)) {
return NO;
}
return [super canPerformAction:action withSender:sender];
}
@end
@implementation SDL_uikitviewcontroller
{
CADisplayLink *displayLink;
int animationInterval;
void (*animationCallback)(void *);
void *animationCallbackParam;
#ifdef SDL_IPHONE_KEYBOARD
SDLUITextField *textField;
BOOL hidingKeyboard;
BOOL rotatingOrientation;
NSString *committedText;
NSString *obligateForBackspace;
BOOL isOTPMode;
#endif
}
@synthesize window;
- (instancetype)initWithSDLWindow:(SDL_Window *)_window
{
if (self = [super initWithNibName:nil bundle:nil]) {
self.window = _window;
#ifdef SDL_IPHONE_KEYBOARD
[self initKeyboard];
hidingKeyboard = NO;
rotatingOrientation = NO;
#endif
#ifdef SDL_PLATFORM_TVOS
SDL_AddHintCallback(SDL_HINT_APPLE_TV_CONTROLLER_UI_EVENTS,
SDL_AppleTVControllerUIHintChanged,
(__bridge void *)self);
#endif
#ifndef SDL_PLATFORM_TVOS
SDL_AddHintCallback(SDL_HINT_IOS_HIDE_HOME_INDICATOR,
SDL_HideHomeIndicatorHintChanged,
(__bridge void *)self);
#endif
if (@available(iOS 15.0, tvOS 15.0, *)) {
const SDL_DisplayMode *mode = SDL_GetDesktopDisplayMode(SDL_GetPrimaryDisplay());
if (mode && mode->refresh_rate > 60.0f) {
int frame_rate = (int)mode->refresh_rate;
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(doLoop:)];
displayLink.preferredFrameRateRange = CAFrameRateRangeMake((frame_rate * 2) / 3, frame_rate, frame_rate);
[displayLink addToRunLoop:NSRunLoop.currentRunLoop forMode:NSDefaultRunLoopMode];
}
}
}
return self;
}
- (void)dealloc
{
#ifdef SDL_IPHONE_KEYBOARD
[self deinitKeyboard];
#endif
#ifdef SDL_PLATFORM_TVOS
SDL_RemoveHintCallback(SDL_HINT_APPLE_TV_CONTROLLER_UI_EVENTS,
SDL_AppleTVControllerUIHintChanged,
(__bridge void *)self);
#endif
#ifndef SDL_PLATFORM_TVOS
SDL_RemoveHintCallback(SDL_HINT_IOS_HIDE_HOME_INDICATOR,
SDL_HideHomeIndicatorHintChanged,
(__bridge void *)self);
#endif
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
SDL_SetSystemTheme(UIKit_GetSystemTheme());
}
- (void)setAnimationCallback:(int)interval
callback:(void (*)(void *))callback
callbackParam:(void *)callbackParam
{
[self stopAnimation];
if (interval <= 0) {
interval = 1;
}
animationInterval = interval;
animationCallback = callback;
animationCallbackParam = callbackParam;
if (animationCallback) {
[self startAnimation];
}
}
- (void)startAnimation
{
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(doLoop:)];
#ifdef SDL_PLATFORM_VISIONOS
displayLink.preferredFramesPerSecond = 90 / animationInterval; #else
SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
displayLink.preferredFramesPerSecond = data.uiwindow.screen.maximumFramesPerSecond / animationInterval;
#endif
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopAnimation
{
[displayLink invalidate];
displayLink = nil;
}
- (void)doLoop:(CADisplayLink *)sender
{
if (animationCallback && !UIKit_ShowingMessageBox()) {
#if defined(SDL_VIDEO_OPENGL_ES) || defined(SDL_VIDEO_OPENGL_ES2)
UIKit_GL_RestoreCurrentContext();
#endif
animationCallback(animationCallbackParam);
}
}
- (void)loadView
{
}
- (void)viewDidLayoutSubviews
{
const CGSize size = self.view.bounds.size;
int w = (int)size.width;
int h = (int)size.height;
SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESIZED, w, h);
}
#ifndef SDL_PLATFORM_TVOS
- (NSUInteger)supportedInterfaceOrientations
{
return UIKit_GetSupportedOrientations(window);
}
- (BOOL)prefersStatusBarHidden
{
BOOL hidden = (window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS)) != 0;
return hidden;
}
- (BOOL)prefersHomeIndicatorAutoHidden
{
BOOL hidden = NO;
if (self.homeIndicatorHidden == 1) {
hidden = YES;
}
return hidden;
}
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures
{
if (self.homeIndicatorHidden >= 0) {
if (self.homeIndicatorHidden == 2) {
return UIRectEdgeAll;
} else {
return UIRectEdgeNone;
}
}
if ((window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS)) != 0) {
return UIRectEdgeAll;
} else {
return UIRectEdgeNone;
}
}
- (BOOL)prefersPointerLocked
{
return SDL_GCMouseRelativeMode() ? YES : NO;
}
#endif
#ifdef SDL_IPHONE_KEYBOARD
@synthesize textInputRect;
@synthesize keyboardHeight;
@synthesize textFieldFocused;
- (void)initKeyboard
{
obligateForBackspace = @" "; textField = [[SDLUITextField alloc] initWithFrame:CGRectZero];
textField.delegate = self;
textField.text = obligateForBackspace;
committedText = textField.text;
textField.hidden = YES;
textFieldFocused = NO;
isOTPMode = NO;
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
#ifndef SDL_PLATFORM_TVOS
[center addObserver:self
selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[center addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
[center addObserver:self
selector:@selector(keyboardDidHide:)
name:UIKeyboardDidHideNotification
object:nil];
#endif
[center addObserver:self
selector:@selector(textFieldTextDidChange:)
name:UITextFieldTextDidChangeNotification
object:nil];
}
- (NSArray *)keyCommands
{
NSMutableArray *commands = [[NSMutableArray alloc] init];
[commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:kNilOptions action:@selector(handleCommand:)]];
[commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:kNilOptions action:@selector(handleCommand:)]];
[commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow modifierFlags:kNilOptions action:@selector(handleCommand:)]];
[commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow modifierFlags:kNilOptions action:@selector(handleCommand:)]];
[commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:kNilOptions action:@selector(handleCommand:)]];
return [NSArray arrayWithArray:commands];
}
- (void)handleCommand:(UIKeyCommand *)keyCommand
{
SDL_Scancode scancode = SDL_SCANCODE_UNKNOWN;
NSString *input = keyCommand.input;
if (input == UIKeyInputUpArrow) {
scancode = SDL_SCANCODE_UP;
} else if (input == UIKeyInputDownArrow) {
scancode = SDL_SCANCODE_DOWN;
} else if (input == UIKeyInputLeftArrow) {
scancode = SDL_SCANCODE_LEFT;
} else if (input == UIKeyInputRightArrow) {
scancode = SDL_SCANCODE_RIGHT;
} else if (input == UIKeyInputEscape) {
scancode = SDL_SCANCODE_ESCAPE;
}
if (scancode != SDL_SCANCODE_UNKNOWN) {
SDL_SendKeyboardKeyAutoRelease(0, scancode);
}
}
- (void)setView:(UIView *)view
{
[super setView:view];
if (SDL_WasInit(SDL_INIT_JOYSTICK)) {
UIKit_SetViewGameControllerInteraction(view, true);
}
[view addSubview:textField];
if (textFieldFocused) {
[self startTextInput];
}
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
rotatingOrientation = YES;
[coordinator
animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
}
completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
self->rotatingOrientation = NO;
}];
}
- (void)deinitKeyboard
{
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
#ifndef SDL_PLATFORM_TVOS
[center removeObserver:self
name:UIKeyboardWillShowNotification
object:nil];
[center removeObserver:self
name:UIKeyboardWillHideNotification
object:nil];
[center removeObserver:self
name:UIKeyboardDidHideNotification
object:nil];
#endif
[center removeObserver:self
name:UITextFieldTextDidChangeNotification
object:nil];
}
- (void)setTextFieldProperties:(SDL_PropertiesID) props
{
textField.secureTextEntry = NO;
switch (SDL_GetTextInputType(props)) {
default:
case SDL_TEXTINPUT_TYPE_TEXT:
textField.keyboardType = UIKeyboardTypeDefault;
textField.textContentType = nil;
break;
case SDL_TEXTINPUT_TYPE_TEXT_NAME:
textField.keyboardType = UIKeyboardTypeDefault;
textField.textContentType = UITextContentTypeName;
break;
case SDL_TEXTINPUT_TYPE_TEXT_EMAIL:
textField.keyboardType = UIKeyboardTypeEmailAddress;
textField.textContentType = UITextContentTypeEmailAddress;
break;
case SDL_TEXTINPUT_TYPE_TEXT_USERNAME:
textField.keyboardType = UIKeyboardTypeDefault;
textField.textContentType = UITextContentTypeUsername;
break;
case SDL_TEXTINPUT_TYPE_TEXT_PASSWORD_HIDDEN:
textField.keyboardType = UIKeyboardTypeDefault;
textField.textContentType = UITextContentTypePassword;
textField.secureTextEntry = YES;
break;
case SDL_TEXTINPUT_TYPE_TEXT_PASSWORD_VISIBLE:
textField.keyboardType = UIKeyboardTypeDefault;
textField.textContentType = UITextContentTypePassword;
break;
case SDL_TEXTINPUT_TYPE_NUMBER:
textField.keyboardType = UIKeyboardTypeDecimalPad;
textField.textContentType = nil;
break;
case SDL_TEXTINPUT_TYPE_NUMBER_PASSWORD_HIDDEN:
textField.keyboardType = UIKeyboardTypeNumberPad;
if (@available(iOS 12.0, tvOS 12.0, *)) {
textField.textContentType = UITextContentTypeOneTimeCode;
} else {
textField.textContentType = nil;
}
textField.secureTextEntry = YES;
break;
case SDL_TEXTINPUT_TYPE_NUMBER_PASSWORD_VISIBLE:
textField.keyboardType = UIKeyboardTypeNumberPad;
if (@available(iOS 12.0, tvOS 12.0, *)) {
textField.textContentType = UITextContentTypeOneTimeCode;
} else {
textField.textContentType = nil;
}
break;
}
switch (SDL_GetTextInputCapitalization(props)) {
default:
case SDL_CAPITALIZE_NONE:
textField.autocapitalizationType = UITextAutocapitalizationTypeNone;
break;
case SDL_CAPITALIZE_LETTERS:
textField.autocapitalizationType = UITextAutocapitalizationTypeAllCharacters;
break;
case SDL_CAPITALIZE_WORDS:
textField.autocapitalizationType = UITextAutocapitalizationTypeWords;
break;
case SDL_CAPITALIZE_SENTENCES:
textField.autocapitalizationType = UITextAutocapitalizationTypeSentences;
break;
}
if (SDL_GetTextInputAutocorrect(props)) {
textField.autocorrectionType = UITextAutocorrectionTypeYes;
textField.spellCheckingType = UITextSpellCheckingTypeYes;
} else {
textField.autocorrectionType = UITextAutocorrectionTypeNo;
textField.spellCheckingType = UITextSpellCheckingTypeNo;
}
if (SDL_GetTextInputMultiline(props)) {
textField.enablesReturnKeyAutomatically = YES;
} else {
textField.enablesReturnKeyAutomatically = NO;
}
if (!textField.window) {
return;
}
UIView *superview = textField.superview;
[textField removeFromSuperview];
[superview addSubview:textField];
if (SDL_TextInputActive(window)) {
[textField becomeFirstResponder];
}
isOTPMode =
(SDL_GetTextInputType(props) == SDL_TEXTINPUT_TYPE_NUMBER_PASSWORD_HIDDEN) ||
(SDL_GetTextInputType(props) == SDL_TEXTINPUT_TYPE_NUMBER_PASSWORD_VISIBLE);
}
- (bool)startTextInput
{
if (!textFieldFocused) {
textFieldFocused = YES;
SDL_SendScreenKeyboardShown();
}
if (!textField.window) {
return true;
}
if (isOTPMode) {
if (textField.text.length == 64 && [textField.text isEqualToString:[@"" stringByPaddingToLength:64 withString:@" " startingAtIndex:0]]) {
textField.text = @"";
committedText = @"";
}
}
return [textField becomeFirstResponder];
}
- (bool)stopTextInput
{
if (textFieldFocused) {
textFieldFocused = NO;
SDL_SendScreenKeyboardHidden();
}
if (!textField.window) {
return true;
}
[self resetTextState];
return [textField resignFirstResponder];
}
- (void)keyboardWillShow:(NSNotification *)notification
{
#ifndef SDL_PLATFORM_TVOS
CGRect kbrect = [[notification userInfo][UIKeyboardFrameEndUserInfoKey] CGRectValue];
kbrect = [self.view convertRect:kbrect fromView:nil];
[self setKeyboardHeight:(int)kbrect.size.height];
#endif
if (hidingKeyboard) {
SDL_StartTextInput(window);
hidingKeyboard = NO;
}
}
- (void)keyboardWillHide:(NSNotification *)notification
{
hidingKeyboard = YES;
[self setKeyboardHeight:0];
if (SDL_TextInputActive(window)
&& !SDL_HasKeyboard()
&& !rotatingOrientation) {
SDL_StopTextInput(window);
}
}
- (void)keyboardDidHide:(NSNotification *)notification
{
hidingKeyboard = NO;
}
- (void)textFieldTextDidChange:(NSNotification *)notification
{
bool startTextInputMomentarily = !SDL_TextInputActive(window);
if (startTextInputMomentarily)
SDL_StartTextInput(window);
if (textField.markedTextRange == nil) {
if (isOTPMode && labs((NSInteger)textField.text.length - (NSInteger)committedText.length) != 1) {
return;
}
NSUInteger compareLength = SDL_min(textField.text.length, committedText.length);
NSUInteger matchLength;
for (matchLength = 0; matchLength < compareLength; ++matchLength) {
if ([committedText characterAtIndex:matchLength] != [textField.text characterAtIndex:matchLength]) {
break;
}
}
if (matchLength < committedText.length) {
size_t deleteLength = SDL_utf8strlen([[committedText substringFromIndex:matchLength] UTF8String]);
while (deleteLength > 0) {
SDL_SendKeyboardKey(0, SDL_GLOBAL_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, true);
SDL_SendKeyboardKey(0, SDL_GLOBAL_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, false);
--deleteLength;
}
}
if (matchLength < textField.text.length) {
NSString *pendingText = [textField.text substringFromIndex:matchLength];
if (!SDL_HardwareKeyboardKeyPressed()) {
NSUInteger i;
for (i = 0; i < pendingText.length; i++) {
SDL_SendKeyboardUnicodeKey(0, [pendingText characterAtIndex:i]);
}
}
SDL_SendKeyboardText([pendingText UTF8String]);
}
committedText = textField.text;
}
if (startTextInputMomentarily)
SDL_StopTextInput(window);
}
- (void)updateKeyboard
{
SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *) window->internal;
CGAffineTransform t = self.view.transform;
CGPoint offset = CGPointMake(0.0, 0.0);
#ifdef SDL_PLATFORM_VISIONOS
CGRect frame = UIKit_ComputeViewFrame(window);
#else
CGRect frame = UIKit_ComputeViewFrame(window, data.uiwindow.screen);
#endif
if (self.keyboardHeight && self.textInputRect.h) {
int rectbottom = (int)(self.textInputRect.y + self.textInputRect.h);
int keybottom = (int)(self.view.bounds.size.height - self.keyboardHeight);
if (keybottom < rectbottom) {
offset.y = keybottom - rectbottom;
}
}
t.tx = 0.0;
t.ty = 0.0;
offset = CGPointApplyAffineTransform(offset, t);
frame.origin.x += offset.x;
frame.origin.y += offset.y;
self.view.frame = frame;
}
- (void)setKeyboardHeight:(int)height
{
keyboardHeight = height;
[self updateKeyboard];
}
- (BOOL)textField:(UITextField *)_textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (!isOTPMode) {
if (textField.markedTextRange == nil && [string length] == 0 && textField.text.length < 16) {
[self resetTextState];
}
}
return YES;
}
- (BOOL)textFieldShouldReturn:(UITextField *)_textField
{
SDL_SendKeyboardKeyAutoRelease(0, SDL_SCANCODE_RETURN);
if (textFieldFocused &&
SDL_GetHintBoolean(SDL_HINT_RETURN_KEY_HIDES_IME, false)) {
SDL_StopTextInput(window);
}
return YES;
}
- (void)resetTextState
{
if (!isOTPMode) {
textField.text = obligateForBackspace;
committedText = textField.text;
}
}
#endif
@end
#ifdef SDL_IPHONE_KEYBOARD
static SDL_uikitviewcontroller *GetWindowViewController(SDL_Window *window)
{
if (!window || !window->internal) {
SDL_SetError("Invalid window");
return nil;
}
SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
return data.viewcontroller;
}
bool UIKit_HasScreenKeyboardSupport(SDL_VideoDevice *_this)
{
return true;
}
bool UIKit_StartTextInput(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props)
{
@autoreleasepool {
SDL_uikitviewcontroller *vc = GetWindowViewController(window);
return [vc startTextInput];
}
}
bool UIKit_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window)
{
@autoreleasepool {
SDL_uikitviewcontroller *vc = GetWindowViewController(window);
return [vc stopTextInput];
}
}
void UIKit_SetTextInputProperties(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props)
{
@autoreleasepool {
SDL_uikitviewcontroller *vc = GetWindowViewController(window);
[vc setTextFieldProperties:props];
}
}
bool UIKit_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window)
{
@autoreleasepool {
SDL_uikitviewcontroller *vc = GetWindowViewController(window);
if (vc != nil) {
vc.textInputRect = window->text_input_rect;
if (vc.textFieldFocused) {
[vc updateKeyboard];
}
}
}
return true;
}
#endif
#endif