iOS/tvOS: use native keyboard (#18355)

This commit is contained in:
Eric Warmenhoven 2025-11-04 02:50:40 -05:00 committed by GitHub
parent a6d765d959
commit 7d5c1b5085
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 236 additions and 0 deletions

View File

@ -23,6 +23,10 @@
#include "../steam/steam.h"
#endif
#ifdef HAVE_COCOATOUCH
#include "../ui/drivers/cocoa/apple_platform.h"
#endif
/* Standard reference DPI value, used when determining
* DPI-aware scaling factors */
#define REFERENCE_DPI 96.0f
@ -999,6 +1003,10 @@ void gfx_display_draw_keyboard(
if (steam_has_osk_open())
return;
#endif
#ifdef HAVE_COCOATOUCH
if (ios_keyboard_active())
return;
#endif
gfx_display_draw_quad(
p_disp,

View File

@ -82,6 +82,10 @@
#include "../steam/steam.h"
#endif
#ifdef HAVE_COCOATOUCH
#include "../ui/drivers/cocoa/apple_platform.h"
#endif
typedef struct menu_input_ctx_bind
{
char *s;
@ -4547,6 +4551,12 @@ void menu_input_dialog_end(void)
* > Required, since input is ignored for 1 frame
* after certain events - e.g. closing the OSK */
menu_st->input_driver_flushing_input = 2;
#ifdef HAVE_COCOATOUCH
/* Dismiss iOS/tvOS native keyboard if it's currently open */
if (ios_keyboard_active())
ios_keyboard_end();
#endif
}
#if defined(_MSC_VER)
@ -5317,6 +5327,13 @@ unsigned menu_event(
/* > If pointer input is disabled, do nothing */
if (!menu_mouse_enable && !menu_pointer_enable)
menu_input->pointer.type = MENU_POINTER_DISABLED;
#ifdef HAVE_COCOATOUCH
/* > Also disable when keyboard dialog is active to prevent touch events
* from iOS keyboard (e.g., Return button) from being interpreted as
* menu input that could reopen the keyboard */
else if (menu_st->flags & MENU_ST_FLAG_INP_DLG_KB_DISPLAY)
menu_input->pointer.type = MENU_POINTER_DISABLED;
#endif
else
{
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)
if (!line || !menu)
return false;
#ifdef HAVE_COCOATOUCH
/* Prevent reopening keyboard if it's already active
* This can happen when return key events trigger menu OK actions */
if (menu_st->flags & MENU_ST_FLAG_INP_DLG_KB_DISPLAY)
return false;
#endif
#ifdef HAVE_MIST
steam_open_osk();
#endif
@ -8228,6 +8252,17 @@ bool menu_input_dialog_start(menu_input_ctx_line_t *line)
input_keyboard_start_line(menu,
&input_st->keyboard_line,
line->cb);
#ifdef HAVE_COCOATOUCH
/* Use iOS/tvOS native keyboard instead of custom on-screen keyboard */
ios_keyboard_start(
(char **)menu_st->input_dialog_keyboard_buffer,
&input_st->keyboard_line.size,
line->label,
line->cb,
menu);
#endif
/* While reading keyboard line input, we have to block all hotkeys. */
input_st->flags |= INP_FLAG_KB_MAPPING_BLOCKED;

View File

@ -15,6 +15,15 @@ extern void ios_show_file_sheet(void);
extern bool ios_running_on_ipad(void);
#endif
#if TARGET_OS_IPHONE
/* iOS native keyboard support */
typedef void (*input_keyboard_line_complete_t)(void *userdata, const char *line);
extern bool ios_keyboard_start(char **buffer_ptr, size_t *size_ptr, const char *label,
input_keyboard_line_complete_t callback, void *userdata);
extern bool ios_keyboard_active(void);
extern void ios_keyboard_end(void);
#endif
#if TARGET_OS_OSX
extern void osx_show_file_sheet(void);
#endif

View File

@ -32,6 +32,7 @@
#include "../../configuration.h"
#include "../../frontend/frontend.h"
#include "../../input/drivers/cocoa_input.h"
#include "../../input/input_driver.h"
#include "../../input/drivers_keyboard/keyboard_event_apple.h"
#include "../../retroarch.h"
#include "../../tasks/task_content.h"
@ -513,6 +514,14 @@ enum
@end
#endif
@interface RetroArch_iOS () <UITextFieldDelegate>
@property (nonatomic, strong) UITextField *keyboardTextField;
@property (nonatomic, copy) void(^keyboardCompletionCallback)(const char *);
@property (nonatomic, assign) char **keyboardBufferPtr;
@property (nonatomic, assign) size_t *keyboardSizePtr;
@property (nonatomic, assign) char *keyboardAllocatedBuffer;
@end
@implementation RetroArch_iOS
#pragma mark - ApplePlatform
@ -1060,6 +1069,21 @@ enum
[self.window setRootViewController:[CocoaView get]];
/* Initialize hidden keyboard text field for iOS native keyboard support */
if (!self.keyboardTextField)
{
self.keyboardTextField = [[UITextField alloc] initWithFrame:CGRectMake(0, -100, 1, 1)];
self.keyboardTextField.delegate = self;
self.keyboardTextField.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.keyboardTextField.autocorrectionType = UITextAutocorrectionTypeNo;
self.keyboardTextField.spellCheckingType = UITextSpellCheckingTypeNo;
self.keyboardTextField.smartQuotesType = UITextSmartQuotesTypeNo;
self.keyboardTextField.smartDashesType = UITextSmartDashesTypeNo;
self.keyboardTextField.smartInsertDeleteType = UITextSmartInsertDeleteTypeNo;
self.keyboardTextField.returnKeyType = UIReturnKeyDone;
[[CocoaView get].view addSubview:self.keyboardTextField];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
command_event(CMD_EVENT_AUDIO_START, NULL);
});
@ -1112,6 +1136,93 @@ enum
}
#endif
#pragma mark - UITextFieldDelegate (iOS/tvOS Native Keyboard Support)
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (textField != self.keyboardTextField || !self.keyboardAllocatedBuffer || !self.keyboardSizePtr)
return YES;
/* Calculate new text */
NSString *newText = [textField.text stringByReplacingCharactersInRange:range withString:string];
/* Update the RetroArch buffer in real-time so the menu can display it */
const char *utf8Text = [newText UTF8String];
if (utf8Text)
{
strlcpy(self.keyboardAllocatedBuffer, utf8Text, 512);
*self.keyboardSizePtr = strlen(self.keyboardAllocatedBuffer);
}
return YES;
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
if (textField == self.keyboardTextField)
{
/* Update buffer with final text before calling callback */
if (self.keyboardAllocatedBuffer && self.keyboardSizePtr)
{
const char *finalText = [textField.text UTF8String];
if (finalText)
{
strlcpy(self.keyboardAllocatedBuffer, finalText, 512);
*self.keyboardSizePtr = strlen(self.keyboardAllocatedBuffer);
}
}
/* Store callback and buffer before clearing callback reference */
void(^callback)(const char *) = self.keyboardCompletionCallback;
char *buffer = self.keyboardAllocatedBuffer;
/* Clear callback to prevent double-invoke, but keep buffer references
* since the callback will free the buffer via input_keyboard_line_free() */
self.keyboardCompletionCallback = nil;
/* DON'T dismiss keyboard here - let menu_input_dialog_end() -> ios_keyboard_end() do it
* This ensures ios_keyboard_active() returns true when the callback checks it */
/* Call completion callback with buffer pointer
* The callback will call menu_input_dialog_end() which will call ios_keyboard_end() */
if (callback && buffer)
callback(buffer);
/* Clear our references after callback completes */
self.keyboardBufferPtr = NULL;
self.keyboardSizePtr = NULL;
self.keyboardAllocatedBuffer = NULL;
return NO; /* Return NO to prevent UIKit from processing the return key event further */
}
return YES;
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{
if (textField == self.keyboardTextField)
{
/* Only call callback if it wasn't already called (by textFieldShouldReturn) */
if (self.keyboardCompletionCallback)
{
/* User dismissed keyboard without hitting return - treat as cancel */
void(^callback)(const char *) = self.keyboardCompletionCallback;
/* Clear callback to prevent double-invoke, but keep buffer references
* since the callback will free the buffer via input_keyboard_line_free() */
self.keyboardCompletionCallback = nil;
/* Call callback with NULL to indicate cancel
* The callback will handle cleanup via input_keyboard_line_free() */
callback(NULL);
/* Clear our references after callback completes */
self.keyboardBufferPtr = NULL;
self.keyboardSizePtr = NULL;
self.keyboardAllocatedBuffer = NULL;
}
}
}
@end
ui_companion_driver_t ui_companion_cocoatouch = {
@ -1135,6 +1246,79 @@ ui_companion_driver_t ui_companion_cocoatouch = {
"cocoatouch",
};
/* C interface for iOS/tvOS native keyboard support */
bool ios_keyboard_start(char **buffer_ptr, size_t *size_ptr, const char *label,
input_keyboard_line_complete_t callback, void *userdata)
{
RetroArch_iOS *app = [RetroArch_iOS get];
if (!app || !app.keyboardTextField || !buffer_ptr || !size_ptr)
return false;
/* Allocate a fixed-size buffer for keyboard input */
char *allocated_buffer = (char *)malloc(512);
if (!allocated_buffer)
return false;
/* Initialize buffer with existing content if any */
if (*buffer_ptr && **buffer_ptr)
strlcpy(allocated_buffer, *buffer_ptr, 512);
else
allocated_buffer[0] = '\0';
/* Update the keyboard_line buffer pointer to point to our allocated buffer */
*buffer_ptr = allocated_buffer;
*size_ptr = strlen(allocated_buffer);
/* Store pointers so we can update them as user types */
app.keyboardBufferPtr = buffer_ptr;
app.keyboardSizePtr = size_ptr;
app.keyboardAllocatedBuffer = allocated_buffer;
/* Set up the text field with initial text from the buffer */
app.keyboardTextField.text = (allocated_buffer[0] != '\0') ?
[NSString stringWithUTF8String:allocated_buffer] : @"";
/* Optionally set placeholder from label */
if (label)
app.keyboardTextField.placeholder = [NSString stringWithUTF8String:label];
/* Store the completion callback */
app.keyboardCompletionCallback = ^(const char *text) {
input_driver_state_t *input_st = input_state_get_ptr();
if (callback)
callback(userdata, text);
/* Clean up RetroArch's keyboard state, mirroring what the built-in keyboard does */
if (input_st)
{
RARCH_LOG("[iOS KB] cleaning up input state\n");
input_keyboard_line_free(input_st);
input_st->flags &= ~INP_FLAG_KB_MAPPING_BLOCKED;
}
};
/* Show the keyboard */
[app.keyboardTextField becomeFirstResponder];
return true;
}
bool ios_keyboard_active(void)
{
RetroArch_iOS *app = [RetroArch_iOS get];
return app && app.keyboardTextField && [app.keyboardTextField isFirstResponder];
}
void ios_keyboard_end(void)
{
RetroArch_iOS *app = [RetroArch_iOS get];
if (app && app.keyboardTextField)
{
[app.keyboardTextField resignFirstResponder];
app.keyboardCompletionCallback = nil;
}
}
int main(int argc, char *argv[])
{
#if TARGET_OS_IOS