From d4747698786bde74dfc7cd14687d5d9f3f25b39a Mon Sep 17 00:00:00 2001 From: Semphriss <66701383+Semphriss@users.noreply.github.com> Date: Sun, 21 Jun 2026 11:33:13 -0400 Subject: [PATCH] Detect device form factor (#12584) --- .../main/java/org/libsdl/app/SDLActivity.java | 19 ++++++ include/SDL3/SDL_system.h | 58 +++++++++++++++++++ src/SDL.c | 57 +++++++++++++----- src/core/android/SDL_android.c | 36 ++++++++++++ src/core/android/SDL_android.h | 1 + src/dynapi/SDL_dynapi.exports | 2 + src/dynapi/SDL_dynapi.sym | 2 + src/dynapi/SDL_dynapi_overrides.h | 2 + src/dynapi/SDL_dynapi_procs.h | 2 + src/video/uikit/SDL_uikitvideo.m | 24 ++++++-- 10 files changed, 184 insertions(+), 19 deletions(-) diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java index 0ae1bcc99d..1f15b4f719 100644 --- a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -1345,6 +1345,25 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh return false; } + /** + * This method is called by SDL using JNI. + */ + static String getDeviceFormFactor() + { + // TODO: WearOS + if (isAndroidTV()) { + return "tv"; + } else if (isVRHeadset()) { + return "headset"; + } else if (isTablet()) { + return "tablet"; + //} else if (isAndroidAutomotive()) { + // return "car"; + } else { + return "phone"; + } + } + public static double getDiagonal() { DisplayMetrics metrics = new DisplayMetrics(); diff --git a/include/SDL3/SDL_system.h b/include/SDL3/SDL_system.h index 3353bb7741..47a3c3e9b3 100644 --- a/include/SDL3/SDL_system.h +++ b/include/SDL3/SDL_system.h @@ -655,6 +655,64 @@ extern SDL_DECLSPEC bool SDLCALL SDL_IsTablet(void); */ extern SDL_DECLSPEC bool SDLCALL SDL_IsTV(void); +/** + * The possible form factors for a device. + * + * \since This enum is available since SDL 3.4.0. + * + * \sa SDL_GetDeviceFormFactor + * \sa SDL_GetDeviceFormFactorName + */ +typedef enum SDL_FormFactor { + SDL_FORMFACTOR_UNKNOWN = 0, + SDL_FORMFACTOR_DESKTOP, + SDL_FORMFACTOR_LAPTOP, + SDL_FORMFACTOR_PHONE, + SDL_FORMFACTOR_TABLET, + SDL_FORMFACTOR_CONSOLE, + SDL_FORMFACTOR_HANDHELD, + SDL_FORMFACTOR_WATCH, + SDL_FORMFACTOR_TV, + SDL_FORMFACTOR_HEADSET, + SDL_FORMFACTOR_CAR +} SDL_FormFactor; + +/** + * Get the form factor of the current device. + * + * This function guesses what the device may be, but may report inaccurate or + * outright wrong results. For example, it may report a laptop as a desktop, or + * a car device as a phone. + * + * Depending on the usage, there may be different functions better suited for + * each purpose. For example, activating touch controls can be done by detecting + * the presence of a touchscreen rather than restricting to phones and tablets. + * + * \returns the best guess for the form factor of the current device. + * + * \since This function is available since SDL 3.4.0. + * + * \sa SDL_FormFactor + * \sa SDL_GetDeviceFormFactorName + */ +extern SDL_DECLSPEC SDL_FormFactor SDLCALL SDL_GetDeviceFormFactor(void); + +/** + * Get a short name for the current device. + * + * The name will be in English. + * + * \param form_factor the form factor to query. + * \returns a human-readable name for the given form factor, or + * "SDL_FORMFACTOR_UNKNOWN" if the form factor isn't recognized. + * + * \since This function is available since SDL 3.4.0. + * + * \sa SDL_FormFactor + * \sa SDL_GetDeviceFormFactor + */ +extern SDL_DECLSPEC const char* SDLCALL SDL_GetDeviceFormFactorName(SDL_FormFactor form_factor); + /** * Application sandbox environment. * diff --git a/src/SDL.c b/src/SDL.c index d3b3b09c93..a903b16743 100644 --- a/src/SDL.c +++ b/src/SDL.c @@ -849,28 +849,59 @@ bool SDL_IsPhone(void) bool SDL_IsTablet(void) { -#ifdef SDL_PLATFORM_ANDROID - return SDL_IsAndroidTablet(); -#elif defined(SDL_PLATFORM_IOS) - extern bool SDL_IsIPad(void); - return SDL_IsIPad(); -#else - return false; -#endif + return SDL_GetDeviceFormFactor() == SDL_FORMFACTOR_TABLET; } bool SDL_IsTV(void) { -#ifdef SDL_PLATFORM_ANDROID - return SDL_IsAndroidTV(); + return SDL_GetDeviceFormFactor() == SDL_FORMFACTOR_TV; +} + +SDL_FormFactor SDL_GetDeviceFormFactor(void) +{ +#ifdef SDL_FORMFACTOR_PRIVATE + return SDL_FORMFACTOR_PRIVATE; +#elif defined(SDL_PLATFORM_ANDROID) + return SDL_GetAndroidDeviceFormFactor(); #elif defined(SDL_PLATFORM_IOS) - extern bool SDL_IsAppleTV(void); - return SDL_IsAppleTV(); + extern bool SDL_GetUIKitDeviceFormFactor(void); + return SDL_GetUIKitDeviceFormFactor(); +#elif defined(SDL_PLATFORM_XBOXONE) || defined(SDL_PLATFORM_XBOXSERIES) || defined(SDL_PLATFORM_PS2) + return SDL_FORMFACTOR_CONSOLE; +#elif defined(SDL_PLATFORM_PSP) || defined(SDL_PLATFORM_VITA) || defined(SDL_PLATFORM_3DS) + return SDL_FORMFACTOR_HANDHELD; +#elif defined(SDL_PLATFORM_QNXNTO) + /* TODO: QNX is used in BlackBerry phones and tablets, and in many embedded devices */ + return SDL_FORMFACTOR_UNKNOWN; +#elif defined(SDL_PLATFORM_WINGDK) + /* TODO: GDK can be either desktop Windows or XBox */ + return SDL_FORMFACTOR_UNKNOWN; #else - return false; + return SDL_FORMFACTOR_DESKTOP; #endif } +const char* SDL_GetDeviceFormFactorName(SDL_FormFactor form_factor) +{ + switch (form_factor) + { +#define CASE(x) case x: return #x; + default: + CASE(SDL_FORMFACTOR_UNKNOWN) + CASE(SDL_FORMFACTOR_DESKTOP) + CASE(SDL_FORMFACTOR_LAPTOP) + CASE(SDL_FORMFACTOR_PHONE) + CASE(SDL_FORMFACTOR_TABLET) + CASE(SDL_FORMFACTOR_CONSOLE) + CASE(SDL_FORMFACTOR_HANDHELD) + CASE(SDL_FORMFACTOR_WATCH) + CASE(SDL_FORMFACTOR_TV) + CASE(SDL_FORMFACTOR_HEADSET) + CASE(SDL_FORMFACTOR_CAR) +#undef CASE + } +} + static SDL_Sandbox SDL_DetectSandbox(void) { #if defined(SDL_PLATFORM_LINUX) diff --git a/src/core/android/SDL_android.c b/src/core/android/SDL_android.c index 6b532e71be..3f0b4c2521 100644 --- a/src/core/android/SDL_android.c +++ b/src/core/android/SDL_android.c @@ -369,6 +369,7 @@ static jmethodID midClipboardSetText; static jmethodID midCreateCustomCursor; static jmethodID midDestroyCustomCursor; static jmethodID midGetContext; +static jmethodID midGetDeviceFormFactor; static jmethodID midGetManifestEnvironmentVariables; static jmethodID midGetNativeSurface; static jmethodID midInitTouch; @@ -659,6 +660,7 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cl midCreateCustomCursor = (*env)->GetStaticMethodID(env, mActivityClass, "createCustomCursor", "([IIIII)I"); midDestroyCustomCursor = (*env)->GetStaticMethodID(env, mActivityClass, "destroyCustomCursor", "(I)V"); midGetContext = (*env)->GetStaticMethodID(env, mActivityClass, "getContext", "()Landroid/app/Activity;"); + midGetDeviceFormFactor = (*env)->GetStaticMethodID(env, mActivityClass, "getDeviceFormFactor", "()Ljava/lang/String;"); midGetManifestEnvironmentVariables = (*env)->GetStaticMethodID(env, mActivityClass, "getManifestEnvironmentVariables", "()Z"); midGetNativeSurface = (*env)->GetStaticMethodID(env, mActivityClass, "getNativeSurface", "()Landroid/view/Surface;"); midInitTouch = (*env)->GetStaticMethodID(env, mActivityClass, "initTouch", "()V"); @@ -691,6 +693,7 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cl !midCreateCustomCursor || !midDestroyCustomCursor || !midGetContext || + !midGetDeviceFormFactor || !midGetManifestEnvironmentVariables || !midGetNativeSurface || !midInitTouch || @@ -3505,4 +3508,37 @@ bool Android_JNI_ShowFileDialog( return true; } +SDL_FormFactor SDL_GetAndroidDeviceFormFactor(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + SDL_FormFactor form_factor = SDL_FORMFACTOR_UNKNOWN; + jstring string; + + string = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetDeviceFormFactor); + if (string) { + const char *utf = (*env)->GetStringUTFChars(env, string, 0); + if (utf) { + if (SDL_strcmp(utf, "tv") == 0) { + form_factor = SDL_FORMFACTOR_TV; + } else if (SDL_strcmp(utf, "tablet") == 0) { + form_factor = SDL_FORMFACTOR_TABLET; + } else if (SDL_strcmp(utf, "phone") == 0) { + form_factor = SDL_FORMFACTOR_PHONE; + } else if (SDL_strcmp(utf, "car") == 0) { + form_factor = SDL_FORMFACTOR_CAR; + } else if (SDL_strcmp(utf, "headset") == 0) { + form_factor = SDL_FORMFACTOR_HEADSET; + } else if (SDL_strcmp(utf, "watch") == 0) { + form_factor = SDL_FORMFACTOR_WATCH; + } else { + form_factor = SDL_FORMFACTOR_UNKNOWN; + } + (*env)->ReleaseStringUTFChars(env, string, utf); + } + (*env)->DeleteLocalRef(env, string); + } + + return form_factor; +} + #endif // SDL_PLATFORM_ANDROID diff --git a/src/core/android/SDL_android.h b/src/core/android/SDL_android.h index fd16927595..b5faa2ff49 100644 --- a/src/core/android/SDL_android.h +++ b/src/core/android/SDL_android.h @@ -151,6 +151,7 @@ int SDL_GetAndroidSDKVersion(void); bool SDL_IsAndroidTablet(void); bool SDL_IsAndroidTV(void); +SDL_FormFactor SDL_GetAndroidDeviceFormFactor(void); char *SDL_GetAndroidPackageName(void); // this is a SDL_malloc'd string the caller will own. diff --git a/src/dynapi/SDL_dynapi.exports b/src/dynapi/SDL_dynapi.exports index 05af5c7b89..f461c2c074 100644 --- a/src/dynapi/SDL_dynapi.exports +++ b/src/dynapi/SDL_dynapi.exports @@ -1298,3 +1298,5 @@ _SDL_RequestNotificationPermission _SDL_ShowNotificationWithProperties _SDL_ShowNotification _SDL_RemoveNotification +_SDL_GetDeviceFormFactor +_SDL_GetDeviceFormFactorName diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 725f621282..778de46710 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1299,6 +1299,8 @@ SDL3_0.0.0 { SDL_ShowNotificationWithProperties; SDL_ShowNotification; SDL_RemoveNotification; + SDL_GetDeviceFormFactor; + SDL_GetDeviceFormFactorName; # extra symbols go here (don't modify this line) local: *; }; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index 0c25bff621..1ce6de5fae 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1325,3 +1325,5 @@ #define SDL_ShowNotificationWithProperties SDL_ShowNotificationWithProperties_REAL #define SDL_ShowNotification SDL_ShowNotification_REAL #define SDL_RemoveNotification SDL_RemoveNotification_REAL +#define SDL_GetDeviceFormFactor SDL_GetDeviceFormFactor_REAL +#define SDL_GetDeviceFormFactorName SDL_GetDeviceFormFactorName_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 69c229401c..807dbc2635 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1333,3 +1333,5 @@ SDL_DYNAPI_PROC(bool,SDL_RequestNotificationPermission,(void),(),return) SDL_DYNAPI_PROC(SDL_NotificationID,SDL_ShowNotificationWithProperties,(SDL_PropertiesID a),(a),return) SDL_DYNAPI_PROC(SDL_NotificationID,SDL_ShowNotification,(const char *a,const char *b,SDL_Surface *c,SDL_NotificationAction *d,int e),(a,b,c,d,e),return) SDL_DYNAPI_PROC(bool,SDL_RemoveNotification,(SDL_NotificationID a),(a),return) +SDL_DYNAPI_PROC(SDL_FormFactor,SDL_GetDeviceFormFactor,(void),(),return) +SDL_DYNAPI_PROC(const char*,SDL_GetDeviceFormFactorName,(SDL_FormFactor a),(a),return) diff --git a/src/video/uikit/SDL_uikitvideo.m b/src/video/uikit/SDL_uikitvideo.m index d31f70a13d..f5bf626b78 100644 --- a/src/video/uikit/SDL_uikitvideo.m +++ b/src/video/uikit/SDL_uikitvideo.m @@ -384,14 +384,26 @@ void SDL_NSLog(const char *prefix, const char *text) * This doesn't really have anything to do with the interfaces of the SDL video * subsystem, but we need to stuff this into an Objective-C source code file. */ -bool SDL_IsIPad(void) -{ - return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad); -} -bool SDL_IsAppleTV(void) +bool SDL_GetUIKitDeviceFormFactor(void) { - return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomTV); + // TODO: Apple Watch + switch ([UIDevice currentDevice].userInterfaceIdiom) { + case UIUserInterfaceIdiomPhone: + return SDL_FORMFACTOR_PHONE; + case UIUserInterfaceIdiomPad: + return SDL_FORMFACTOR_TABLET; + case UIUserInterfaceIdiomTV: + return SDL_FORMFACTOR_TV; + case UIUserInterfaceIdiomCarPlay: + return SDL_FORMFACTOR_CAR; + case UIUserInterfaceIdiomMac: + return SDL_FORMFACTOR_DESKTOP; + case UIUserInterfaceIdiomVision: + return SDL_FORMFACTOR_HEADSET; + default: + return SDL_FORMFACTOR_UNKNOWN; + } } #endif // SDL_VIDEO_DRIVER_UIKIT