From d4747698786bde74dfc7cd14687d5d9f3f25b39a Mon Sep 17 00:00:00 2001
From: Semphriss <[EMAIL REDACTED]>
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 0ae1bcc99d4ef..1f15b4f719cc1 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 static boolean isVRHeadset() {
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 3353bb7741b92..47a3c3e9b3940 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 d3b3b09c934da..a903b16743081 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 6b532e71bea56..3f0b4c2521a51 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 fd16927595953..b5faa2ff49348 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 05af5c7b89ccd..f461c2c074d95 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 725f62128234c..778de467103ce 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 0c25bff621b6d..1ce6de5fae5ca 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 69c229401ca80..807dbc263504e 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 d31f70a13d572..f5bf626b782dc 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