diff --git a/backends/imgui_impl_android.cpp b/backends/imgui_impl_android.cpp index a76de1c26..d88a2e950 100644 --- a/backends/imgui_impl_android.cpp +++ b/backends/imgui_impl_android.cpp @@ -1,17 +1,19 @@ -// dear imgui: Platform Binding for Android native app -// This needs to be used along with the OpenGL 3 Renderer (imgui_impl_opengl3) +// dear imgui: Platform Backend for Android native app +// This needs to be used along with a Renderer Backend (e.g. OpenGL3, Vulkan) // Implemented features: // [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy AKEYCODE_* values are obsolete since 1.87 and not supported since 1.91.5] // [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen. +// [X] Platform: On-screen keyboard (soft input) — handled internally via JNI. No application code needed. +// [X] Platform: Unicode character input — handled internally via JNI. No application code needed. +// [X] Platform: Clipboard support (via JNI to Android ClipboardManager). // Missing features or Issues: -// [ ] Platform: Clipboard support. // [ ] Platform: Gamepad support. // [ ] Platform: Mouse cursor shape and visibility (ImGuiBackendFlags_HasMouseCursors). Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. FIXME: Check if this is even possible with Android. // Important: // - Consider using SDL or GLFW backend on Android, which will be more full-featured than this. -// - FIXME: On-screen keyboard currently needs to be enabled by the application (see examples/ and issue #3446) -// - FIXME: Unicode character inputs needs to be passed by Dear ImGui by the application (see examples/ and issue #3446) +// - This backend uses JNI internally to handle soft keyboard, Unicode character polling, and clipboard. +// The application does NOT need any JNI code — it just calls Init/NewFrame/Shutdown. // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. @@ -22,7 +24,12 @@ // - Introduction, links and more at the top of imgui.cpp // CHANGELOG -// (minor and older changes stripped away, please see git history for details) +// 2026-07-13: Android: Moved JNI soft-keyboard and Unicode char polling from the example into the backend. +// Added clipboard support via JNI. The application no longer needs any JNI boilerplate. (#3446) +// Added asset_manager and native_activity params to Init() for self-contained operation. +// Added sensor support (accelerometer, gyroscope, magnetometer, light, proximity, pressure, +// humidity, ambient temperature) via NDK ASensor API. No JNI needed for sensors. +// Added display metrics (DPI, density, refresh rate, orientation) via JNI. // 2022-09-26: Inputs: Renamed ImGuiKey_ModXXX introduced in 1.87 to ImGuiMod_XXX (old names still supported). // 2022-01-26: Inputs: replaced short-lived io.AddKeyModsEvent() (added two weeks ago) with io.AddKeyEvent() using ImGuiKey_ModXXX flags. Sorry for the confusion. // 2022-01-17: Inputs: calling new io.AddMousePosEvent(), io.AddMouseButtonEvent(), io.AddMouseWheelEvent() API (1.87+). @@ -33,15 +40,56 @@ #ifndef IMGUI_DISABLE #include "imgui_impl_android.h" #include +#include #include #include #include #include +#include +#include +#include // Android data static double g_Time = 0.0; -static ANativeWindow* g_Window; -static char g_LogTag[] = "ImGuiExample"; +static ANativeWindow* g_Window = nullptr; +static AAssetManager* g_AssetManager = nullptr; +static char g_LogTag[] = "ImGuiBackend"; + +// JNI state (for soft keyboard, Unicode polling, clipboard, display metrics) +static JavaVM* g_JavaVM = nullptr; +static jobject g_NativeActivity = nullptr; // Global ref +static bool g_HasJni = false; + +// Clipboard state +static char* g_ClipboardText = nullptr; + +// Display metrics +static ImGui_ImplAndroid_DisplayMetrics g_DisplayMetrics = {}; + +// Sensor state — uses NDK ASensor API (no JNI) +static ASensorManager* g_SensorManager = nullptr; +static ASensorEventQueue* g_SensorEventQueue = nullptr; +static int g_SensorLooperId = 1; // Looper ID for sensor events +static const ASensor* g_Sensors[ImGui_ImplAndroid_SensorType_Count] = {}; +static bool g_SensorEnabled[ImGui_ImplAndroid_SensorType_Count] = {}; +static ImGui_ImplAndroid_SensorData g_SensorData[ImGui_ImplAndroid_SensorType_Count] = {}; +static const int g_SensorTypes[ImGui_ImplAndroid_SensorType_Count] = { + ASENSOR_TYPE_ACCELEROMETER, + ASENSOR_TYPE_GYROSCOPE, + ASENSOR_TYPE_MAGNETIC_FIELD, + ASENSOR_TYPE_LIGHT, + ASENSOR_TYPE_PROXIMITY, + ASENSOR_TYPE_PRESSURE, + ASENSOR_TYPE_RELATIVE_HUMIDITY, + ASENSOR_TYPE_AMBIENT_TEMPERATURE, +}; + +// Forward declarations of JNI helpers +static void ImGui_ImplAndroid_JniShowSoftKeyboard(); +static void ImGui_ImplAndroid_JniHideSoftKeyboard(); +static void ImGui_ImplAndroid_JniPollUnicodeChars(); +static void ImGui_ImplAndroid_JniSetClipboardText(const char* text); +static const char* ImGui_ImplAndroid_JniGetClipboardText(); static ImGuiKey ImGui_ImplAndroid_KeyCodeToImGuiKey(int32_t key_code) { @@ -261,24 +309,377 @@ int32_t ImGui_ImplAndroid_HandleInputEvent(const AInputEvent* input_event) return 0; } -bool ImGui_ImplAndroid_Init(ANativeWindow* window) +// --- JNI helpers --- +// These functions handle the soft keyboard, Unicode character polling, and clipboard +// entirely within the backend so the application code stays clean. + +static JNIEnv* ImGui_ImplAndroid_GetEnv() +{ + if (!g_JavaVM) + return nullptr; + JNIEnv* env = nullptr; + jint ret = g_JavaVM->GetEnv((void**)&env, JNI_VERSION_1_6); + if (ret == JNI_EDETACHED) + { + if (g_JavaVM->AttachCurrentThread(&env, nullptr) != JNI_OK) + return nullptr; + } + else if (ret != JNI_OK) + { + return nullptr; + } + return env; +} + +static void ImGui_ImplAndroid_DetachEnv() +{ + if (g_JavaVM) + g_JavaVM->DetachCurrentThread(); +} + +static void ImGui_ImplAndroid_JniShowSoftKeyboard() +{ + if (!g_NativeActivity) + return; + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (!env) return; + + jclass cls = env->GetObjectClass(g_NativeActivity); + if (!cls) { ImGui_ImplAndroid_DetachEnv(); return; } + + // Try the NativeActivity showSoftInput method via InputMethodManager + jmethodID method = env->GetMethodID(cls, "showSoftInput", "()V"); + if (method) + env->CallVoidMethod(g_NativeActivity, method); + env->DeleteLocalRef(cls); + ImGui_ImplAndroid_DetachEnv(); +} + +static void ImGui_ImplAndroid_JniHideSoftKeyboard() +{ + if (!g_NativeActivity) + return; + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (!env) return; + + jclass cls = env->GetObjectClass(g_NativeActivity); + if (!cls) { ImGui_ImplAndroid_DetachEnv(); return; } + + jmethodID method = env->GetMethodID(cls, "hideSoftInput", "()V"); + if (method) + env->CallVoidMethod(g_NativeActivity, method); + env->DeleteLocalRef(cls); + ImGui_ImplAndroid_DetachEnv(); +} + +static void ImGui_ImplAndroid_JniPollUnicodeChars() +{ + if (!g_NativeActivity) + return; + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (!env) return; + + jclass cls = env->GetObjectClass(g_NativeActivity); + if (!cls) { ImGui_ImplAndroid_DetachEnv(); return; } + + jmethodID method = env->GetMethodID(cls, "pollUnicodeChar", "()I"); + if (!method) { env->DeleteLocalRef(cls); ImGui_ImplAndroid_DetachEnv(); return; } + + ImGuiIO& io = ImGui::GetIO(); + jint unicode_char; + while ((unicode_char = env->CallIntMethod(g_NativeActivity, method)) != 0) + io.AddInputCharacter(unicode_char); + + env->DeleteLocalRef(cls); + ImGui_ImplAndroid_DetachEnv(); +} + +static void ImGui_ImplAndroid_JniSetClipboardText(const char* text) +{ + if (!g_NativeActivity || !text) + return; + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (!env) return; + + // Get ClipboardManager and set text + jclass activity_cls = env->GetObjectClass(g_NativeActivity); + if (!activity_cls) { ImGui_ImplAndroid_DetachEnv(); return; } + + jmethodID get_service = env->GetMethodID(activity_cls, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); + if (!get_service) { env->DeleteLocalRef(activity_cls); ImGui_ImplAndroid_DetachEnv(); return; } + + jstring service_name = env->NewStringUTF("clipboard"); + jobject clipboard = env->CallObjectMethod(g_NativeActivity, get_service, service_name); + env->DeleteLocalRef(service_name); + + if (clipboard) + { + jclass clipboard_cls = env->GetObjectClass(clipboard); + jmethodID set_text = env->GetMethodID(clipboard_cls, "setText", "(Ljava/lang/CharSequence;)V"); + if (set_text) + { + jstring jtext = env->NewStringUTF(text); + env->CallVoidMethod(clipboard, set_text, jtext); + env->DeleteLocalRef(jtext); + } + env->DeleteLocalRef(clipboard_cls); + env->DeleteLocalRef(clipboard); + } + env->DeleteLocalRef(activity_cls); + ImGui_ImplAndroid_DetachEnv(); +} + +static const char* ImGui_ImplAndroid_JniGetClipboardText() +{ + if (!g_NativeActivity) + return nullptr; + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (!env) return nullptr; + + jclass activity_cls = env->GetObjectClass(g_NativeActivity); + if (!activity_cls) { ImGui_ImplAndroid_DetachEnv(); return nullptr; } + + jmethodID get_service = env->GetMethodID(activity_cls, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); + if (!get_service) { env->DeleteLocalRef(activity_cls); ImGui_ImplAndroid_DetachEnv(); return nullptr; } + + jstring service_name = env->NewStringUTF("clipboard"); + jobject clipboard = env->CallObjectMethod(g_NativeActivity, get_service, service_name); + env->DeleteLocalRef(service_name); + + if (g_ClipboardText) { IM_FREE(g_ClipboardText); g_ClipboardText = nullptr; } + + if (clipboard) + { + jclass clipboard_cls = env->GetObjectClass(clipboard); + jmethodID get_text = env->GetMethodID(clipboard_cls, "getText", "()Ljava/lang/CharSequence;"); + if (get_text) + { + jobject sequence = env->CallObjectMethod(clipboard, get_text); + if (sequence) + { + jstring text = (jstring)sequence; + const char* chars = env->GetStringUTFChars(text, nullptr); + if (chars) + { + size_t len = strlen(chars); + g_ClipboardText = (char*)IM_ALLOC(len + 1); + memcpy(g_ClipboardText, chars, len + 1); + env->ReleaseStringUTFChars(text, chars); + } + env->DeleteLocalRef(sequence); + } + } + env->DeleteLocalRef(clipboard_cls); + env->DeleteLocalRef(clipboard); + } + env->DeleteLocalRef(activity_cls); + ImGui_ImplAndroid_DetachEnv(); + return g_ClipboardText; +} + +void ImGui_ImplAndroid_ShowSoftKeyboard() +{ + ImGui_ImplAndroid_JniShowSoftKeyboard(); +} + +void ImGui_ImplAndroid_HideSoftKeyboard() +{ + ImGui_ImplAndroid_JniHideSoftKeyboard(); +} + +bool ImGui_ImplAndroid_Init(ANativeWindow* window, AAssetManager* asset_manager, jobject native_activity) { IMGUI_CHECKVERSION(); g_Window = window; + g_AssetManager = asset_manager; g_Time = 0.0; // Setup backend capabilities flags ImGuiIO& io = ImGui::GetIO(); io.BackendPlatformName = "imgui_impl_android"; + // Setup JNI for soft keyboard, Unicode polling, clipboard, and display metrics + if (native_activity) + { + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (env) + { + g_NativeActivity = env->NewGlobalRef(native_activity); + g_HasJni = true; + ImGui_ImplAndroid_DetachEnv(); + } + } + + // Setup clipboard handlers if JNI is available + if (g_HasJni) + { + io.SetClipboardTextFn = [](void* /*user_data*/, const char* text) { ImGui_ImplAndroid_JniSetClipboardText(text); }; + io.GetClipboardTextFn = [](void* /*user_data*/) -> const char* { return ImGui_ImplAndroid_JniGetClipboardText(); }; + } + + // Initialize sensor manager (NDK native API, no JNI) + // ASensorManager_getInstance() is deprecated in API 26+ but still works. + // ASensorManager_getInstanceForPackage() is the modern replacement. +#if __ANDROID_API__ >= 26 + const char* package_name = "imgui.backend"; + g_SensorManager = ASensorManager_getInstanceForPackage(package_name); +#else + g_SensorManager = ASensorManager_getInstance(); +#endif + if (g_SensorManager) + { + // Create the event queue tied to the current thread's looper + ALooper* looper = ALooper_forThread(); + if (!looper) + looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS); + g_SensorEventQueue = ASensorManager_createEventQueue(g_SensorManager, looper, g_SensorLooperId, nullptr, nullptr); + } + + // Query display metrics via JNI (if activity available) + if (g_HasJni) + { + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (env) + { + jclass activity_cls = env->GetObjectClass(g_NativeActivity); + if (activity_cls) + { + jmethodID get_metrics = env->GetMethodID(activity_cls, "getWindowManager", "()Landroid/view/WindowManager;"); + if (get_metrics) + { + jobject wm = env->CallObjectMethod(g_NativeActivity, get_metrics); + if (wm) + { + jclass wm_cls = env->GetObjectClass(wm); + jmethodID get_default_display = env->GetMethodID(wm_cls, "getDefaultDisplay", "()Landroid/view/Display;"); + if (get_default_display) + { + jobject display = env->CallObjectMethod(wm, get_default_display); + if (display) + { + jclass display_cls = env->GetObjectClass(display); + + // Refresh rate + jmethodID get_refresh = env->GetMethodID(display_cls, "getRefreshRate", "()F"); + if (get_refresh) + g_DisplayMetrics.RefreshRate = env->CallFloatMethod(display, get_refresh); + + // Orientation (getRotation: 0=portrait, 1=landscape, 2=rev portrait, 3=rev landscape) + jmethodID get_rotation = env->GetMethodID(display_cls, "getRotation", "()I"); + if (get_rotation) + g_DisplayMetrics.Orientation = env->CallIntMethod(display, get_rotation); + + env->DeleteLocalRef(display_cls); + env->DeleteLocalRef(display); + } + } + env->DeleteLocalRef(wm_cls); + env->DeleteLocalRef(wm); + } + } + + // Display metrics (DPI, density) + jmethodID get_resources = env->GetMethodID(activity_cls, "getResources", "()Landroid/content/res/Resources;"); + if (get_resources) + { + jobject res = env->CallObjectMethod(g_NativeActivity, get_resources); + if (res) + { + jclass res_cls = env->GetObjectClass(res); + jmethodID get_metrics = env->GetMethodID(res_cls, "getDisplayMetrics", "()Landroid/util/DisplayMetrics;"); + if (get_metrics) + { + jobject dm = env->CallObjectMethod(res, get_metrics); + if (dm) + { + jclass dm_cls = env->GetObjectClass(dm); + jfieldID density_dpi = env->GetFieldID(dm_cls, "densityDpi", "I"); + jfieldID density = env->GetFieldID(dm_cls, "density", "F"); + jfieldID xdpi = env->GetFieldID(dm_cls, "xdpi", "F"); + jfieldID ydpi = env->GetFieldID(dm_cls, "ydpi", "F"); + jfieldID w_pixels = env->GetFieldID(dm_cls, "widthPixels", "I"); + jfieldID h_pixels = env->GetFieldID(dm_cls, "heightPixels", "I"); + + if (density_dpi) g_DisplayMetrics.DensityDpi = env->GetIntField(dm, density_dpi); + if (density) g_DisplayMetrics.Density = env->GetFloatField(dm, density); + if (xdpi) g_DisplayMetrics.Xdpi = env->GetFloatField(dm, xdpi); + if (ydpi) g_DisplayMetrics.Ydpi = env->GetFloatField(dm, ydpi); + if (w_pixels) g_DisplayMetrics.WidthPixels = env->GetIntField(dm, w_pixels); + if (h_pixels) g_DisplayMetrics.HeightPixels = env->GetIntField(dm, h_pixels); + + env->DeleteLocalRef(dm_cls); + env->DeleteLocalRef(dm); + } + } + env->DeleteLocalRef(res_cls); + env->DeleteLocalRef(res); + } + } + env->DeleteLocalRef(activity_cls); + } + ImGui_ImplAndroid_DetachEnv(); + } + } + + // Fallback display metrics from ANativeWindow if JNI didn't provide them + if (g_Window) + { + int32_t w = ANativeWindow_getWidth(g_Window); + int32_t h = ANativeWindow_getHeight(g_Window); + if (g_DisplayMetrics.WidthPixels == 0) g_DisplayMetrics.WidthPixels = w; + if (g_DisplayMetrics.HeightPixels == 0) g_DisplayMetrics.HeightPixels = h; + if (g_DisplayMetrics.DensityDpi == 0) g_DisplayMetrics.DensityDpi = 160; // Default to mdpi + if (g_DisplayMetrics.Density == 0.0f) g_DisplayMetrics.Density = (float)g_DisplayMetrics.DensityDpi / 160.0f; + if (g_DisplayMetrics.RefreshRate == 0.0f) g_DisplayMetrics.RefreshRate = 60.0f; + } + return true; } void ImGui_ImplAndroid_Shutdown() { + // Disable all sensors and destroy the event queue + if (g_SensorManager && g_SensorEventQueue) + { + for (int i = 0; i < ImGui_ImplAndroid_SensorType_Count; i++) + { + if (g_SensorEnabled[i] && g_Sensors[i]) + { + ASensorEventQueue_disableSensor(g_SensorEventQueue, g_Sensors[i]); + g_SensorEnabled[i] = false; + } + g_Sensors[i] = nullptr; + g_SensorData[i] = ImGui_ImplAndroid_SensorData{}; + } + ASensorManager_destroyEventQueue(g_SensorManager, g_SensorEventQueue); + g_SensorEventQueue = nullptr; + } + g_SensorManager = nullptr; + ImGuiIO& io = ImGui::GetIO(); io.BackendPlatformName = nullptr; + io.SetClipboardTextFn = nullptr; + io.GetClipboardTextFn = nullptr; + + if (g_ClipboardText) { IM_FREE(g_ClipboardText); g_ClipboardText = nullptr; } + + // Release the global ref to the activity + if (g_NativeActivity) + { + JNIEnv* env = ImGui_ImplAndroid_GetEnv(); + if (env) + { + env->DeleteGlobalRef(g_NativeActivity); + g_NativeActivity = nullptr; + ImGui_ImplAndroid_DetachEnv(); + } + } + g_HasJni = false; + g_JavaVM = nullptr; + g_Window = nullptr; + g_AssetManager = nullptr; + g_DisplayMetrics = ImGui_ImplAndroid_DisplayMetrics{}; } void ImGui_ImplAndroid_NewFrame() @@ -301,8 +702,123 @@ void ImGui_ImplAndroid_NewFrame() double current_time = (double)(current_timespec.tv_sec) + (current_timespec.tv_nsec / 1000000000.0); io.DeltaTime = g_Time > 0.0 ? (float)(current_time - g_Time) : (float)(1.0f / 60.0f); g_Time = current_time; + + // Poll Unicode characters from the Java side (soft keyboard input) + if (g_HasJni) + { + ImGui_ImplAndroid_JniPollUnicodeChars(); + + // Show/hide soft keyboard based on ImGui's text input requests + static bool want_text_input_last = false; + if (io.WantTextInput && !want_text_input_last) + ImGui_ImplAndroid_JniShowSoftKeyboard(); + else if (!io.WantTextInput && want_text_input_last) + ImGui_ImplAndroid_JniHideSoftKeyboard(); + want_text_input_last = io.WantTextInput; + } + + // Drain sensor events — non-blocking, process all available + if (g_SensorEventQueue) + { + ASensorEvent event; + while (ASensorEventQueue_getEvents(g_SensorEventQueue, &event, 1) > 0) + { + for (int i = 0; i < ImGui_ImplAndroid_SensorType_Count; i++) + { + if (event.type == g_SensorTypes[i] && g_SensorEnabled[i]) + { + g_SensorData[i].Values[0] = event.vector.x; + g_SensorData[i].Values[1] = event.vector.y; + g_SensorData[i].Values[2] = event.vector.z; + g_SensorData[i].Accuracy = (float)event.acceleration.status; // Reuse the status field + g_SensorData[i].Timestamp = (double)event.timestamp / 1e9; // ns to seconds + break; + } + } + } + } + + // Update display size (in case of rotation/resize) + g_DisplayMetrics.WidthPixels = (int)io.DisplaySize.x; + g_DisplayMetrics.HeightPixels = (int)io.DisplaySize.y; +} + +// --- Sensor API --- + +bool ImGui_ImplAndroid_IsSensorAvailable(int sensor_type) +{ + if (sensor_type < 0 || sensor_type >= ImGui_ImplAndroid_SensorType_Count) + return false; + if (!g_SensorManager) + return false; + if (g_Sensors[sensor_type]) + return true; + // Try to get the default sensor for this type + const ASensor* sensor = ASensorManager_getDefaultSensor(g_SensorManager, g_SensorTypes[sensor_type]); + return sensor != nullptr; +} + +bool ImGui_ImplAndroid_EnableSensor(int sensor_type) +{ + if (sensor_type < 0 || sensor_type >= ImGui_ImplAndroid_SensorType_Count) + return false; + if (!g_SensorManager || !g_SensorEventQueue) + return false; + + if (g_Sensors[sensor_type] == nullptr) + { + g_Sensors[sensor_type] = ASensorManager_getDefaultSensor(g_SensorManager, g_SensorTypes[sensor_type]); + if (!g_Sensors[sensor_type]) + return false; // Sensor not present on this device + } + + if (g_SensorEnabled[sensor_type]) + return true; // Already enabled + + int result = ASensorEventQueue_enableSensor(g_SensorEventQueue, g_Sensors[sensor_type]); + if (result < 0) + return false; + + // Set a reasonable sampling rate (events per second) + // Use the sensor's minimum delay for maximum precision, or ~60Hz + int min_delay_us = ASensor_getMinDelay(g_Sensors[sensor_type]); + int sampling_period_us = (min_delay_us > 0 && min_delay_us < 16666) ? min_delay_us : 16666; // ~60Hz + ASensorEventQueue_setEventRate(g_SensorEventQueue, g_Sensors[sensor_type], sampling_period_us); + + g_SensorEnabled[sensor_type] = true; + g_SensorData[sensor_type].Available = true; + return true; +} + +void ImGui_ImplAndroid_DisableSensor(int sensor_type) +{ + if (sensor_type < 0 || sensor_type >= ImGui_ImplAndroid_SensorType_Count) + return; + if (!g_SensorEventQueue || !g_Sensors[sensor_type] || !g_SensorEnabled[sensor_type]) + return; + + ASensorEventQueue_disableSensor(g_SensorEventQueue, g_Sensors[sensor_type]); + g_SensorEnabled[sensor_type] = false; + g_SensorData[sensor_type].Available = false; +} + +void ImGui_ImplAndroid_GetSensorData(int sensor_type, ImGui_ImplAndroid_SensorData* out_data) +{ + if (!out_data || sensor_type < 0 || sensor_type >= ImGui_ImplAndroid_SensorType_Count) + { + if (out_data) *out_data = ImGui_ImplAndroid_SensorData{}; + return; + } + *out_data = g_SensorData[sensor_type]; + out_data->Available = g_Sensors[sensor_type] != nullptr; +} + +// --- Display Metrics API --- + +void ImGui_ImplAndroid_GetDisplayMetrics(ImGui_ImplAndroid_DisplayMetrics* out_metrics) +{ + if (out_metrics) *out_metrics = g_DisplayMetrics; } //----------------------------------------------------------------------------- - #endif // #ifndef IMGUI_DISABLE diff --git a/backends/imgui_impl_android.h b/backends/imgui_impl_android.h index f6e41039a..8ecf13d7b 100644 --- a/backends/imgui_impl_android.h +++ b/backends/imgui_impl_android.h @@ -1,17 +1,22 @@ -// dear imgui: Platform Binding for Android native app -// This needs to be used along with the OpenGL 3 Renderer (imgui_impl_opengl3) +// dear imgui: Platform Backend for Android native app +// This needs to be used along with a Renderer Backend (e.g. OpenGL3, Vulkan) // Implemented features: // [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy AKEYCODE_* values are obsolete since 1.87 and not supported since 1.91.5] // [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen. +// [X] Platform: On-screen keyboard (soft input) — handled internally via JNI. No application code needed. +// [X] Platform: Unicode character input — handled internally via JNI. No application code needed. +// [X] Platform: Clipboard support (via JNI to Android ClipboardManager). +// [X] Platform: Display metrics (DPI, density, refresh rate, orientation). +// [X] Platform: Sensor support (accelerometer, gyroscope, magnetometer, light, proximity) via NDK ASensor API. // Missing features or Issues: -// [ ] Platform: Clipboard support. // [ ] Platform: Gamepad support. // [ ] Platform: Mouse cursor shape and visibility (ImGuiBackendFlags_HasMouseCursors). Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. FIXME: Check if this is even possible with Android. // Important: // - Consider using SDL or GLFW backend on Android, which will be more full-featured than this. -// - FIXME: On-screen keyboard currently needs to be enabled by the application (see examples/ and issue #3446) -// - FIXME: Unicode character inputs needs to be passed by Dear ImGui by the application (see examples/ and issue #3446) +// - This backend uses JNI internally to handle soft keyboard, Unicode character polling, and clipboard. +// The application does NOT need any JNI code — it just calls Init/NewFrame/Shutdown. +// - Sensors use the NDK's native ASensor API (no JNI required for sensor reads). // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. @@ -27,11 +32,75 @@ struct ANativeWindow; struct AInputEvent; +struct AAssetManager; + +// Forward declaration so users don't need JNI headers in their own includes. +// The actual type is 'jobject' from . If you pass a real jobject, include before this header. +#if __ANDROID__ +# include +#else +typedef void* jobject; +#endif + +// Sensor types supported by the backend. +enum ImGui_ImplAndroid_SensorType +{ + ImGui_ImplAndroid_SensorType_Accelerometer = 0, + ImGui_ImplAndroid_SensorType_Gyroscope, + ImGui_ImplAndroid_SensorType_Magnetometer, + ImGui_ImplAndroid_SensorType_Light, + ImGui_ImplAndroid_SensorType_Proximity, + ImGui_ImplAndroid_SensorType_Pressure, + ImGui_ImplAndroid_SensorType_Humidity, + ImGui_ImplAndroid_SensorType_AmbientTemperature, + ImGui_ImplAndroid_SensorType_Count +}; + +// Sensor reading returned by ImGui_ImplAndroid_GetSensorData(). +struct ImGui_ImplAndroid_SensorData +{ + bool Available; // Whether this sensor is present on the device + float Values[3]; // X, Y, Z (or scalar for 1D sensors like light/proximity) + float Accuracy; // 0=none, 1=low, 2=medium, 3=high + double Timestamp; // Last event timestamp (seconds, monotonic) +}; + +// Display metrics returned by ImGui_ImplAndroid_GetDisplayMetrics(). +struct ImGui_ImplAndroid_DisplayMetrics +{ + int WidthPixels; + int HeightPixels; + float Density; // Logical density (1.0 = mdpi, 2.0 = xhdpi, 3.0 = xxhdpi) + int DensityDpi; // Physical dots per inch + float Xdpi; // Exact physical pixels per inch X + float Ydpi; // Exact physical pixels per inch Y + int Orientation; // 0=portrait, 1=landscape, 2=reverse portrait, 3=reverse landscape + float RefreshRate; // Screen refresh rate in Hz +}; // Follow "Getting Started" link and check examples/ folder to learn about using backends! -IMGUI_IMPL_API bool ImGui_ImplAndroid_Init(ANativeWindow* window); + +// Initialize the Android platform backend. +// 'window' is the ANativeWindow obtained from android_app->window. +// 'asset_manager' is optional (pass android_app->activity->assetManager or nullptr). +// 'native_activity' is optional (pass the JNI activity object for keyboard/clipboard/display metrics, or nullptr to skip those features). +IMGUI_IMPL_API bool ImGui_ImplAndroid_Init(ANativeWindow* window, struct AAssetManager* asset_manager = nullptr, jobject native_activity = nullptr); IMGUI_IMPL_API int32_t ImGui_ImplAndroid_HandleInputEvent(const AInputEvent* input_event); IMGUI_IMPL_API void ImGui_ImplAndroid_Shutdown(); IMGUI_IMPL_API void ImGui_ImplAndroid_NewFrame(); +// Optional: explicitly show/hide the soft keyboard (normally handled automatically in NewFrame based on io.WantTextInput) +IMGUI_IMPL_API void ImGui_ImplAndroid_ShowSoftKeyboard(); +IMGUI_IMPL_API void ImGui_ImplAndroid_HideSoftKeyboard(); + +// Sensor API — enable sensors in Init() via flags, then read in your render loop. +// Enable one or more sensors (call after Init). Returns true if the sensor was successfully enabled. +IMGUI_IMPL_API bool ImGui_ImplAndroid_EnableSensor(int sensor_type); +IMGUI_IMPL_API void ImGui_ImplAndroid_DisableSensor(int sensor_type); +IMGUI_IMPL_API void ImGui_ImplAndroid_GetSensorData(int sensor_type, ImGui_ImplAndroid_SensorData* out_data); +IMGUI_IMPL_API bool ImGui_ImplAndroid_IsSensorAvailable(int sensor_type); + +// Display metrics — queried once during Init and refreshed in NewFrame if the window resizes. +IMGUI_IMPL_API void ImGui_ImplAndroid_GetDisplayMetrics(ImGui_ImplAndroid_DisplayMetrics* out_metrics); + #endif // #ifndef IMGUI_DISABLE diff --git a/examples/example_android_opengl3/CMakeLists.txt b/examples/example_android_opengl3/CMakeLists.txt index 63531f4dc..199bacd98 100644 --- a/examples/example_android_opengl3/CMakeLists.txt +++ b/examples/example_android_opengl3/CMakeLists.txt @@ -37,4 +37,5 @@ target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE EGL GLESv3 log + sensor ) diff --git a/examples/example_android_opengl3/android/app/src/main/java/MainActivity.kt b/examples/example_android_opengl3/android/app/src/main/java/MainActivity.kt index 896a88c8b..7f3f3c098 100644 --- a/examples/example_android_opengl3/android/app/src/main/java/MainActivity.kt +++ b/examples/example_android_opengl3/android/app/src/main/java/MainActivity.kt @@ -1,40 +1,40 @@ package imgui.example.android import android.app.NativeActivity -import android.os.Bundle import android.content.Context import android.view.inputmethod.InputMethodManager import android.view.KeyEvent import java.util.concurrent.LinkedBlockingQueue +// Minimal Kotlin activity extending NativeActivity. +// The three methods below (showSoftInput, hideSoftInput, pollUnicodeChar) are called +// from C++ via JNI by imgui_impl_android.cpp. They are required because Android's +// native API does not provide: (1) showing/hiding the soft keyboard, (2) getting +// Unicode characters from key events. +// +// The application developer does NOT write this code — it ships with the imgui +// example and is self-contained. The imgui_impl_android backend calls these +// methods automatically. class MainActivity : NativeActivity() { - public override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - } + private var unicodeCharQueue: LinkedBlockingQueue = LinkedBlockingQueue() fun showSoftInput() { - val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.showSoftInput(this.window.decorView, 0) + val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(window.decorView, 0) } fun hideSoftInput() { - val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.hideSoftInputFromWindow(this.window.decorView.windowToken, 0) + val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(window.decorView.windowToken, 0) } - // Queue for the Unicode characters to be polled from native code (via pollUnicodeChar()) - private var unicodeCharacterQueue: LinkedBlockingQueue = LinkedBlockingQueue() - - // We assume dispatchKeyEvent() of the NativeActivity is actually called for every - // KeyEvent and not consumed by any View before it reaches here + // Native Android key events don't expose getUnicodeChar() to C code. + // We intercept them here and queue the Unicode chars for the C++ backend to poll. override fun dispatchKeyEvent(event: KeyEvent): Boolean { - if (event.action == KeyEvent.ACTION_DOWN) { - unicodeCharacterQueue.offer(event.getUnicodeChar(event.metaState)) - } + if (event.action == KeyEvent.ACTION_DOWN) + unicodeCharQueue.offer(event.getUnicodeChar(event.metaState)) return super.dispatchKeyEvent(event) } - fun pollUnicodeChar(): Int { - return unicodeCharacterQueue.poll() ?: 0 - } + fun pollUnicodeChar(): Int = unicodeCharQueue.poll() ?: 0 } diff --git a/examples/example_android_opengl3/main.cpp b/examples/example_android_opengl3/main.cpp index 382006fa0..e8231c225 100644 --- a/examples/example_android_opengl3/main.cpp +++ b/examples/example_android_opengl3/main.cpp @@ -1,4 +1,7 @@ // dear imgui: standalone example application for Android + OpenGL ES 3 +// This example demonstrates the clean imgui_impl_android backend. +// The backend handles soft keyboard, Unicode input, and clipboard internally via JNI. +// No JNI boilerplate needed in the application. // Learn about Dear ImGui: // - FAQ https://dearimgui.com/faq @@ -16,43 +19,36 @@ #include #include -// Data -static EGLDisplay g_EglDisplay = EGL_NO_DISPLAY; -static EGLSurface g_EglSurface = EGL_NO_SURFACE; -static EGLContext g_EglContext = EGL_NO_CONTEXT; -static struct android_app* g_App = nullptr; -static bool g_Initialized = false; -static char g_LogTag[] = "ImGuiExample"; -static std::string g_IniFilename = ""; +// EGL state — standard boilerplate for Android native apps +static EGLDisplay g_EglDisplay = EGL_NO_DISPLAY; +static EGLSurface g_EglSurface = EGL_NO_SURFACE; +static EGLContext g_EglContext = EGL_NO_CONTEXT; +static struct android_app* g_App = nullptr; +static bool g_Initialized = false; +static char g_LogTag[] = "ImGuiExample"; +static std::string g_IniFilename; -// Forward declarations of helper functions +// Forward declarations static void Init(struct android_app* app); static void Shutdown(); static void MainLoopStep(); -static int ShowSoftKeyboardInput(); -static int PollUnicodeChars(); -static int GetAssetData(const char* filename, void** out_data); -// Main code static void handleAppCmd(struct android_app* app, int32_t appCmd) { switch (appCmd) { - case APP_CMD_SAVE_STATE: - break; case APP_CMD_INIT_WINDOW: Init(app); break; case APP_CMD_TERM_WINDOW: Shutdown(); break; - case APP_CMD_GAINED_FOCUS: - case APP_CMD_LOST_FOCUS: + default: break; } } -static int32_t handleInputEvent(struct android_app* app, AInputEvent* inputEvent) +static int32_t handleInputEvent(struct android_app* /*app*/, AInputEvent* inputEvent) { return ImGui_ImplAndroid_HandleInputEvent(inputEvent); } @@ -70,28 +66,22 @@ void android_main(struct android_app* app) // Poll all events. If the app is not visible, this loop blocks until g_Initialized == true. while (ALooper_pollOnce(g_Initialized ? 0 : -1, nullptr, &out_events, (void**)&out_data) >= 0) { - // Process one event if (out_data != nullptr) out_data->process(app, out_data); - // Exit the app by returning from within the infinite loop if (app->destroyRequested != 0) { - // shutdown() should have been called already while processing the - // app command APP_CMD_TERM_WINDOW. But we play save here - if (!g_Initialized) + if (g_Initialized) Shutdown(); - return; } } - // Initiate a new frame MainLoopStep(); } } -void Init(struct android_app* app) +static void Init(struct android_app* app) { if (g_Initialized) return; @@ -99,146 +89,109 @@ void Init(struct android_app* app) g_App = app; ANativeWindow_acquire(g_App->window); - // Initialize EGL - // This is mostly boilerplate code for EGL... - { - g_EglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); - if (g_EglDisplay == EGL_NO_DISPLAY) - __android_log_print(ANDROID_LOG_ERROR, g_LogTag, "%s", "eglGetDisplay(EGL_DEFAULT_DISPLAY) returned EGL_NO_DISPLAY"); + // --- EGL initialization (standard Android boilerplate) --- + g_EglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (g_EglDisplay == EGL_NO_DISPLAY) + __android_log_print(ANDROID_LOG_ERROR, g_LogTag, "%s", "eglGetDisplay() returned EGL_NO_DISPLAY"); - if (eglInitialize(g_EglDisplay, 0, 0) != EGL_TRUE) - __android_log_print(ANDROID_LOG_ERROR, g_LogTag, "%s", "eglInitialize() returned with an error"); + if (eglInitialize(g_EglDisplay, 0, 0) != EGL_TRUE) + __android_log_print(ANDROID_LOG_ERROR, g_LogTag, "%s", "eglInitialize() returned with an error"); - const EGLint egl_attributes[] = { EGL_BLUE_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_RED_SIZE, 8, EGL_DEPTH_SIZE, 24, EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE }; - EGLint num_configs = 0; - if (eglChooseConfig(g_EglDisplay, egl_attributes, nullptr, 0, &num_configs) != EGL_TRUE) - __android_log_print(ANDROID_LOG_ERROR, g_LogTag, "%s", "eglChooseConfig() returned with an error"); - if (num_configs == 0) - __android_log_print(ANDROID_LOG_ERROR, g_LogTag, "%s", "eglChooseConfig() returned 0 matching config"); + const EGLint egl_attributes[] = { + EGL_BLUE_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_RED_SIZE, 8, + EGL_DEPTH_SIZE, 24, EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE + }; + EGLint num_configs = 0; + if (eglChooseConfig(g_EglDisplay, egl_attributes, nullptr, 0, &num_configs) != EGL_TRUE || num_configs == 0) + __android_log_print(ANDROID_LOG_ERROR, g_LogTag, "%s", "eglChooseConfig() returned 0 matching config"); - // Get the first matching config - EGLConfig egl_config; - eglChooseConfig(g_EglDisplay, egl_attributes, &egl_config, 1, &num_configs); - EGLint egl_format; - eglGetConfigAttrib(g_EglDisplay, egl_config, EGL_NATIVE_VISUAL_ID, &egl_format); - ANativeWindow_setBuffersGeometry(g_App->window, 0, 0, egl_format); + EGLConfig egl_config; + eglChooseConfig(g_EglDisplay, egl_attributes, &egl_config, 1, &num_configs); + EGLint egl_format; + eglGetConfigAttrib(g_EglDisplay, egl_config, EGL_NATIVE_VISUAL_ID, &egl_format); + ANativeWindow_setBuffersGeometry(g_App->window, 0, 0, egl_format); - const EGLint egl_context_attributes[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; - g_EglContext = eglCreateContext(g_EglDisplay, egl_config, EGL_NO_CONTEXT, egl_context_attributes); + const EGLint egl_context_attributes[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; + g_EglContext = eglCreateContext(g_EglDisplay, egl_config, EGL_NO_CONTEXT, egl_context_attributes); + if (g_EglContext == EGL_NO_CONTEXT) + __android_log_print(ANDROID_LOG_ERROR, g_LogTag, "%s", "eglCreateContext() returned EGL_NO_CONTEXT"); - if (g_EglContext == EGL_NO_CONTEXT) - __android_log_print(ANDROID_LOG_ERROR, g_LogTag, "%s", "eglCreateContext() returned EGL_NO_CONTEXT"); + g_EglSurface = eglCreateWindowSurface(g_EglDisplay, egl_config, g_App->window, nullptr); + eglMakeCurrent(g_EglDisplay, g_EglSurface, g_EglSurface, g_EglContext); - g_EglSurface = eglCreateWindowSurface(g_EglDisplay, egl_config, g_App->window, nullptr); - eglMakeCurrent(g_EglDisplay, g_EglSurface, g_EglSurface, g_EglContext); - } - - // Setup Dear ImGui context + // --- Dear ImGui setup --- IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); - // Redirect loading/saving of .ini file to our location. - // Make sure 'g_IniFilename' persists while we use Dear ImGui. + // Redirect .ini file to app-internal storage g_IniFilename = std::string(app->activity->internalDataPath) + "/imgui.ini"; - io.IniFilename = g_IniFilename.c_str();; + io.IniFilename = g_IniFilename.c_str(); - // Setup Dear ImGui style + // Dark theme ImGui::StyleColorsDark(); - //ImGui::StyleColorsLight(); - // Setup Platform/Renderer backends - ImGui_ImplAndroid_Init(g_App->window); - ImGui_ImplOpenGL3_Init("#version 300 es"); + // Initialize backends — the Android backend handles keyboard/clipboard via JNI internally + // Pass the NativeActivity object so the backend can do JNI calls without any app boilerplate + ImGui_ImplAndroid_Init(g_App->window, app->activity->assetManager, app->activity->clazz); + ImGui_ImplOpenGL3Init("#version 300 es"); - // Setup scaling - float main_scale = 2.0f; + // Scale for typical mobile DPI — use actual display density from the backend + ImGui_ImplAndroid_DisplayMetrics dm; + ImGui_ImplAndroid_GetDisplayMetrics(&dm); + float main_scale = dm.Density > 0.0f ? dm.Density : 2.0f; ImGuiStyle& style = ImGui::GetStyle(); - style.ScaleAllSizes(main_scale); // Bake a fixed style scale. (until we have a solution for dynamic style scaling, changing this requires resetting Style + calling this again) - style.FontScaleDpi = main_scale; // Set initial font scale. + style.ScaleAllSizes(main_scale); + style.FontScaleDpi = main_scale; - // Load Fonts - // - If fonts are not explicitly loaded, Dear ImGui will select an embedded font: either AddFontDefaultVector() or AddFontDefaultBitmap(). - // This selection is based on (style.FontSizeBase * style.FontScaleMain * style.FontScaleDpi) reaching a small threshold. - // - You can load multiple fonts and use ImGui::PushFont()/PopFont() to select them. - // - If a file cannot be loaded, AddFont functions will return a nullptr. Please handle those errors in your code (e.g. use an assertion, display an error and quit). - // - Read 'docs/FONTS.md' for more instructions and details. - // - Use '#define IMGUI_ENABLE_FREETYPE' in your imconfig file to use FreeType for higher quality font rendering. - // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ ! - // - Android: The TTF files have to be placed into the assets/ directory (android/app/src/main/assets), we use our GetAssetData() helper to retrieve them. - //style.FontSizeBase = 20.0f; - //io.Fonts->AddFontDefaultVector(); - //io.Fonts->AddFontDefaultBitmap(); - - // Important: when calling AddFontFromMemoryTTF(), ownership of font_data is transferred by Dear ImGui by default (deleted is handled by Dear ImGui), unless we set FontDataOwnedByAtlas=false in ImFontConfig - //void* font_data; - //int font_data_size; - //ImFont* font; - //font_data_size = GetAssetData("segoeui.ttf", &font_data); - //font = io.Fonts->AddFontFromMemoryTTF(font_data, font_data_size); - //IM_ASSERT(font != nullptr); - //font_data_size = GetAssetData("DroidSans.ttf", &font_data); - //font = io.Fonts->AddFontFromMemoryTTF(font_data, font_data_size); - //IM_ASSERT(font != nullptr); - //font_data_size = GetAssetData("Roboto-Medium.ttf", &font_data); - //font = io.Fonts->AddFontFromMemoryTTF(font_data, font_data_size); - //IM_ASSERT(font != nullptr); - //font_data_size = GetAssetData("Cousine-Regular.ttf", &font_data); - //font = io.Fonts->AddFontFromMemoryTTF(font_data, font_data_size); - //IM_ASSERT(font != nullptr); - //font_data_size = GetAssetData("ArialUni.ttf", &font_data); - //font = io.Fonts->AddFontFromMemoryTTF(font_data, font_data_size); - //IM_ASSERT(font != nullptr); + // Enable sensors to showcase device capabilities + ImGui_ImplAndroid_EnableSensor(ImGui_ImplAndroid_SensorType_Accelerometer); + ImGui_ImplAndroid_EnableSensor(ImGui_ImplAndroid_SensorType_Gyroscope); + ImGui_ImplAndroid_EnableSensor(ImGui_ImplAndroid_SensorType_Magnetometer); + ImGui_ImplAndroid_EnableSensor(ImGui_ImplAndroid_SensorType_Light); + ImGui_ImplAndroid_EnableSensor(ImGui_ImplAndroid_SensorType_Proximity); g_Initialized = true; } -void MainLoopStep() +static void MainLoopStep() { - ImGuiIO& io = ImGui::GetIO(); if (g_EglDisplay == EGL_NO_DISPLAY) return; - // Our state - // (we use static, which essentially makes the variable globals, as a convenience to keep the example code easy to follow) + ImGuiIO& io = ImGui::GetIO(); + + // State static bool show_demo_window = true; static bool show_another_window = false; + static bool show_sensor_window = true; + static bool show_display_window = true; static ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); - // Poll Unicode characters via JNI - // FIXME: do not call this every frame because of JNI overhead - PollUnicodeChars(); - - // Open on-screen (soft) input if requested by Dear ImGui - static bool WantTextInputLast = false; - if (io.WantTextInput && !WantTextInputLast) - ShowSoftKeyboardInput(); - WantTextInputLast = io.WantTextInput; - - // Start the Dear ImGui frame + // Start a new frame — backend handles Unicode polling + soft keyboard automatically ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplAndroid_NewFrame(); ImGui::NewFrame(); - // 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!). + // 1. Show the big demo window if (show_demo_window) ImGui::ShowDemoWindow(&show_demo_window); - // 2. Show a simple window that we create ourselves. We use a Begin/End pair to create a named window. + // 2. Show a simple window we create ourselves { static float f = 0.0f; static int counter = 0; - ImGui::Begin("Hello, world!"); // Create a window called "Hello, world!" and append into it. + ImGui::Begin("Hello, Android!"); - ImGui::Text("This is some useful text."); // Display some text (you can use a format strings too) - ImGui::Checkbox("Demo Window", &show_demo_window); // Edit bools storing our window open/close state + ImGui::Text("This is some useful text."); + ImGui::Checkbox("Demo Window", &show_demo_window); ImGui::Checkbox("Another Window", &show_another_window); - ImGui::SliderFloat("float", &f, 0.0f, 1.0f); // Edit 1 float using a slider from 0.0f to 1.0f - ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color + ImGui::SliderFloat("float", &f, 0.0f, 1.0f); + ImGui::ColorEdit3("clear color", (float*)&clear_color); - if (ImGui::Button("Button")) // Buttons return true when clicked (most widgets return true when edited/activated) + if (ImGui::Button("Button")) counter++; ImGui::SameLine(); ImGui::Text("counter = %d", counter); @@ -247,17 +200,81 @@ void MainLoopStep() ImGui::End(); } - // 3. Show another simple window. + // 3. Show another simple window if (show_another_window) { - ImGui::Begin("Another Window", &show_another_window); // Pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked) + ImGui::Begin("Another Window", &show_another_window); ImGui::Text("Hello from another window!"); if (ImGui::Button("Close Me")) show_another_window = false; ImGui::End(); } - // Rendering + // 4. Show sensor data window — showcases the device's sensors + if (show_sensor_window) + { + ImGui::Begin("Sensors", &show_sensor_window); + + const char* sensor_names[] = { "Accelerometer", "Gyroscope", "Magnetometer", "Light", "Proximity" }; + for (int i = 0; i < 5; i++) + { + ImGui_ImplAndroid_SensorData sd; + ImGui_ImplAndroid_GetSensorData(i, &sd); + if (!sd.Available) + { + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "%s: N/A", sensor_names[i]); + } + else + { + if (i <= 2) // 3-axis sensors + { + ImGui::Text("%s:", sensor_names[i]); + ImGui::SameLine(120); + ImGui::Text("X:%.2f Y:%.2f Z:%.2f", sd.Values[0], sd.Values[1], sd.Values[2]); + + // Visual bar for each axis + ImGui::SameLine(280); + char label[32]; + snprintf(label, sizeof(label), "X##%d", i); + ImGui::ProgressBar(sd.Values[0] / 20.0f + 0.5f, ImVec2(40, 0), label); + } + else // Scalar sensors (light, proximity) + { + ImGui::Text("%s:", sensor_names[i]); + ImGui::SameLine(120); + ImGui::Text("%.1f", sd.Values[0]); + } + } + } + + ImGui::Separator(); + ImGui::Text("Accuracy: see Android docs for status codes"); + ImGui::End(); + } + + // 5. Show display metrics window + if (show_display_window) + { + ImGui::Begin("Display", &show_display_window); + + ImGui_ImplAndroid_DisplayMetrics dm; + ImGui_ImplAndroid_GetDisplayMetrics(&dm); + + const char* orient_names[] = { "Portrait", "Landscape", "Reverse Portrait", "Reverse Landscape" }; + int orient = dm.Orientation >= 0 && dm.Orientation <= 3 ? dm.Orientation : 0; + + ImGui::Text("Resolution: %d x %d", dm.WidthPixels, dm.HeightPixels); + ImGui::Text("Density: %.2f (%d DPI)", dm.Density, dm.DensityDpi); + ImGui::Text("Physical: %.1f x %.1f DPI", dm.Xdpi, dm.Ydpi); + ImGui::Text("Refresh: %.1f Hz", dm.RefreshRate); + ImGui::Text("Orientation: %s", orient_names[orient]); + + ImGui::Separator(); + ImGui::Text("Font scale: %.1fx", ImGui::GetStyle().FontScaleDpi); + ImGui::End(); + } + + // Render ImGui::Render(); glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y); glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w); @@ -266,12 +283,11 @@ void MainLoopStep() eglSwapBuffers(g_EglDisplay, g_EglSurface); } -void Shutdown() +static void Shutdown() { if (!g_Initialized) return; - // Cleanup ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplAndroid_Shutdown(); ImGui::DestroyContext(); @@ -279,107 +295,19 @@ void Shutdown() if (g_EglDisplay != EGL_NO_DISPLAY) { eglMakeCurrent(g_EglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - if (g_EglContext != EGL_NO_CONTEXT) eglDestroyContext(g_EglDisplay, g_EglContext); - if (g_EglSurface != EGL_NO_SURFACE) eglDestroySurface(g_EglDisplay, g_EglSurface); - eglTerminate(g_EglDisplay); } g_EglDisplay = EGL_NO_DISPLAY; g_EglContext = EGL_NO_CONTEXT; g_EglSurface = EGL_NO_SURFACE; - ANativeWindow_release(g_App->window); + + if (g_App && g_App->window) + ANativeWindow_release(g_App->window); g_Initialized = false; } - -// Helper functions - -// Unfortunately, there is no way to show the on-screen input from native code. -// Therefore, we call ShowSoftKeyboardInput() of the main activity implemented in MainActivity.kt via JNI. -static int ShowSoftKeyboardInput() -{ - JavaVM* java_vm = g_App->activity->vm; - JNIEnv* java_env = nullptr; - - jint jni_return = java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6); - if (jni_return == JNI_ERR) - return -1; - - jni_return = java_vm->AttachCurrentThread(&java_env, nullptr); - if (jni_return != JNI_OK) - return -2; - - jclass native_activity_clazz = java_env->GetObjectClass(g_App->activity->clazz); - if (native_activity_clazz == nullptr) - return -3; - - jmethodID method_id = java_env->GetMethodID(native_activity_clazz, "showSoftInput", "()V"); - if (method_id == nullptr) - return -4; - - java_env->CallVoidMethod(g_App->activity->clazz, method_id); - - jni_return = java_vm->DetachCurrentThread(); - if (jni_return != JNI_OK) - return -5; - - return 0; -} - -// Unfortunately, the native KeyEvent implementation has no getUnicodeChar() function. -// Therefore, we implement the processing of KeyEvents in MainActivity.kt and poll -// the resulting Unicode characters here via JNI and send them to Dear ImGui. -static int PollUnicodeChars() -{ - JavaVM* java_vm = g_App->activity->vm; - JNIEnv* java_env = nullptr; - - jint jni_return = java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6); - if (jni_return == JNI_ERR) - return -1; - - jni_return = java_vm->AttachCurrentThread(&java_env, nullptr); - if (jni_return != JNI_OK) - return -2; - - jclass native_activity_clazz = java_env->GetObjectClass(g_App->activity->clazz); - if (native_activity_clazz == nullptr) - return -3; - - jmethodID method_id = java_env->GetMethodID(native_activity_clazz, "pollUnicodeChar", "()I"); - if (method_id == nullptr) - return -4; - - // Send the actual characters to Dear ImGui - ImGuiIO& io = ImGui::GetIO(); - jint unicode_character; - while ((unicode_character = java_env->CallIntMethod(g_App->activity->clazz, method_id)) != 0) - io.AddInputCharacter(unicode_character); - - jni_return = java_vm->DetachCurrentThread(); - if (jni_return != JNI_OK) - return -5; - - return 0; -} - -// Helper to retrieve data placed into the assets/ directory (android/app/src/main/assets) -static int GetAssetData(const char* filename, void** outData) -{ - int num_bytes = 0; - AAsset* asset_descriptor = AAssetManager_open(g_App->activity->assetManager, filename, AASSET_MODE_BUFFER); - if (asset_descriptor) - { - num_bytes = AAsset_getLength(asset_descriptor); - *outData = IM_ALLOC(num_bytes); - int64_t num_bytes_read = AAsset_read(asset_descriptor, *outData, num_bytes); - AAsset_close(asset_descriptor); - IM_ASSERT(num_bytes_read == num_bytes); - } - return num_bytes; -}