diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 888593e01b953..b30458ec18616 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -458,6 +458,18 @@ jobs: popd make -C examples/example_emscripten_wgpu + Android: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + + - name: Build example_android_opengl3 + run: | + cd examples/example_android_opengl3 + ./build_android.sh + Discord-CI: runs-on: ubuntu-18.04 if: always() diff --git a/backends/imgui_impl_android.cpp b/backends/imgui_impl_android.cpp new file mode 100644 index 0000000000000..56df3340dc900 --- /dev/null +++ b/backends/imgui_impl_android.cpp @@ -0,0 +1,166 @@ +// dear imgui: Platform Binding for Android native app +// This needs to be used along with the OpenGL 3 Renderer (imgui_impl_opengl3) + +// Implemented features: +// [x] Basic mouse input via touch +// [x] Open soft keyboard if io.WantTextInput and perform proper keyboard input +// [x] Handle Unicode characters +// [ ] Handle physical mouse input + +// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this. +// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp. +// https://github.com/ocornut/imgui + +// CHANGELOG +// (minor and older changes stripped away, please see git history for details) +// 2020-09-13: Support for Unicode characters +// 2020-08-31: On-screen and physical keyboard input (ASCII characters only) +// 2020-03-02: basic draft, touch input + +#include "imgui.h" +#include "imgui_impl_android.h" +#include +#include +#include + +// Android +#include +#include +#include +#include + +static double g_Time = 0.0; +static ANativeWindow* g_Window; +static char g_logTag[] = "ImguiExample"; +static std::map> g_keyEventQueues; + +int32_t ImGui_ImplAndroid_HandleInputEvent(AInputEvent* inputEvent) +{ + ImGuiIO &io = ImGui::GetIO(); + int32_t evType = AInputEvent_getType(inputEvent); + switch (evType) + { + case AINPUT_EVENT_TYPE_KEY: + { + int32_t evKeyCode = AKeyEvent_getKeyCode(inputEvent); + int32_t evAction = AKeyEvent_getAction(inputEvent); + int32_t evMetaState = AKeyEvent_getMetaState(inputEvent); + + io.KeyCtrl = ((evMetaState & AMETA_CTRL_ON) != 0); + io.KeyShift = ((evMetaState & AMETA_SHIFT_ON) != 0); + io.KeyAlt = ((evMetaState & AMETA_ALT_ON) != 0); + + switch (evAction) + { + // todo: AKEY_EVENT_ACTION_DOWN and AKEY_EVENT_ACTION_UP occur at once + // as soon as a touch pointer goes up from a key. We use a simple key event queue + // and process one event per key per ImGui frame in ImGui_ImplAndroid_NewFrame(). + // ...or consider ImGui IO queue, if suitable: https://github.com/ocornut/imgui/issues/2787 + case AKEY_EVENT_ACTION_DOWN: + case AKEY_EVENT_ACTION_UP: + g_keyEventQueues[evKeyCode].push(evAction); + break; + default: + break; + } + break; + } + case AINPUT_EVENT_TYPE_MOTION: + { + int32_t evAction = AMotionEvent_getAction(inputEvent); + int32_t evPointerIndex = (evAction & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT; + int32_t evPointerId = AMotionEvent_getPointerId(inputEvent, evPointerIndex); + evAction &= AMOTION_EVENT_ACTION_MASK; + switch (evAction) + { + case AMOTION_EVENT_ACTION_DOWN: + case AMOTION_EVENT_ACTION_UP: + io.MouseDown[0] = (evAction == AMOTION_EVENT_ACTION_DOWN) ? true : false; + // intended fallthrough... + case AMOTION_EVENT_ACTION_MOVE: + io.MousePos = ImVec2( + AMotionEvent_getX(inputEvent, evPointerIndex), + AMotionEvent_getY(inputEvent, evPointerIndex)); + break; + default: + break; + } + } + return 1; + default: + break; + } + + return 0; +} + +bool ImGui_ImplAndroid_Init(ANativeWindow* window) +{ + g_Window = window; + g_Time = 0.0; + + // Setup back-end capabilities flags + ImGuiIO &io = ImGui::GetIO(); + // todo: any reasonable io.BackendFlags? + io.BackendPlatformName = "imgui_impl_android"; + + // Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array. + io.KeyMap[ImGuiKey_Tab] = AKEYCODE_TAB; + io.KeyMap[ImGuiKey_LeftArrow] = AKEYCODE_DPAD_LEFT; // also covers physical keyboard arrow key + io.KeyMap[ImGuiKey_RightArrow] = AKEYCODE_DPAD_RIGHT; // also covers physical keyboard arrow key + io.KeyMap[ImGuiKey_UpArrow] = AKEYCODE_DPAD_UP; // also covers physical keyboard arrow key + io.KeyMap[ImGuiKey_DownArrow] = AKEYCODE_DPAD_DOWN; // also covers physical keyboard arrow key + io.KeyMap[ImGuiKey_PageUp] = AKEYCODE_PAGE_UP; + io.KeyMap[ImGuiKey_PageDown] = AKEYCODE_PAGE_DOWN; + io.KeyMap[ImGuiKey_Home] = AKEYCODE_MOVE_HOME; + io.KeyMap[ImGuiKey_End] = AKEYCODE_MOVE_END; + io.KeyMap[ImGuiKey_Insert] = AKEYCODE_INSERT; + io.KeyMap[ImGuiKey_Delete] = AKEYCODE_FORWARD_DEL; + io.KeyMap[ImGuiKey_Backspace] = AKEYCODE_DEL; + io.KeyMap[ImGuiKey_Space] = AKEYCODE_SPACE; + io.KeyMap[ImGuiKey_Enter] = AKEYCODE_ENTER; + io.KeyMap[ImGuiKey_Escape] = AKEYCODE_ESCAPE; + io.KeyMap[ImGuiKey_KeyPadEnter] = AKEYCODE_NUMPAD_ENTER; + io.KeyMap[ImGuiKey_A] = AKEYCODE_A; + io.KeyMap[ImGuiKey_C] = AKEYCODE_C; + io.KeyMap[ImGuiKey_V] = AKEYCODE_V; + io.KeyMap[ImGuiKey_X] = AKEYCODE_X; + io.KeyMap[ImGuiKey_Y] = AKEYCODE_Y; + io.KeyMap[ImGuiKey_Z] = AKEYCODE_Z; + + return true; +} + +void ImGui_ImplAndroid_Shutdown() {} + +void ImGui_ImplAndroid_NewFrame() +{ + ImGuiIO &io = ImGui::GetIO(); + IM_ASSERT(io.Fonts->IsBuilt() && "Font atlas not built! It is generally built by the renderer back-end. Missing call to renderer _NewFrame() function? e.g. ImGui_ImplOpenGL3_NewFrame()."); + + // Process queued key events + for (auto &keyQueue : g_keyEventQueues) + { + if (keyQueue.second.size() == 0) + continue; + io.KeysDown[keyQueue.first] = (keyQueue.second.front() == AKEY_EVENT_ACTION_DOWN); + keyQueue.second.pop(); + } + + // Setup display size (every frame to accommodate for window resizing) + int32_t w = ANativeWindow_getWidth(g_Window); + int32_t h = ANativeWindow_getHeight(g_Window); + int display_w = w; + int display_h = h; + + io.DisplaySize = ImVec2((float)w, (float)h); + if (w > 0 && h > 0) + io.DisplayFramebufferScale = ImVec2((float)display_w / w, (float)display_h / h); + + // Setup time step + struct timespec current_timespec; + clock_gettime(CLOCK_MONOTONIC, ¤t_timespec); + 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; +} diff --git a/backends/imgui_impl_android.h b/backends/imgui_impl_android.h new file mode 100644 index 0000000000000..05f069a630016 --- /dev/null +++ b/backends/imgui_impl_android.h @@ -0,0 +1,22 @@ +// dear imgui: Platform Binding for Android native app +// This needs to be used along with the OpenGL 3 Renderer (imgui_impl_opengl3) + +// Implemented features: +// [x] Basic mouse input via touch +// [x] Open soft keyboard if io.WantTextInput and perform proper keyboard input +// [x] Handle Unicode characters +// [ ] Handle physical mouse input + +// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this. +// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp. +// https://github.com/ocornut/imgui + +#pragma once + +struct ANativeWindow; +struct AInputEvent; + +IMGUI_IMPL_API int32_t ImGui_ImplAndroid_HandleInputEvent(AInputEvent* inputEvent); +IMGUI_IMPL_API bool ImGui_ImplAndroid_Init(ANativeWindow* window); +IMGUI_IMPL_API void ImGui_ImplAndroid_Shutdown(); +IMGUI_IMPL_API void ImGui_ImplAndroid_NewFrame(); diff --git a/docs/BACKENDS.md b/docs/BACKENDS.md index ad9a02355fbad..a45fdaa4031b2 100644 --- a/docs/BACKENDS.md +++ b/docs/BACKENDS.md @@ -57,6 +57,7 @@ In the [backends/](https://github.com/ocornut/imgui/blob/master/backends) folder List of Platforms Backends: + imgui_impl_android.cpp ; Android native app API imgui_impl_glfw.cpp ; GLFW (Windows, macOS, Linux, etc.) http://www.glfw.org/ imgui_impl_osx.mm ; macOS native API (not as feature complete as glfw/sdl backends) imgui_impl_sdl.cpp ; SDL2 (Windows, macOS, Linux, iOS, Android) https://www.libsdl.org diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 35d4bec373205..34261edbbf619 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -79,6 +79,10 @@ Changelog, so if you want to update them later it will be easier to catch up wit Allegro 5 example.
= main.cpp + imgui_impl_allegro5.cpp +[example_android_opengl3/](https://github.com/ocornut/imgui/blob/master/examples/example_android_opengl3/)
+Android + OpenGL3 (ES) example.
+= main.cpp + imgui_impl_android.cpp + imgui_impl_opengl3.cpp + [example_apple_metal/](https://github.com/ocornut/imgui/blob/master/examples/example_metal/)
OSX & iOS + Metal example.
= main.m + imgui_impl_osx.mm + imgui_impl_metal.mm
diff --git a/docs/README.md b/docs/README.md index 0da03cdf4ea34..9fdd084ddbca6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -117,7 +117,7 @@ Integrating Dear ImGui within your custom engine is a matter of 1) wiring mouse/ Officially maintained backends/bindings (in repository): - Renderers: DirectX9, DirectX10, DirectX11, DirectX12, Metal, OpenGL (legacy), OpenGL3/ES/ES2 (modern), Vulkan, WebGPU. -- Platforms: GLFW, SDL2, Win32, Glut, OSX. +- Platforms: GLFW, SDL2, Win32, Glut, OSX, Android. - Frameworks: Emscripten, Allegro5, Marmalade. [Third-party backends/bindings](https://github.com/ocornut/imgui/wiki/Bindings) wiki page: diff --git a/examples/example_android_opengl3/CMakeLists.txt b/examples/example_android_opengl3/CMakeLists.txt new file mode 100644 index 0000000000000..0010b4bdb8320 --- /dev/null +++ b/examples/example_android_opengl3/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.6) + +project(ImguiExample) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_library(${CMAKE_PROJECT_NAME} SHARED + ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../imgui.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../imgui_demo.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../imgui_draw.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../imgui_tables.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../imgui_widgets.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../backends/imgui_impl_android.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../backends/imgui_impl_opengl3.cpp + ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c +) + +set(CMAKE_SHARED_LINKER_FLAGS + "${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate" +) + +target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE + IMGUI_IMPL_OPENGL_ES3 +) + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../.. + ${CMAKE_CURRENT_SOURCE_DIR}/../../backends + ${ANDROID_NDK}/sources/android/native_app_glue +) + +target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE + android + EGL + GLESv3 + log +) diff --git a/examples/example_android_opengl3/android/.gitignore b/examples/example_android_opengl3/android/.gitignore new file mode 100644 index 0000000000000..777fa1827d090 --- /dev/null +++ b/examples/example_android_opengl3/android/.gitignore @@ -0,0 +1,12 @@ +.cxx +.externalNativeBuild +build/ +*.iml + +.idea +.gradle +local.properties +/captures + +.DS_Store +Thumbs.db diff --git a/examples/example_android_opengl3/android/app/build.gradle b/examples/example_android_opengl3/android/app/build.gradle new file mode 100644 index 0000000000000..30b379d4e719d --- /dev/null +++ b/examples/example_android_opengl3/android/app/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + defaultConfig { + applicationId "imgui.example.android" + minSdkVersion 23 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') + } + } + + externalNativeBuild { + cmake { + path "../../CMakeLists.txt" + version "3.6.0" + } + } +} +repositories { + mavenCentral() +} +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/examples/example_android_opengl3/android/app/src/main/AndroidManifest.xml b/examples/example_android_opengl3/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000..ea838e6d71409 --- /dev/null +++ b/examples/example_android_opengl3/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + 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 new file mode 100644 index 0000000000000..4060984f0c832 --- /dev/null +++ b/examples/example_android_opengl3/android/app/src/main/java/MainActivity.kt @@ -0,0 +1,50 @@ +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 + +class MainActivity : NativeActivity() { + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + fun showSoftInput() { + val inputMM = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMM.showSoftInput(this.window.decorView, 0) + } + + fun hideSoftInput() { + val inputMM = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMM.hideSoftInputFromWindow(this.window.decorView.windowToken, 0) + } + + // Queue for the Unicode characters to be polled from native code (via pollUnicodeChar()) + private var unicCharQueue: LinkedBlockingQueue = LinkedBlockingQueue() + + // We assume dispatchKeyEvent() of the NativeActivity is actually called for every + // KeyEvent and not consumed by any View before it reaches here + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + var unic = event.getUnicodeChar(event.metaState) + + if (event.action == KeyEvent.ACTION_DOWN) { + if (unic != 0) { + unicCharQueue.offer(Integer.valueOf(unic)) + } else { + unicCharQueue.offer(Integer.valueOf(0)) + } + } else if (event.action == KeyEvent.ACTION_MULTIPLE) { + unic = Character.codePointAt(event.characters, 0) + unicCharQueue.offer(Integer.valueOf(unic)) + } + + return super.dispatchKeyEvent(event) + } + + fun pollUnicodeChar(): Int { + return if (!unicCharQueue.isEmpty()) unicCharQueue.poll().toInt() else 0 + } +} diff --git a/examples/example_android_opengl3/android/build.gradle b/examples/example_android_opengl3/android/build.gradle new file mode 100644 index 0000000000000..cce52ba83932d --- /dev/null +++ b/examples/example_android_opengl3/android/build.gradle @@ -0,0 +1,25 @@ +buildscript { + ext.kotlin_version = '1.3.72' + repositories { + google() + jcenter() + + } + dependencies { + classpath 'com.android.tools.build:gradle:3.5.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + + } +} + +allprojects { + repositories { + google() + jcenter() + + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/examples/example_android_opengl3/android/gradle/wrapper/gradle-wrapper.jar b/examples/example_android_opengl3/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000..5c2d1cf016b38 Binary files /dev/null and b/examples/example_android_opengl3/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/example_android_opengl3/android/gradle/wrapper/gradle-wrapper.properties b/examples/example_android_opengl3/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000000..f4d7b2bf616f7 --- /dev/null +++ b/examples/example_android_opengl3/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/example_android_opengl3/android/gradlew b/examples/example_android_opengl3/android/gradlew new file mode 100755 index 0000000000000..b0d6d0ab5deb5 --- /dev/null +++ b/examples/example_android_opengl3/android/gradlew @@ -0,0 +1,188 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/example_android_opengl3/android/gradlew.bat b/examples/example_android_opengl3/android/gradlew.bat new file mode 100644 index 0000000000000..9991c503266b5 --- /dev/null +++ b/examples/example_android_opengl3/android/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/example_android_opengl3/android/settings.gradle b/examples/example_android_opengl3/android/settings.gradle new file mode 100644 index 0000000000000..e7b4def49cb53 --- /dev/null +++ b/examples/example_android_opengl3/android/settings.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/examples/example_android_opengl3/build_android.sh b/examples/example_android_opengl3/build_android.sh new file mode 100755 index 0000000000000..4324006acc4f4 --- /dev/null +++ b/examples/example_android_opengl3/build_android.sh @@ -0,0 +1,11 @@ +#!/bin/bash +cd android + +cd gradle/wrapper +curl --location --output gradle-wrapper.jar.sha256 \ + https://services.gradle.org/distributions/gradle-5.4.1-wrapper.jar.sha256 +echo " gradle-wrapper.jar" >> gradle-wrapper.jar.sha256 +sha256sum --check gradle-wrapper.jar.sha256 +cd ../.. + +./gradlew assembleDebug diff --git a/examples/example_android_opengl3/main.cpp b/examples/example_android_opengl3/main.cpp new file mode 100644 index 0000000000000..5b481d570bf72 --- /dev/null +++ b/examples/example_android_opengl3/main.cpp @@ -0,0 +1,314 @@ +// dear imgui: standalone example application for Android + OpenGL ES 3 +// If you are new to dear imgui, see examples/README.txt and documentation at the top of imgui.cpp. + +#include "imgui.h" +#include "imgui_impl_android.h" +#include "imgui_impl_opengl3.h" +#include +#include +#include +#include + +static EGLDisplay g_elgDisplay = EGL_NO_DISPLAY; +static EGLSurface g_eglSurface = EGL_NO_SURFACE; +static EGLContext g_eglContext = EGL_NO_CONTEXT; +static struct android_app* g_App = NULL; +static bool g_initialized = false; +static char g_logTag[] = "ImguiExample"; + +// Unfortunately, there is no way to show the on-screen input from native code. +// Therefore, we call showSoftInput() of the main activity implemented in MainActivity.kt via JNI. +static int showSoftInput() +{ + JavaVM *jVM = g_App->activity->vm; + JNIEnv *jEnv = NULL; + + jint jniRet = jVM->GetEnv((void **)&jEnv, JNI_VERSION_1_6); + if (jniRet == JNI_ERR) + return -1; + + jniRet = jVM->AttachCurrentThread(&jEnv, NULL); + if (jniRet != JNI_OK) + return -2; + + jclass natActClazz = jEnv->GetObjectClass(g_App->activity->clazz); + if (natActClazz == NULL) + return -3; + + jmethodID methodID = jEnv->GetMethodID(natActClazz, "showSoftInput", "()V"); + if (methodID == NULL) + return -4; + + jEnv->CallVoidMethod(g_App->activity->clazz, methodID); + + jniRet = jVM->DetachCurrentThread(); + if (jniRet != 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 *jVM = g_App->activity->vm; + JNIEnv *jEnv = NULL; + + jint jniRet = jVM->GetEnv((void **)&jEnv, JNI_VERSION_1_6); + if (jniRet == JNI_ERR) + return -1; + + jniRet = jVM->AttachCurrentThread(&jEnv, NULL); + if (jniRet != JNI_OK) + return -2; + + jclass natActClazz = jEnv->GetObjectClass(g_App->activity->clazz); + if (natActClazz == NULL) + return -3; + + jmethodID methodID = jEnv->GetMethodID(natActClazz, "pollUnicodeChar", "()I"); + if (methodID == NULL) + return -4; + + // Send the actual characters to Dear ImGui + ImGuiIO &io = ImGui::GetIO(); + jint unicChar; + while ((unicChar = jEnv->CallIntMethod(g_App->activity->clazz, methodID)) != 0) + { + io.AddInputCharacter(unicChar); + } + + jniRet = jVM->DetachCurrentThread(); + if (jniRet != JNI_OK) + return -5; + + return 0; +} + +void init(struct android_app *app) +{ + if (g_initialized) + return; + + g_App = app; + + // Initialize EGL + // This is mostly boilerplate code for EGL... + g_elgDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); + + if (g_elgDisplay == EGL_NO_DISPLAY) + __android_log_print(ANDROID_LOG_ERROR, g_logTag, "%s", "eglGetDisplay(EGL_DEFAULT_DISPLAY) returned EGL_NO_DISPLAY"); + + if (eglInitialize(g_elgDisplay, 0, 0) != EGL_TRUE) + __android_log_print(ANDROID_LOG_ERROR, g_logTag, "%s", "eglInitialize(..) returned with an error"); + + const EGLint eglAttribs[] = { + 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 numConfigs = 0; + if (eglChooseConfig(g_elgDisplay, eglAttribs, nullptr, 0, &numConfigs) != EGL_TRUE) + __android_log_print(ANDROID_LOG_ERROR, g_logTag, "%s", "eglChooseConfig(..) returned with an error"); + + if (numConfigs == 0) + __android_log_print(ANDROID_LOG_ERROR, g_logTag, "%s", "eglChooseConfig(..) returned 0 matching configs"); + + // Get the (first) matching config + EGLConfig config; + eglChooseConfig(g_elgDisplay, eglAttribs, &config, 1, &numConfigs); + EGLint format; + eglGetConfigAttrib(g_elgDisplay, config, EGL_NATIVE_VISUAL_ID, &format); + ANativeWindow_setBuffersGeometry(g_App->window, 0, 0, format); + + const EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; + g_eglContext = eglCreateContext(g_elgDisplay, config, EGL_NO_CONTEXT, contextAttribs); + + if (g_eglContext == EGL_NO_CONTEXT) + __android_log_print(ANDROID_LOG_ERROR, g_logTag, "%s", "eglCreateContext(..) returned EGL_NO_CONTEXT"); + + g_eglSurface = eglCreateWindowSurface(g_elgDisplay, config, g_App->window, NULL); + eglMakeCurrent(g_elgDisplay, g_eglSurface, g_eglSurface, g_eglContext); + + // Dear Imgui + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO &io = ImGui::GetIO(); + io.IniFilename = NULL; + ImGui::StyleColorsDark(); + ImGui_ImplAndroid_Init(app->window); + ImGui_ImplOpenGL3_Init("#version 300 es"); + + // Arbitrary scale-up + // todo: Put some effort into DPI awareness + ImGui::GetStyle().ScaleAllSizes(3.0f); + + g_initialized = true; +} + +void tick() +{ + // Our state (Dear Imgui) + static bool show_demo_window = true; + static bool show_another_window = false; + static ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); + + if (g_elgDisplay != EGL_NO_DISPLAY) + { + ImGuiIO& io = ImGui::GetIO(); + + // Poll Unicode characters via JNI + // todo: do not call this every frame because of JNI overhead + pollUnicodeChars(); + + // Open on-screen (soft) input if demanded by Dear ImGui + static bool WantTextInputLast = false; + if (io.WantTextInput && !WantTextInputLast) + showSoftInput(); + WantTextInputLast = io.WantTextInput; + + // Start the Dear ImGui frame + 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!). + 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 created a named window. + { + static float f = 0.0f; + static int counter = 0; + + ImGui::Begin("Hello, world!"); // Create a window called "Hello, world!" and append into it. + + 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::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 + + if (ImGui::Button("Button")) // Buttons return true when clicked (most widgets return true when edited/activated) + counter++; + ImGui::SameLine(); + ImGui::Text("counter = %d", counter); + + ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); + ImGui::End(); + } + + // 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::Text("Hello from another window!"); + if (ImGui::Button("Close Me")) + show_another_window = false; + ImGui::End(); + } + + // Rendering + ImGui::Render(); + glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y); + glClearColor(clear_color.x, clear_color.y, clear_color.z, clear_color.w); + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + eglSwapBuffers(g_elgDisplay, g_eglSurface); + } +} + +void shutdown() +{ + if (!g_initialized) + return; + + // Cleanup (Dear Imgui) + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplAndroid_Shutdown(); + ImGui::DestroyContext(); + + if (g_elgDisplay != EGL_NO_DISPLAY) + { + eglMakeCurrent(g_elgDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + + if (g_eglContext != EGL_NO_CONTEXT) + eglDestroyContext(g_elgDisplay, g_eglContext); + + if (g_eglSurface != EGL_NO_SURFACE) + eglDestroySurface(g_elgDisplay, g_eglSurface); + + eglTerminate(g_elgDisplay); + } + + g_elgDisplay = EGL_NO_DISPLAY; + g_eglContext = EGL_NO_CONTEXT; + g_eglSurface = EGL_NO_SURFACE; + + g_initialized = false; +} + +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: + break; + case APP_CMD_LOST_FOCUS: + break; + } +} + +static int32_t handleInputEvent(struct android_app *app, AInputEvent *inputEvent) +{ + return ImGui_ImplAndroid_HandleInputEvent(inputEvent); +} + +void android_main(struct android_app *app) +{ + app->onAppCmd = handleAppCmd; + app->onInputEvent = handleInputEvent; + + while (true) + { + int ident; + int outEvents; + struct android_poll_source *outData; + + // Poll all events. If the app is not visible, this loop blocks until g_initialized == true. + while ((ident = ALooper_pollAll(g_initialized ? 0 : -1, NULL, &outEvents, (void **)&outData)) >= 0) + { + // Process one event + if (outData != NULL) + outData->process(app, outData); + + // 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) + shutdown(); + + return; + } + } + + // Initiate a new frame + tick(); + } +}