diff --git a/CMakeLists.txt b/CMakeLists.txt index a86cfb187e..cd08097c50 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2142,6 +2142,7 @@ elseif(UNIX AND NOT (APPLE OR RISCOS OR HAIKU OR CYGWIN)) "${SDL3_SOURCE_DIR}/src/core/linux/SDL_evdev_capabilities.c" "${SDL3_SOURCE_DIR}/src/core/linux/SDL_evdev_capabilities.h" "${SDL3_SOURCE_DIR}/src/core/linux/SDL_threadprio.c" + "${SDL3_SOURCE_DIR}/src/core/linux/SDL_ubuntu_touch.c" ) # src/core/unix/*.c is included in a generic if(UNIX) section, elsewhere. diff --git a/docs/README-ubuntu-touch.md b/docs/README-ubuntu-touch.md new file mode 100644 index 0000000000..fe8926922a --- /dev/null +++ b/docs/README-ubuntu-touch.md @@ -0,0 +1,32 @@ +# Ubuntu Touch / Lomiri + +Ubuntu Touch being similar to Ubuntu desktop, most features should be supported +out-of-the-box with SDL. + +## Developing apps + +Ubuntu Touch apps are developed using [Clickable](https://clickable-ut.dev/). + +Clickable provides an SDL template. It is highly recommended to use the template +as a starting point for both new and existing apps. + +## Considerations + +Ubuntu Touch is similar to the desktop version of Ubuntu, but presents some +differences in behavior. Developers should be wary of the following: + +### SDL_GetPrefPath + +The only allowed writable folder is `~/.local/share//`. +`SDL_GetPrefPath` ignores its arguments and will always return that path on +Ubuntu Touch. No changes are needed in apps. + +### Video driver + +Currently, [a bug](https://github.com/libsdl-org/SDL/issues/12247) forces SDL to +use the Wayland driver on Ubuntu Touch. No changes are needed in apps. + +### Extra functions + +SDL provides `SDL_IsUbuntuTouch()` to differentiate between Ubuntu Touch and +regular Unix, which can help if certain platform-specific tweaks are needed. diff --git a/include/SDL3/SDL_system.h b/include/SDL3/SDL_system.h index 2d3d6e9d80..0f9e72cb3a 100644 --- a/include/SDL3/SDL_system.h +++ b/include/SDL3/SDL_system.h @@ -725,7 +725,8 @@ typedef enum SDL_Sandbox SDL_SANDBOX_UNKNOWN_CONTAINER, SDL_SANDBOX_FLATPAK, SDL_SANDBOX_SNAP, - SDL_SANDBOX_MACOS + SDL_SANDBOX_MACOS, + SDL_SANDBOX_LOMIRI } SDL_Sandbox; /** @@ -903,6 +904,59 @@ extern SDL_DECLSPEC bool SDLCALL SDL_GetGDKDefaultUser(XUserHandle *outUserHandl #endif +/* + * Functions used only with Ubuntu Touch + */ +#ifdef SDL_PLATFORM_LINUX + +/** + * Detect whether the current platform is Ubuntu Touch. + * + * \returns true if the platform is Ubuntu Touch; false otherwise. + * + * \since This function is available since SDL 3.6.0. + */ +extern SDL_DECLSPEC bool SDLCALL SDL_IsUbuntuTouch(void); + +/** + * The ID of the application on Ubuntu Touch, as reported in the manifest. + * + * This is often called the "App Name"; the human-readable name for an app is + * called the "App Title". + * + * This string is needed by some low-level OS features to operate properly. + * + * \since This macro is available since SDL 3.6.0. + * + * \sa SDL_IsUbuntuTouch + */ +#define SDL_PROP_GLOBAL_SYSTEM_UBUNTU_TOUCH_APPID_STRING "SDL.system.ubuntu_touch.appid" + +/** + * The identifier for the specific hook which launched the current executable, + * as reported in the manifest. + * + * This is relevant for application packages that ship multiple applications + * with their desktop files; they will have the same app ID but will differ by + * their hook. + * + * \since This macro is available since SDL 3.6.0. + * + * \sa SDL_IsUbuntuTouch + */ +#define SDL_PROP_GLOBAL_SYSTEM_UBUNTU_TOUCH_HOOK_STRING "SDL.system.ubuntu_touch.hook" + +/** + * The version of the application on Ubuntu Touch, as reported in the manifest. + * + * \since This macro is available since SDL 3.6.0. + * + * \sa SDL_IsUbuntuTouch + */ +#define SDL_PROP_GLOBAL_SYSTEM_UBUNTU_TOUCH_APP_VERSION_STRING "SDL.system.ubuntu_touch.app_version" + +#endif + /* Ends C function definitions when using C++ */ #ifdef __cplusplus } diff --git a/src/SDL.c b/src/SDL.c index a903b16743..b96ee54aa6 100644 --- a/src/SDL.c +++ b/src/SDL.c @@ -169,6 +169,16 @@ const char *SDL_GetAppMetadataProperty(const char *name) value = SDL_GetStringProperty(SDL_GetGlobalProperties(), name, NULL); } if (!value || !*value) { +#ifdef SDL_PLATFORM_LINUX + if (SDL_IsUbuntuTouch()) { + if (SDL_strcmp(name, SDL_PROP_APP_METADATA_IDENTIFIER_STRING) == 0) { + value = SDL_GetStringProperty(SDL_GetGlobalProperties(), SDL_PROP_GLOBAL_SYSTEM_UBUNTU_TOUCH_APPID_STRING, NULL); + } else if (SDL_strcmp(name, SDL_PROP_APP_METADATA_VERSION_STRING) == 0) { + value = SDL_GetStringProperty(SDL_GetGlobalProperties(), SDL_PROP_GLOBAL_SYSTEM_UBUNTU_TOUCH_APP_VERSION_STRING, NULL); + } + } +#endif + if (SDL_strcmp(name, SDL_PROP_APP_METADATA_NAME_STRING) == 0) { value = SDL_GetExeName(); if (!value) { @@ -915,6 +925,12 @@ static SDL_Sandbox SDL_DetectSandbox(void) return SDL_SANDBOX_SNAP; } + /* Ubuntu Touch also supports Snap; check for classic sandboxing only if + * Snap hasn't been detected. */ + if (SDL_getenv("LOMIRI_APPLICATION_ISOLATION") || SDL_getenv("CLICKABLE_DESKTOP_MODE")) { + return SDL_SANDBOX_LOMIRI; + } + if (access("/run/host/container-manager", F_OK) == 0) { return SDL_SANDBOX_UNKNOWN_CONTAINER; } diff --git a/src/SDL_properties.c b/src/SDL_properties.c index 762af38b9b..f91db7de5a 100644 --- a/src/SDL_properties.c +++ b/src/SDL_properties.c @@ -22,6 +22,7 @@ #include "SDL_hints_c.h" #include "SDL_properties_c.h" +#include "core/linux/SDL_ubuntu_touch.h" typedef struct @@ -146,6 +147,14 @@ SDL_PropertiesID SDL_GetGlobalProperties(void) props = SDL_CreateProperties(); // Set global platform properties +#ifdef SDL_PLATFORM_LINUX + if (SDL_IsUbuntuTouch()) { + if (!SDL_SetupUbuntuTouchGlobalProperties(props)) { + SDL_DestroyProperties(props); + return 0; + } + } +#endif if (!SDL_CompareAndSwapAtomicU32(&SDL_global_properties, 0, props)) { // Somebody else created global properties before us, just use those diff --git a/src/core/SDL_core_unsupported.c b/src/core/SDL_core_unsupported.c index 09b675d6ec..7f9dbbe0dc 100644 --- a/src/core/SDL_core_unsupported.c +++ b/src/core/SDL_core_unsupported.c @@ -195,3 +195,12 @@ Sint32 JNI_OnLoad(JavaVM *vm, void *reserved) return 0x00010004; // JNI_VERSION_1_4 } #endif + +#ifndef SDL_PLATFORM_LINUX + +bool SDL_IsUbuntuTouch(void) +{ + return false; +} + +#endif diff --git a/src/core/SDL_core_unsupported.h b/src/core/SDL_core_unsupported.h index b61a9a3a2f..d100836019 100644 --- a/src/core/SDL_core_unsupported.h +++ b/src/core/SDL_core_unsupported.h @@ -65,3 +65,7 @@ extern SDL_DECLSPEC bool SDLCALL SDL_IsChromebook(void); extern SDL_DECLSPEC bool SDLCALL SDL_IsDeXMode(void); extern SDL_DECLSPEC Sint32 SDLCALL JNI_OnLoad(JavaVM *vm, void *reserved); #endif /* !SDL_PLATFORM_ANDROID */ + +#if !defined(SDL_PLATFORM_LINUX) +extern SDL_DECLSPEC bool SDLCALL SDL_IsUbuntuTouch(void); +#endif /* !SDL_PLATFORM_LINUX */ diff --git a/src/core/linux/SDL_system_theme.c b/src/core/linux/SDL_system_theme.c index 100661d6d5..a2a9a7100c 100644 --- a/src/core/linux/SDL_system_theme.c +++ b/src/core/linux/SDL_system_theme.c @@ -24,6 +24,7 @@ #include "SDL_system_theme.h" #include "../../video/SDL_sysvideo.h" +#include #include #define PORTAL_DESTINATION "org.freedesktop.portal.Desktop" @@ -150,7 +151,55 @@ incorrect_type: return true; } +SDL_SystemTheme UbuntuTouch_GetSystemTheme(void) +{ + SDL_SystemTheme theme = SDL_SYSTEM_THEME_UNKNOWN; + FILE *config_file = NULL; + char *line = NULL; + size_t line_alloc = 0; + ssize_t line_size = 0; + bool is_in_general_category = false; + + // "Lomiri": Ubuntu Touch 20.04+ + // "Ubuntu": Ubuntu Touch 16.04 + config_file = fopen("/home/phablet/.config/lomiri-ui-toolkit/theme.ini", "r"); + if (!config_file) { + config_file = fopen("/home/phablet/.config/ubuntu-ui-toolkit/theme.ini", "r"); + if (!config_file) { + return SDL_SYSTEM_THEME_UNKNOWN; + } + } + + while ((line_size = getline(&line, &line_alloc, config_file)) != -1) { + if (line_size >= 1 && line[0] == '[') { + is_in_general_category = SDL_strcmp(line, "[General]\n") == 0; + } else if (is_in_general_category && SDL_strncmp(line, "theme=", 6) == 0) { + if (SDL_strcmp(line, "theme=Lomiri.Components.Themes.SuruDark\n") == 0 || + SDL_strcmp(line, "theme=Ubuntu.Components.Themes.SuruDark\n") == 0) { + theme = SDL_SYSTEM_THEME_DARK; + } else if (SDL_strcmp(line, "theme=Lomiri.Components.Themes.Ambiance\n") == 0 || + SDL_strcmp(line, "theme=Ubuntu.Components.Themes.Ambiance\n") == 0) { + theme = SDL_SYSTEM_THEME_LIGHT; + } else { + theme = SDL_SYSTEM_THEME_UNKNOWN; + } + } + } + + free(line); // This should NOT be SDL_free() + + fclose(config_file); + + return theme; +} + SDL_SystemTheme SDL_SystemTheme_Get(void) { + if (system_theme_data.theme == SDL_SYSTEM_THEME_UNKNOWN) { + // TODO: Use inotify to watch for changes, so that the config file + // doesn't need to be checked each time. + return UbuntuTouch_GetSystemTheme(); + } + return system_theme_data.theme; } diff --git a/src/core/linux/SDL_ubuntu_touch.c b/src/core/linux/SDL_ubuntu_touch.c new file mode 100644 index 0000000000..102b45bcfd --- /dev/null +++ b/src/core/linux/SDL_ubuntu_touch.c @@ -0,0 +1,90 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_PLATFORM_LINUX +#include +#include + +bool SDL_IsUbuntuTouch(void) +{ + static bool checked = false; + static bool is_ubuntu_touch = false; + + if (!checked) { + const char *xdg_session_desktop = SDL_getenv("XDG_SESSION_DESKTOP"); + if (xdg_session_desktop && SDL_strcmp(xdg_session_desktop, "ubuntu-touch") == 0) { + is_ubuntu_touch = true; + } + checked = true; + } + return is_ubuntu_touch; +} + +bool SDL_SetupUbuntuTouchGlobalProperties(SDL_PropertiesID props) +{ + if (!SDL_IsUbuntuTouch()) { + return true; + } + + // Format is __, like myapp.myname_myapp_1.0.0. + // None of those are allowed to have underscores. + const char *app_id = SDL_getenv("APP_ID"); + if (!app_id) { + SDL_SetError("Missing APP_ID"); + return false; + } + + char *buffer = SDL_strdup(app_id); + if (!buffer) { + return false; + } + + char *it = SDL_strchr(buffer, '_'); + if (!*it) { + SDL_SetError("Malformed APP_ID"); + SDL_free(buffer); + return NULL; + } + + char *it2 = SDL_strchr(it + 1, '_'); + if (!*it2) { + SDL_SetError("Malformed APP_ID"); + SDL_free(buffer); + return NULL; + } + + *it++ = '\0'; + *it2++ = '\0'; + + if (!SDL_SetStringProperty(props, SDL_PROP_GLOBAL_SYSTEM_UBUNTU_TOUCH_APPID_STRING, buffer) || + !SDL_SetStringProperty(props, SDL_PROP_GLOBAL_SYSTEM_UBUNTU_TOUCH_HOOK_STRING, it) || + !SDL_SetStringProperty(props, SDL_PROP_GLOBAL_SYSTEM_UBUNTU_TOUCH_APP_VERSION_STRING, it2)) { + SDL_free(buffer); + return false; + } + + SDL_free(buffer); + + return true; +} + +#endif diff --git a/src/core/linux/SDL_ubuntu_touch.h b/src/core/linux/SDL_ubuntu_touch.h new file mode 100644 index 0000000000..70a9feb351 --- /dev/null +++ b/src/core/linux/SDL_ubuntu_touch.h @@ -0,0 +1,27 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_PLATFORM_LINUX + +bool SDL_SetupUbuntuTouchGlobalProperties(SDL_PropertiesID props); + +#endif diff --git a/src/dynapi/SDL_dynapi.exports b/src/dynapi/SDL_dynapi.exports index f461c2c074..87e38965c9 100644 --- a/src/dynapi/SDL_dynapi.exports +++ b/src/dynapi/SDL_dynapi.exports @@ -1300,3 +1300,4 @@ _SDL_ShowNotification _SDL_RemoveNotification _SDL_GetDeviceFormFactor _SDL_GetDeviceFormFactorName +_SDL_IsUbuntuTouch diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 778de46710..607222b2b7 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1301,6 +1301,7 @@ SDL3_0.0.0 { SDL_RemoveNotification; SDL_GetDeviceFormFactor; SDL_GetDeviceFormFactorName; + SDL_IsUbuntuTouch; # 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 1ce6de5fae..b2d767829f 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1327,3 +1327,4 @@ #define SDL_RemoveNotification SDL_RemoveNotification_REAL #define SDL_GetDeviceFormFactor SDL_GetDeviceFormFactor_REAL #define SDL_GetDeviceFormFactorName SDL_GetDeviceFormFactorName_REAL +#define SDL_IsUbuntuTouch SDL_IsUbuntuTouch_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 807dbc2635..884cf2ae96 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1335,3 +1335,4 @@ SDL_DYNAPI_PROC(SDL_NotificationID,SDL_ShowNotification,(const char *a,const cha 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) +SDL_DYNAPI_PROC(bool,SDL_IsUbuntuTouch,(void),(),return) diff --git a/src/filesystem/unix/SDL_sysfilesystem.c b/src/filesystem/unix/SDL_sysfilesystem.c index 8c9259b5f1..127c285200 100644 --- a/src/filesystem/unix/SDL_sysfilesystem.c +++ b/src/filesystem/unix/SDL_sysfilesystem.c @@ -285,6 +285,27 @@ char *SDL_SYS_GetPrefPath(const char *org, const char *app) char *result = NULL; char *ptr = NULL; +#ifdef SDL_PLATFORM_LINUX + if (SDL_IsUbuntuTouch()) { + // On Ubuntu Touch, the only allowed data folder is: + // ~/.local/share// + + SDL_PropertiesID props = SDL_GetGlobalProperties(); + if (!props) { + return NULL; + } + + const char *appid = SDL_GetStringProperty(props, SDL_PROP_GLOBAL_SYSTEM_UBUNTU_TOUCH_APPID_STRING, NULL); + if (!appid) { + SDL_SetError("Ubuntu Touch App ID missing from global properties"); + return NULL; + } + + app = appid; + org = ""; + } +#endif + if (!envr) { // You end up with "$HOME/.local/share/Game Name 2" envr = SDL_getenv("HOME"); diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index 5029fe7072..aed81189e3 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -657,6 +657,12 @@ bool SDL_VideoInit(const char *driver_name) // Select the proper video driver video = NULL; +#ifdef SDL_PLATFORM_LINUX + // https://github.com/libsdl-org/SDL/issues/12247 + if (SDL_IsUbuntuTouch()) { + driver_name = "wayland"; + } +#endif if (!driver_name) { driver_name = SDL_GetHint(SDL_HINT_VIDEO_DRIVER); } diff --git a/test/testsymbols.c b/test/testsymbols.c index cf363e6251..3b5591e6ac 100644 --- a/test/testsymbols.c +++ b/test/testsymbols.c @@ -53,6 +53,7 @@ extern SDL_DECLSPEC void SDLCALL SDL_SetiOSEventPump(void); #if !defined(SDL_PLATFORM_LINUX) extern SDL_DECLSPEC void SDLCALL SDL_SetLinuxThreadPriority(void); extern SDL_DECLSPEC void SDLCALL SDL_SetLinuxThreadPriorityAndPolicy(void); +extern SDL_DECLSPEC bool SDLCALL SDL_IsUbuntuTouch(void); #endif #if !(defined(SDL_PLATFORM_WIN32) || defined(SDL_PLATFORM_WINGDK))