Skip to content

Commit 7d5c1b5

Browse files
authored
iOS/tvOS: use native keyboard (#18355)
1 parent a6d765d commit 7d5c1b5

File tree

4 files changed

+236
-0
lines changed

4 files changed

+236
-0
lines changed

gfx/gfx_display.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
#include "../steam/steam.h"
2424
#endif
2525

26+
#ifdef HAVE_COCOATOUCH
27+
#include "../ui/drivers/cocoa/apple_platform.h"
28+
#endif
29+
2630
/* Standard reference DPI value, used when determining
2731
* DPI-aware scaling factors */
2832
#define REFERENCE_DPI 96.0f
@@ -999,6 +1003,10 @@ void gfx_display_draw_keyboard(
9991003
if (steam_has_osk_open())
10001004
return;
10011005
#endif
1006+
#ifdef HAVE_COCOATOUCH
1007+
if (ios_keyboard_active())
1008+
return;
1009+
#endif
10021010

10031011
gfx_display_draw_quad(
10041012
p_disp,

menu/menu_driver.c

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@
8282
#include "../steam/steam.h"
8383
#endif
8484

85+
#ifdef HAVE_COCOATOUCH
86+
#include "../ui/drivers/cocoa/apple_platform.h"
87+
#endif
88+
8589
typedef struct menu_input_ctx_bind
8690
{
8791
char *s;
@@ -4547,6 +4551,12 @@ void menu_input_dialog_end(void)
45474551
* > Required, since input is ignored for 1 frame
45484552
* after certain events - e.g. closing the OSK */
45494553
menu_st->input_driver_flushing_input = 2;
4554+
4555+
#ifdef HAVE_COCOATOUCH
4556+
/* Dismiss iOS/tvOS native keyboard if it's currently open */
4557+
if (ios_keyboard_active())
4558+
ios_keyboard_end();
4559+
#endif
45504560
}
45514561

45524562
#if defined(_MSC_VER)
@@ -5317,6 +5327,13 @@ unsigned menu_event(
53175327
/* > If pointer input is disabled, do nothing */
53185328
if (!menu_mouse_enable && !menu_pointer_enable)
53195329
menu_input->pointer.type = MENU_POINTER_DISABLED;
5330+
#ifdef HAVE_COCOATOUCH
5331+
/* > Also disable when keyboard dialog is active to prevent touch events
5332+
* from iOS keyboard (e.g., Return button) from being interpreted as
5333+
* menu input that could reopen the keyboard */
5334+
else if (menu_st->flags & MENU_ST_FLAG_INP_DLG_KB_DISPLAY)
5335+
menu_input->pointer.type = MENU_POINTER_DISABLED;
5336+
#endif
53205337
else
53215338
{
53225339
menu_input_pointer_hw_state_t mouse_hw_state = {0};
@@ -8194,6 +8211,13 @@ bool menu_input_dialog_start(menu_input_ctx_line_t *line)
81948211
if (!line || !menu)
81958212
return false;
81968213

8214+
#ifdef HAVE_COCOATOUCH
8215+
/* Prevent reopening keyboard if it's already active
8216+
* This can happen when return key events trigger menu OK actions */
8217+
if (menu_st->flags & MENU_ST_FLAG_INP_DLG_KB_DISPLAY)
8218+
return false;
8219+
#endif
8220+
81978221
#ifdef HAVE_MIST
81988222
steam_open_osk();
81998223
#endif
@@ -8228,6 +8252,17 @@ bool menu_input_dialog_start(menu_input_ctx_line_t *line)
82288252
input_keyboard_start_line(menu,
82298253
&input_st->keyboard_line,
82308254
line->cb);
8255+
8256+
#ifdef HAVE_COCOATOUCH
8257+
/* Use iOS/tvOS native keyboard instead of custom on-screen keyboard */
8258+
ios_keyboard_start(
8259+
(char **)menu_st->input_dialog_keyboard_buffer,
8260+
&input_st->keyboard_line.size,
8261+
line->label,
8262+
line->cb,
8263+
menu);
8264+
#endif
8265+
82318266
/* While reading keyboard line input, we have to block all hotkeys. */
82328267
input_st->flags |= INP_FLAG_KB_MAPPING_BLOCKED;
82338268

ui/drivers/cocoa/apple_platform.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ extern void ios_show_file_sheet(void);
1515
extern bool ios_running_on_ipad(void);
1616
#endif
1717

18+
#if TARGET_OS_IPHONE
19+
/* iOS native keyboard support */
20+
typedef void (*input_keyboard_line_complete_t)(void *userdata, const char *line);
21+
extern bool ios_keyboard_start(char **buffer_ptr, size_t *size_ptr, const char *label,
22+
input_keyboard_line_complete_t callback, void *userdata);
23+
extern bool ios_keyboard_active(void);
24+
extern void ios_keyboard_end(void);
25+
#endif
26+
1827
#if TARGET_OS_OSX
1928
extern void osx_show_file_sheet(void);
2029
#endif

ui/drivers/ui_cocoatouch.m

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "../../configuration.h"
3333
#include "../../frontend/frontend.h"
3434
#include "../../input/drivers/cocoa_input.h"
35+
#include "../../input/input_driver.h"
3536
#include "../../input/drivers_keyboard/keyboard_event_apple.h"
3637
#include "../../retroarch.h"
3738
#include "../../tasks/task_content.h"
@@ -513,6 +514,14 @@ @interface RetroArch_iOS () <MXMetricManagerSubscriber, UIPointerInteractionDele
513514
@end
514515
#endif
515516

517+
@interface RetroArch_iOS () <UITextFieldDelegate>
518+
@property (nonatomic, strong) UITextField *keyboardTextField;
519+
@property (nonatomic, copy) void(^keyboardCompletionCallback)(const char *);
520+
@property (nonatomic, assign) char **keyboardBufferPtr;
521+
@property (nonatomic, assign) size_t *keyboardSizePtr;
522+
@property (nonatomic, assign) char *keyboardAllocatedBuffer;
523+
@end
524+
516525
@implementation RetroArch_iOS
517526

518527
#pragma mark - ApplePlatform
@@ -1060,6 +1069,21 @@ - (void)showGameView
10601069

10611070
[self.window setRootViewController:[CocoaView get]];
10621071

1072+
/* Initialize hidden keyboard text field for iOS native keyboard support */
1073+
if (!self.keyboardTextField)
1074+
{
1075+
self.keyboardTextField = [[UITextField alloc] initWithFrame:CGRectMake(0, -100, 1, 1)];
1076+
self.keyboardTextField.delegate = self;
1077+
self.keyboardTextField.autocapitalizationType = UITextAutocapitalizationTypeNone;
1078+
self.keyboardTextField.autocorrectionType = UITextAutocorrectionTypeNo;
1079+
self.keyboardTextField.spellCheckingType = UITextSpellCheckingTypeNo;
1080+
self.keyboardTextField.smartQuotesType = UITextSmartQuotesTypeNo;
1081+
self.keyboardTextField.smartDashesType = UITextSmartDashesTypeNo;
1082+
self.keyboardTextField.smartInsertDeleteType = UITextSmartInsertDeleteTypeNo;
1083+
self.keyboardTextField.returnKeyType = UIReturnKeyDone;
1084+
[[CocoaView get].view addSubview:self.keyboardTextField];
1085+
}
1086+
10631087
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
10641088
command_event(CMD_EVENT_AUDIO_START, NULL);
10651089
});
@@ -1112,6 +1136,93 @@ - (UIPointerRegion *)pointerInteraction:(UIPointerInteraction *)interaction
11121136
}
11131137
#endif
11141138

1139+
#pragma mark - UITextFieldDelegate (iOS/tvOS Native Keyboard Support)
1140+
1141+
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
1142+
{
1143+
if (textField != self.keyboardTextField || !self.keyboardAllocatedBuffer || !self.keyboardSizePtr)
1144+
return YES;
1145+
1146+
/* Calculate new text */
1147+
NSString *newText = [textField.text stringByReplacingCharactersInRange:range withString:string];
1148+
1149+
/* Update the RetroArch buffer in real-time so the menu can display it */
1150+
const char *utf8Text = [newText UTF8String];
1151+
if (utf8Text)
1152+
{
1153+
strlcpy(self.keyboardAllocatedBuffer, utf8Text, 512);
1154+
*self.keyboardSizePtr = strlen(self.keyboardAllocatedBuffer);
1155+
}
1156+
1157+
return YES;
1158+
}
1159+
1160+
- (BOOL)textFieldShouldReturn:(UITextField *)textField
1161+
{
1162+
if (textField == self.keyboardTextField)
1163+
{
1164+
/* Update buffer with final text before calling callback */
1165+
if (self.keyboardAllocatedBuffer && self.keyboardSizePtr)
1166+
{
1167+
const char *finalText = [textField.text UTF8String];
1168+
if (finalText)
1169+
{
1170+
strlcpy(self.keyboardAllocatedBuffer, finalText, 512);
1171+
*self.keyboardSizePtr = strlen(self.keyboardAllocatedBuffer);
1172+
}
1173+
}
1174+
1175+
/* Store callback and buffer before clearing callback reference */
1176+
void(^callback)(const char *) = self.keyboardCompletionCallback;
1177+
char *buffer = self.keyboardAllocatedBuffer;
1178+
1179+
/* Clear callback to prevent double-invoke, but keep buffer references
1180+
* since the callback will free the buffer via input_keyboard_line_free() */
1181+
self.keyboardCompletionCallback = nil;
1182+
1183+
/* DON'T dismiss keyboard here - let menu_input_dialog_end() -> ios_keyboard_end() do it
1184+
* This ensures ios_keyboard_active() returns true when the callback checks it */
1185+
1186+
/* Call completion callback with buffer pointer
1187+
* The callback will call menu_input_dialog_end() which will call ios_keyboard_end() */
1188+
if (callback && buffer)
1189+
callback(buffer);
1190+
/* Clear our references after callback completes */
1191+
self.keyboardBufferPtr = NULL;
1192+
self.keyboardSizePtr = NULL;
1193+
self.keyboardAllocatedBuffer = NULL;
1194+
1195+
return NO; /* Return NO to prevent UIKit from processing the return key event further */
1196+
}
1197+
return YES;
1198+
}
1199+
1200+
- (void)textFieldDidEndEditing:(UITextField *)textField
1201+
{
1202+
if (textField == self.keyboardTextField)
1203+
{
1204+
/* Only call callback if it wasn't already called (by textFieldShouldReturn) */
1205+
if (self.keyboardCompletionCallback)
1206+
{
1207+
/* User dismissed keyboard without hitting return - treat as cancel */
1208+
void(^callback)(const char *) = self.keyboardCompletionCallback;
1209+
1210+
/* Clear callback to prevent double-invoke, but keep buffer references
1211+
* since the callback will free the buffer via input_keyboard_line_free() */
1212+
self.keyboardCompletionCallback = nil;
1213+
1214+
/* Call callback with NULL to indicate cancel
1215+
* The callback will handle cleanup via input_keyboard_line_free() */
1216+
callback(NULL);
1217+
1218+
/* Clear our references after callback completes */
1219+
self.keyboardBufferPtr = NULL;
1220+
self.keyboardSizePtr = NULL;
1221+
self.keyboardAllocatedBuffer = NULL;
1222+
}
1223+
}
1224+
}
1225+
11151226
@end
11161227

11171228
ui_companion_driver_t ui_companion_cocoatouch = {
@@ -1135,6 +1246,79 @@ - (UIPointerRegion *)pointerInteraction:(UIPointerInteraction *)interaction
11351246
"cocoatouch",
11361247
};
11371248

1249+
/* C interface for iOS/tvOS native keyboard support */
1250+
bool ios_keyboard_start(char **buffer_ptr, size_t *size_ptr, const char *label,
1251+
input_keyboard_line_complete_t callback, void *userdata)
1252+
{
1253+
RetroArch_iOS *app = [RetroArch_iOS get];
1254+
if (!app || !app.keyboardTextField || !buffer_ptr || !size_ptr)
1255+
return false;
1256+
1257+
/* Allocate a fixed-size buffer for keyboard input */
1258+
char *allocated_buffer = (char *)malloc(512);
1259+
if (!allocated_buffer)
1260+
return false;
1261+
1262+
/* Initialize buffer with existing content if any */
1263+
if (*buffer_ptr && **buffer_ptr)
1264+
strlcpy(allocated_buffer, *buffer_ptr, 512);
1265+
else
1266+
allocated_buffer[0] = '\0';
1267+
1268+
/* Update the keyboard_line buffer pointer to point to our allocated buffer */
1269+
*buffer_ptr = allocated_buffer;
1270+
*size_ptr = strlen(allocated_buffer);
1271+
1272+
/* Store pointers so we can update them as user types */
1273+
app.keyboardBufferPtr = buffer_ptr;
1274+
app.keyboardSizePtr = size_ptr;
1275+
app.keyboardAllocatedBuffer = allocated_buffer;
1276+
1277+
/* Set up the text field with initial text from the buffer */
1278+
app.keyboardTextField.text = (allocated_buffer[0] != '\0') ?
1279+
[NSString stringWithUTF8String:allocated_buffer] : @"";
1280+
1281+
/* Optionally set placeholder from label */
1282+
if (label)
1283+
app.keyboardTextField.placeholder = [NSString stringWithUTF8String:label];
1284+
1285+
/* Store the completion callback */
1286+
app.keyboardCompletionCallback = ^(const char *text) {
1287+
input_driver_state_t *input_st = input_state_get_ptr();
1288+
1289+
if (callback)
1290+
callback(userdata, text);
1291+
1292+
/* Clean up RetroArch's keyboard state, mirroring what the built-in keyboard does */
1293+
if (input_st)
1294+
{
1295+
RARCH_LOG("[iOS KB] cleaning up input state\n");
1296+
input_keyboard_line_free(input_st);
1297+
input_st->flags &= ~INP_FLAG_KB_MAPPING_BLOCKED;
1298+
}
1299+
};
1300+
1301+
/* Show the keyboard */
1302+
[app.keyboardTextField becomeFirstResponder];
1303+
return true;
1304+
}
1305+
1306+
bool ios_keyboard_active(void)
1307+
{
1308+
RetroArch_iOS *app = [RetroArch_iOS get];
1309+
return app && app.keyboardTextField && [app.keyboardTextField isFirstResponder];
1310+
}
1311+
1312+
void ios_keyboard_end(void)
1313+
{
1314+
RetroArch_iOS *app = [RetroArch_iOS get];
1315+
if (app && app.keyboardTextField)
1316+
{
1317+
[app.keyboardTextField resignFirstResponder];
1318+
app.keyboardCompletionCallback = nil;
1319+
}
1320+
}
1321+
11381322
int main(int argc, char *argv[])
11391323
{
11401324
#if TARGET_OS_IOS

0 commit comments

Comments
 (0)