From dd5d4ebc61674a5e42ed6b1187c5d6a88c62f18f Mon Sep 17 00:00:00 2001 From: Le Philousophe Date: Sat, 22 Jun 2024 10:52:45 +0200 Subject: [PATCH] ANDROID: Rework the virtual gamepad controller - Use a SVG asset to allow for better scalability - Make the controller visible when touching the screen - Allow for oblique moving by placing finger between two directions - Add more buttons on the center area of the screen (GUIDE, START, LEFT STICK, RIGHT STICK) and simplify right area (only four buttons) - Cleanup now unused code --- .../graphics/android/android-graphics.cpp | 49 +- backends/graphics/android/android-graphics.h | 1 + .../graphics3d/android/android-graphics3d.cpp | 46 +- .../graphics3d/android/android-graphics3d.h | 1 + backends/platform/android/android.cpp | 4 + backends/platform/android/android.mk | 6 +- backends/platform/android/jni-android.cpp | 67 --- backends/platform/android/jni-android.h | 6 - .../android/org/scummvm/scummvm/ScummVM.java | 1 - .../org/scummvm/scummvm/ScummVMActivity.java | 17 - backends/platform/android/touchcontrols.cpp | 556 ++++++++++++++---- backends/platform/android/touchcontrols.h | 79 ++- dists/android/gamepad.svg | 90 +++ dists/android/res/drawable/touch_arrows.png | Bin 18028 -> 0 bytes 14 files changed, 625 insertions(+), 298 deletions(-) create mode 100644 dists/android/gamepad.svg delete mode 100644 dists/android/res/drawable/touch_arrows.png diff --git a/backends/graphics/android/android-graphics.cpp b/backends/graphics/android/android-graphics.cpp index f65e963bcd32c..c32ee5474bf2f 100644 --- a/backends/graphics/android/android-graphics.cpp +++ b/backends/graphics/android/android-graphics.cpp @@ -42,24 +42,7 @@ #include "backends/graphics/opengl/pipelines/pipeline.h" #include "graphics/blit.h" - -static void loadBuiltinTexture(JNI::BitmapResources resource, OpenGL::Surface *surf) { - const Graphics::Surface *src = JNI::getBitmapResource(resource); - if (!src) { - error("Failed to fetch touch arrows bitmap"); - } - - surf->allocate(src->w, src->h); - Graphics::Surface *dst = surf->getSurface(); - - Graphics::crossBlit( - (byte *)dst->getPixels(), (const byte *)src->getPixels(), - dst->pitch, src->pitch, - src->w, src->h, - src->format, dst->format); - - delete src; -} +#include "graphics/managed_surface.h" // // AndroidGraphicsManager @@ -72,10 +55,6 @@ AndroidGraphicsManager::AndroidGraphicsManager() : // Initialize our OpenGL ES context. initSurface(); - _touchcontrols = createSurface(_defaultFormatAlpha); - loadBuiltinTexture(JNI::BitmapResources::TOUCH_ARROWS_BITMAP, _touchcontrols); - _touchcontrols->updateGLTexture(); - // not in 3D, not in GUI dynamic_cast(g_system)->applyTouchSettings(false, false); dynamic_cast(g_system)->applyOrientationSettings(); @@ -85,6 +64,8 @@ AndroidGraphicsManager::~AndroidGraphicsManager() { ENTER(); deinitSurface(); + + delete _touchcontrols; } void AndroidGraphicsManager::initSurface() { @@ -118,8 +99,10 @@ void AndroidGraphicsManager::initSurface() { if (_touchcontrols) { _touchcontrols->recreate(); _touchcontrols->updateGLTexture(); + } else { + _touchcontrols = createSurface(_defaultFormatAlpha); } - dynamic_cast(g_system)->getTouchControls().init( + dynamic_cast(g_system)->getTouchControls().setDrawer( this, JNI::egl_surface_width, JNI::egl_surface_height); handleResize(JNI::egl_surface_width, JNI::egl_surface_height); @@ -132,7 +115,7 @@ void AndroidGraphicsManager::deinitSurface() { LOGD("deinitializing 2D surface"); // Deregister us from touch control - dynamic_cast(g_system)->getTouchControls().init( + dynamic_cast(g_system)->getTouchControls().setDrawer( nullptr, 0, 0); if (_touchcontrols) { _touchcontrols->destroy(); @@ -157,7 +140,7 @@ void AndroidGraphicsManager::resizeSurface() { error("JNI::initSurface failed"); } - dynamic_cast(g_system)->getTouchControls().init( + dynamic_cast(g_system)->getTouchControls().setDrawer( this, JNI::egl_surface_width, JNI::egl_surface_height); handleResize(JNI::egl_surface_width, JNI::egl_surface_height); @@ -246,6 +229,22 @@ void AndroidGraphicsManager::syncVirtkeyboardState(bool virtkeybd_on) { _forceRedraw = true; } +void AndroidGraphicsManager::touchControlInitSurface(const Graphics::ManagedSurface &surf) { + if (_touchcontrols->getWidth() == surf.w && _touchcontrols->getHeight() == surf.h) { + return; + } + + _touchcontrols->allocate(surf.w, surf.h); + Graphics::Surface *dst = _touchcontrols->getSurface(); + + Graphics::crossBlit( + (byte *)dst->getPixels(), (const byte *)surf.getPixels(), + dst->pitch, surf.pitch, + surf.w, surf.h, + surf.format, dst->format); + _touchcontrols->updateGLTexture(); +} + void AndroidGraphicsManager::touchControlDraw(int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) { _targetBuffer->enableBlend(OpenGL::Framebuffer::kBlendModeTraditionalTransparency); OpenGL::Pipeline *pipeline = getPipeline(); diff --git a/backends/graphics/android/android-graphics.h b/backends/graphics/android/android-graphics.h index 4ae9bae0c5e97..e1252a2d0fd4d 100644 --- a/backends/graphics/android/android-graphics.h +++ b/backends/graphics/android/android-graphics.h @@ -88,6 +88,7 @@ class AndroidGraphicsManager : float getHiDPIScreenFactor() const override; + void touchControlInitSurface(const Graphics::ManagedSurface &surf) override; void touchControlNotifyChanged() override; void touchControlDraw(int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) override; diff --git a/backends/graphics3d/android/android-graphics3d.cpp b/backends/graphics3d/android/android-graphics3d.cpp index bd4a24352558f..fd5a6b1f3d4a1 100644 --- a/backends/graphics3d/android/android-graphics3d.cpp +++ b/backends/graphics3d/android/android-graphics3d.cpp @@ -41,6 +41,7 @@ #include "common/tokenizer.h" #include "graphics/blit.h" +#include "graphics/managed_surface.h" #include "graphics/opengl/shader.h" #include "graphics/opengl/context.h" @@ -54,26 +55,6 @@ #define CONTEXT_RESET_ENABLE(gl_param) if (!(saved ## gl_param)) { GLCALL(glDisable(gl_param)); } #define CONTEXT_RESET_DISABLE(gl_param) if (saved ## gl_param) { GLCALL(glEnable(gl_param)); } -static GLES8888Texture *loadBuiltinTexture(JNI::BitmapResources resource) { - const Graphics::Surface *src = JNI::getBitmapResource(JNI::BitmapResources::TOUCH_ARROWS_BITMAP); - if (!src) { - error("Failed to fetch touch arrows bitmap"); - } - - GLES8888Texture *ret = new GLES8888Texture(); - ret->allocBuffer(src->w, src->h); - Graphics::Surface *dst = ret->surface(); - - Graphics::crossBlit( - (byte *)dst->getPixels(), (const byte *)src->getPixels(), - dst->pitch, src->pitch, - src->w, src->h, - src->format, dst->format); - - delete src; - return ret; -} - AndroidGraphics3dManager::AndroidGraphics3dManager() : _screenChangeID(0), _graphicsMode(0), @@ -94,7 +75,7 @@ AndroidGraphics3dManager::AndroidGraphics3dManager() : _mouse_hotspot(), _mouse_dont_scale(false), _show_mouse(false), - _touchcontrols_texture(nullptr), + _touchcontrols_texture(new GLES8888Texture()), _old_touch_mode(OSystem_Android::TOUCH_MODE_TOUCHPAD) { if (JNI::egl_bits_per_pixel == 16) { @@ -112,8 +93,6 @@ AndroidGraphics3dManager::AndroidGraphics3dManager() : } _mouse_texture = _mouse_texture_palette; - _touchcontrols_texture = loadBuiltinTexture(JNI::BitmapResources::TOUCH_ARROWS_BITMAP); - initSurface(); // in 3D, not in GUI @@ -218,7 +197,7 @@ void AndroidGraphics3dManager::initSurface() { if (_touchcontrols_texture) { _touchcontrols_texture->reinit(); } - dynamic_cast(g_system)->getTouchControls().init( + dynamic_cast(g_system)->getTouchControls().setDrawer( this, JNI::egl_surface_width, JNI::egl_surface_height); updateScreenRect(); @@ -256,7 +235,7 @@ void AndroidGraphics3dManager::deinitSurface() { _mouse_texture_palette->release(); } - dynamic_cast(g_system)->getTouchControls().init( + dynamic_cast(g_system)->getTouchControls().setDrawer( nullptr, 0, 0); if (_touchcontrols_texture) { _touchcontrols_texture->release(); @@ -286,7 +265,7 @@ void AndroidGraphics3dManager::resizeSurface() { initOverlay(); } - dynamic_cast(g_system)->getTouchControls().init( + dynamic_cast(g_system)->getTouchControls().setDrawer( this, JNI::egl_surface_width, JNI::egl_surface_height); updateScreenRect(); @@ -1103,6 +1082,21 @@ bool AndroidGraphics3dManager::setState(const AndroidCommonGraphics::State &stat return true; } +void AndroidGraphics3dManager::touchControlInitSurface(const Graphics::ManagedSurface &surf) { + if (_touchcontrols_texture->width() == surf.w && _touchcontrols_texture->height() == surf.h) { + return; + } + + _touchcontrols_texture->allocBuffer(surf.w, surf.h); + Graphics::Surface *dst = _touchcontrols_texture->surface(); + + Graphics::crossBlit( + (byte *)dst->getPixels(), (const byte *)surf.getPixels(), + dst->pitch, surf.pitch, + surf.w, surf.h, + surf.format, dst->format); +} + void AndroidGraphics3dManager::touchControlNotifyChanged() { // Make sure we redraw the screen _force_redraw = true; diff --git a/backends/graphics3d/android/android-graphics3d.h b/backends/graphics3d/android/android-graphics3d.h index 22ebae16386b6..aff669794453d 100644 --- a/backends/graphics3d/android/android-graphics3d.h +++ b/backends/graphics3d/android/android-graphics3d.h @@ -121,6 +121,7 @@ class AndroidGraphics3dManager : virtual Common::List getSupportedFormats() const override; #endif + void touchControlInitSurface(const Graphics::ManagedSurface &surf) override; void touchControlNotifyChanged() override; void touchControlDraw(int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) override; diff --git a/backends/platform/android/android.cpp b/backends/platform/android/android.cpp index a1ccd4f49d5e6..2dd3259e690aa 100644 --- a/backends/platform/android/android.cpp +++ b/backends/platform/android/android.cpp @@ -557,6 +557,10 @@ void OSystem_Android::initBackend() { _audio_thread_exit = false; pthread_create(&_audio_thread, 0, audioThreadFunc, this); + JNI::DPIValues dpi; + JNI::getDPI(dpi); + _touchControls.init(dpi[2]); + _graphicsManager = new AndroidGraphicsManager(); // renice this thread to boost the audio thread diff --git a/backends/platform/android/android.mk b/backends/platform/android/android.mk index 03d421016b8c6..835095828a525 100644 --- a/backends/platform/android/android.mk +++ b/backends/platform/android/android.mk @@ -13,7 +13,7 @@ APK_MAIN = ScummVM-debug.apk APK_MAIN_RELEASE = ScummVM-release-unsigned.apk AAB_MAIN_RELEASE = ScummVM-release.aab -DIST_FILES_HELP = $(PATH_DIST)/android-help.zip +DIST_FILES_PLATFORM = $(PATH_DIST)/android-help.zip $(PATH_DIST)/gamepad.svg $(PATH_BUILD): $(MKDIR) $(PATH_BUILD) @@ -38,9 +38,9 @@ $(PATH_BUILD)/local.properties: configure.stamp | $(PATH_BUILD) $(PATH_BUILD)/src.properties: configure.stamp | $(PATH_BUILD) $(ECHO) "srcdir=$(realpath $(srcdir))\n" > $(PATH_BUILD)/src.properties -$(PATH_BUILD_ASSETS): $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_HELP) | $(PATH_BUILD) +$(PATH_BUILD_ASSETS): $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_PLATFORM) | $(PATH_BUILD) $(INSTALL) -d $(PATH_BUILD_ASSETS) - $(INSTALL) -c -m 644 $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_HELP) $(PATH_BUILD_ASSETS)/ + $(INSTALL) -c -m 644 $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_PLATFORM) $(PATH_BUILD_ASSETS)/ ifneq ($(DIST_FILES_SHADERS),) $(INSTALL) -d $(PATH_BUILD_ASSETS)/shaders $(INSTALL) -c -m 644 $(DIST_FILES_SHADERS) $(PATH_BUILD_ASSETS)/shaders diff --git a/backends/platform/android/jni-android.cpp b/backends/platform/android/jni-android.cpp index e2205e269c04f..397c51e669c07 100644 --- a/backends/platform/android/jni-android.cpp +++ b/backends/platform/android/jni-android.cpp @@ -92,7 +92,6 @@ jmethodID JNI::_MID_isConnectionLimited = 0; jmethodID JNI::_MID_setWindowCaption = 0; jmethodID JNI::_MID_showVirtualKeyboard = 0; jmethodID JNI::_MID_showOnScreenControls = 0; -jmethodID JNI::_MID_getBitmapResource = 0; jmethodID JNI::_MID_setTouchMode = 0; jmethodID JNI::_MID_getTouchMode = 0; jmethodID JNI::_MID_setOrientation = 0; @@ -437,71 +436,6 @@ void JNI::showOnScreenControls(int enableMask) { } } -Graphics::Surface *JNI::getBitmapResource(BitmapResources resource) { - JNIEnv *env = JNI::getEnv(); - - jobject bitmap = env->CallObjectMethod(_jobj, _MID_getBitmapResource, (int) resource); - - if (env->ExceptionCheck()) { - LOGE("Can't get bitmap resource"); - - env->ExceptionDescribe(); - env->ExceptionClear(); - - return nullptr; - } - - if (bitmap == nullptr) { - LOGE("Bitmap resource was not found"); - return nullptr; - } - - AndroidBitmapInfo bitmap_info; - if (AndroidBitmap_getInfo(env, bitmap, &bitmap_info) != ANDROID_BITMAP_RESULT_SUCCESS) { - LOGE("Error reading bitmap info"); - env->DeleteLocalRef(bitmap); - return nullptr; - } - - Graphics::PixelFormat fmt; - switch(bitmap_info.format) { - case ANDROID_BITMAP_FORMAT_RGBA_8888: -#ifdef SCUMM_BIG_ENDIAN - fmt = Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0); -#else - fmt = Graphics::PixelFormat(4, 8, 8, 8, 8, 0, 8, 16, 24); -#endif - break; - case ANDROID_BITMAP_FORMAT_RGBA_4444: - fmt = Graphics::PixelFormat(2, 4, 4, 4, 4, 12, 8, 4, 0); - break; - case ANDROID_BITMAP_FORMAT_RGB_565: - fmt = Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0); - break; - default: - LOGE("Bitmap has unsupported format"); - env->DeleteLocalRef(bitmap); - return nullptr; - } - - void *src_pixels = nullptr; - if (AndroidBitmap_lockPixels(env, bitmap, &src_pixels) != ANDROID_BITMAP_RESULT_SUCCESS) { - LOGE("Error locking bitmap pixels"); - env->DeleteLocalRef(bitmap); - return nullptr; - } - - Graphics::Surface *ret = new Graphics::Surface(); - ret->create(bitmap_info.width, bitmap_info.height, fmt); - ret->copyRectToSurface(src_pixels, bitmap_info.stride, - 0, 0, bitmap_info.width, bitmap_info.height); - - AndroidBitmap_unlockPixels(env, bitmap); - env->DeleteLocalRef(bitmap); - - return ret; -} - void JNI::setTouchMode(int touchMode) { JNIEnv *env = JNI::getEnv(); @@ -822,7 +756,6 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager, FIND_METHOD(, isConnectionLimited, "()Z"); FIND_METHOD(, showVirtualKeyboard, "(Z)V"); FIND_METHOD(, showOnScreenControls, "(I)V"); - FIND_METHOD(, getBitmapResource, "(I)Landroid/graphics/Bitmap;"); FIND_METHOD(, setTouchMode, "(I)V"); FIND_METHOD(, getTouchMode, "()I"); FIND_METHOD(, setOrientation, "(I)V"); diff --git a/backends/platform/android/jni-android.h b/backends/platform/android/jni-android.h index 9cbdc98a3f3e8..28259821ea889 100644 --- a/backends/platform/android/jni-android.h +++ b/backends/platform/android/jni-android.h @@ -46,10 +46,6 @@ class JNI { virtual ~JNI(); public: - enum struct BitmapResources { - TOUCH_ARROWS_BITMAP = 0 - }; - static bool pause; static sem_t pause_sem; @@ -94,7 +90,6 @@ class JNI { static bool isConnectionLimited(); static void showVirtualKeyboard(bool enable); static void showOnScreenControls(int enableMask); - static Graphics::Surface *getBitmapResource(BitmapResources resource); static void setTouchMode(int touchMode); static int getTouchMode(); static void setOrientation(int touchMode); @@ -157,7 +152,6 @@ class JNI { static jmethodID _MID_setWindowCaption; static jmethodID _MID_showVirtualKeyboard; static jmethodID _MID_showOnScreenControls; - static jmethodID _MID_getBitmapResource; static jmethodID _MID_setTouchMode; static jmethodID _MID_getTouchMode; static jmethodID _MID_setOrientation; diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVM.java b/backends/platform/android/org/scummvm/scummvm/ScummVM.java index 59ce7fcbcd5e7..5268d72b6c264 100644 --- a/backends/platform/android/org/scummvm/scummvm/ScummVM.java +++ b/backends/platform/android/org/scummvm/scummvm/ScummVM.java @@ -84,7 +84,6 @@ final public native void pushEvent(int type, int arg1, int arg2, int arg3, abstract protected void setWindowCaption(String caption); abstract protected void showVirtualKeyboard(boolean enable); abstract protected void showOnScreenControls(int enableMask); - abstract protected Bitmap getBitmapResource(int resource); abstract protected void setTouchMode(int touchMode); abstract protected int getTouchMode(); abstract protected void setOrientation(int orientation); diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java index b9cf14c7e813d..138a0d8b1b137 100644 --- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java +++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java @@ -777,23 +777,6 @@ public void run() { }); } - @Override - protected Bitmap getBitmapResource(int resource) { - int id; - switch(resource) { - case 0: // TOUCH_ARROWS_BITMAP - id = R.drawable.touch_arrows; - break; - default: - return null; - } - - BitmapFactory.Options opts = new BitmapFactory.Options(); - opts.inScaled = false; - - return BitmapFactory.decodeResource(getResources(), id, opts); - } - @Override protected void setTouchMode(final int touchMode) { if (_events.getTouchMode() == touchMode) { diff --git a/backends/platform/android/touchcontrols.cpp b/backends/platform/android/touchcontrols.cpp index 380513daa5b0e..e439e161b2060 100644 --- a/backends/platform/android/touchcontrols.cpp +++ b/backends/platform/android/touchcontrols.cpp @@ -38,12 +38,73 @@ #define FORBIDDEN_SYMBOL_EXCEPTION_printf #include "backends/platform/android/android.h" +#include "backends/platform/android/jni-android.h" #include "backends/platform/android/touchcontrols.h" +#include "common/file.h" +#include "graphics/svg.h" + +#define SVG_WIDTH 384 +#define SVG_HEIGHT 256 +#define BG_WIDTH 128 +#define BG_HEIGHT 128 +#define ARROW_DEAD 21 +#define ARROW_SUPER 42 +#define ARROW_WIDTH 64 +#define ARROW_SEMI_HEIGHT 52 +#define ARROW_HEIGHT 64 +#define ARROW_HL_LEFT 0 +#define ARROW_HL_TOP 128 +#define BUTTON_DEAD 12 +#define BUTTON_END 64 +#define BUTTON_WIDTH 64 +#define BUTTON_HEIGHT 64 +#define BUTTON_HL_LEFT 128 +#define BUTTON_HL_TOP 128 +#define BUTTON2_HL_LEFT 256 +#define BUTTON2_HL_TOP 128 + +// The scale factor is stored as a fixed point 30.2 bits +// This avoids floating point operations +#define SCALE_FACTOR_FXP 4 + +// gamepad.svg was designed with a basis of 128x128 sized widget +// As it's too small on screen, we apply a factor of 1.5x +// It can be tuned here and in SVG viewBox without changing anything else +#define SVG_UNSCALED(x) ((x) * 3 / 2) +#define SVG_SQ_UNSCALED(x) ((x * x) * 9 / 4) + +#define SVG_SCALED(x) (SVG_UNSCALED(x) * _scale) +#define SVG_SQ_SCALED(x) (SVG_SQ_UNSCALED(x) * _scale2) +#define SCALED_PIXELS(x) ((x) / SCALE_FACTOR_FXP) + +#define SVG_PIXELS(x) SCALED_PIXELS(SVG_SCALED(x)) + TouchControls::TouchControls() : _drawer(nullptr), _screen_width(0), - _screen_height(0) { + _screen_height(0), + _svg(nullptr), + _scale(0), + _scale2(0) { +} + +void TouchControls::init(float scale) { + _scale = scale * SCALE_FACTOR_FXP; + // As scale is small, this should fit in int + _scale2 = _scale * _scale; + + Common::File stream; + + if (!stream.open("gamepad.svg")) { + error("Failed to fetch gamepad image"); + } + + _svg = new Graphics::SVGBitmap(&stream, SVG_PIXELS(SVG_WIDTH), SVG_PIXELS(SVG_HEIGHT)); +} + +TouchControls::~TouchControls() { + delete _svg; } TouchControls::Function TouchControls::getFunction(int x, int y) { @@ -52,106 +113,376 @@ TouchControls::Function TouchControls::getFunction(int x, int y) { return kFunctionNone; } - float xPercent = float(x) / _screen_width; + // Exclude areas reserved for system + if ((x < JNI::gestures_insets[0] * SCALE_FACTOR_FXP) || + (y < JNI::gestures_insets[1] * SCALE_FACTOR_FXP) || + (x >= _screen_width - JNI::gestures_insets[2] * SCALE_FACTOR_FXP) || + (y >= _screen_height - JNI::gestures_insets[3] * SCALE_FACTOR_FXP)) { + return kFunctionNone; + } + + float xRatio = float(x) / _screen_width; - if (xPercent < 0.3) { - return kFunctionJoystick; - } else if (xPercent < 0.8) { + if (xRatio < 0.3) { + return kFunctionLeft; + } else if (xRatio < 0.7) { return kFunctionCenter; } else { return kFunctionRight; } } -void TouchControls::touchToJoystickState(int dX, int dY, FunctionState &state) { - int sqNorm = dX * dX + dY * dY; - if (sqNorm < 50 * 50) { +void TouchControls::maskToLeftButtons(uint32 oldMask, uint32 newMask) { + static const Common::JoystickButton buttons[] = { + Common::JOYSTICK_BUTTON_DPAD_UP, Common::JOYSTICK_BUTTON_DPAD_RIGHT, + Common::JOYSTICK_BUTTON_DPAD_DOWN, Common::JOYSTICK_BUTTON_DPAD_LEFT + }; + + uint32 diff = newMask ^ oldMask; + + for(int i = 0, m = 1; i < ARRAYSIZE(buttons); i++, m <<= 1) { + if (!(diff & m)) { + continue; + } + if (oldMask & m) { + buttonUp(buttons[i]); + } + } + if (diff & 16) { + if (oldMask & 16) { + buttonUp(Common::JOYSTICK_BUTTON_RIGHT_SHOULDER); + } else { + buttonDown(Common::JOYSTICK_BUTTON_RIGHT_SHOULDER); + } + } + for(int i = 0, m = 1; i < ARRAYSIZE(buttons); i++, m <<= 1) { + if (!(diff & m)) { + continue; + } + if (newMask & m) { + buttonDown(buttons[i]); + } + } +} + +void TouchControls::touchLeft(int dX, int dY, Action action, FunctionState &state) { + if (action == JACTION_CANCEL || + action == JACTION_UP) { + maskToLeftButtons(state.mask, 0); + state.reset(); return; } - if (dY > abs(dX)) { - state.main = Common::JOYSTICK_BUTTON_DPAD_DOWN; - state.clip = Common::Rect(256, 0, 384, 128); - } else if (dX > abs(dY)) { - state.main = Common::JOYSTICK_BUTTON_DPAD_RIGHT; - state.clip = Common::Rect(128, 0, 256, 128); - } else if (-dY > abs(dX)) { - state.main = Common::JOYSTICK_BUTTON_DPAD_UP; - state.clip = Common::Rect(0, 0, 128, 128); - } else if (-dX > abs(dY)) { - state.main = Common::JOYSTICK_BUTTON_DPAD_LEFT; - state.clip = Common::Rect(384, 0, 512, 128); - } else { + FunctionState newState; + + // norm 2 squared (to avoid square root) + unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY); + + if (sqNorm >= SVG_SQ_SCALED(ARROW_DEAD)) { + // We are far enough from the center + // For right we use angles -60,60 as a sensitive zone + // For left it's the same but mirrored in negative + // We must be between the two lines which are of tan(60),tan(-60) and this corrsponds to sqrt(3) + // For up down we use angles -30,30 as a sensitive zone + // We must be outside the two lines which are of tan(30),tan(-30) and this corrsponds to 1/sqrt(3) + /* + static const double SQRT3 = 1.73205080756887719318; + unsigned int sq3 = SQRT3 * abs(dX); + */ + // Optimize by using an approximation of sqrt(3) + unsigned int sq3 = abs(dX) * 51409 / 29681; + unsigned int isq3 = abs(dX) * 29681 / 51409; + + unsigned int adY = abs(dY); + + if (adY <= sq3) { + // Left or right + if (dX < 0) { + newState.mask |= 8; + } else { + newState.mask |= 2; + } + } + if (adY >= isq3) { + // Up or down + if (dY < 0) { + newState.mask |= 1; + } else { + newState.mask |= 4; + } + + } + } + + if (sqNorm > SVG_SQ_SCALED(ARROW_SUPER)) { + newState.mask |= 16; + } + + if (state.mask != newState.mask) { + maskToLeftButtons(state.mask, newState.mask); + } + + state = newState; +} + +void TouchControls::drawLeft(int centerX, int centerY, const FunctionState &state) { + // Draw background + { + Common::Rect clip(0, 0, BG_WIDTH, BG_HEIGHT); + TouchControls::drawSurface(centerX, centerY, -clip.width() / 2, -clip.height() / 2, clip); + } + + if (state.mask == 0) { return; } - if (sqNorm > 20000) { - state.modifier = Common::JOYSTICK_BUTTON_RIGHT_SHOULDER; + + // Width and height here are rotated for left/right + uint16 width = ARROW_WIDTH; + uint16 height; + if (state.mask & 16) { + height = ARROW_HEIGHT; + } else { + height = ARROW_SEMI_HEIGHT; } + // We can draw multiple arrows + if (state.mask & 1) { + // Draw UP + Common::Rect clip(width, height); + clip.translate(0, ARROW_HL_TOP + ARROW_HEIGHT - height); + int16 offX = -1, offY = -2; + TouchControls::drawSurface(centerX, centerY, offX * clip.width() / 2, offY * clip.height() / 2, clip); + } + if (state.mask & 2) { + // Draw RIGHT + Common::Rect clip(height, width); + clip.translate(ARROW_WIDTH, ARROW_HL_TOP); + int16 offX = 0, offY = -1; + TouchControls::drawSurface(centerX, centerY, offX * clip.width() / 2, offY * clip.height() / 2, clip); + } + if (state.mask & 4) { + // Draw DOWN + Common::Rect clip(width, height); + clip.translate(0, ARROW_HL_TOP + ARROW_HEIGHT); + int16 offX = -1, offY = 0; + TouchControls::drawSurface(centerX, centerY, offX * clip.width() / 2, offY * clip.height() / 2, clip); + } + if (state.mask & 8) { + // Draw LEFT + Common::Rect clip(height, width); + clip.translate(ARROW_WIDTH + ARROW_WIDTH - height, ARROW_HL_TOP + ARROW_HEIGHT); + int16 offX = -2, offY = -1; + TouchControls::drawSurface(centerX, centerY, offX * clip.width() / 2, offY * clip.height() / 2, clip); + } } -void TouchControls::touchToCenterState(int dX, int dY, FunctionState &state) { - int sqNorm = dX * dX + dY * dY; - if (sqNorm < 50 * 50) { - state.main = Common::JOYSTICK_BUTTON_GUIDE; +void TouchControls::touchRight(int dX, int dY, Action action, FunctionState &state) { + if (action == JACTION_CANCEL) { + state.reset(); + return; + } + + // norm 2 squared (to avoid square root) + unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY); + + if (sqNorm >= SVG_SQ_SCALED(BUTTON_DEAD) && sqNorm <= SVG_SQ_SCALED(BUTTON_END)) { + // We are far enough from the center + // For right we use angles -45,45 as a sensitive zone + // For left it's the same but mirrored in negative + // We must be between the two lines which are of tan(45),tan(-45) and this corrsponds to 1 + // For up down we use angles -45,45 as a sensitive zone + // We must be outside the two lines which are of tan(45),tan(-45) and this corrsponds to 1 + unsigned int adX = abs(dX); + unsigned int adY = abs(dY); + + if (adY <= adX) { + // X or B + if (dX < 0) { + state.mask = 4; + } else { + state.mask = 2; + } + } else { + // Y or A + if (dY < 0) { + state.mask = 1; + } else { + state.mask = 3; + } + + } + } else { + state.reset(); + } + + static const Common::JoystickButton buttons[] = { + Common::JOYSTICK_BUTTON_Y, Common::JOYSTICK_BUTTON_B, + Common::JOYSTICK_BUTTON_A, Common::JOYSTICK_BUTTON_X + }; + static const Common::JoystickButton modifiers[] = { + Common::JOYSTICK_BUTTON_INVALID, Common::JOYSTICK_BUTTON_INVALID, + Common::JOYSTICK_BUTTON_INVALID, Common::JOYSTICK_BUTTON_INVALID + }; + if (action == JACTION_UP && state.mask) { + buttonDown(modifiers[state.mask - 1]); + buttonPress(buttons[state.mask - 1]); + buttonUp(modifiers[state.mask - 1]); } } -void TouchControls::touchToRightState(int dX, int dY, FunctionState &state) { - if (dX * dX + dY * dY < 100 * 100) { +void TouchControls::drawRight(int centerX, int centerY, const FunctionState &state) { + // Draw background + { + Common::Rect clip(BG_WIDTH, 0, 2 * BG_WIDTH, BG_HEIGHT); + TouchControls::drawSurface(centerX, centerY, -clip.width() / 2, -clip.height() / 2, clip); + } + + if (state.mask == 0) { return; } - if (dX > abs(dY)) { - // right - state.main = Common::JOYSTICK_BUTTON_RIGHT_STICK; - state.clip = Common::Rect(512, 128, 640, 256); + + Common::Rect clip(BUTTON_WIDTH, BUTTON_HEIGHT); + + int16 offX, offY; + + if (state.mask == 1) { + // Draw Y + clip.translate(BUTTON_HL_LEFT, BUTTON_HL_TOP); + offX = -1; + offY = -2; + } else if (state.mask == 2) { + // Draw B + clip.translate(BUTTON_HL_LEFT + BUTTON_WIDTH, BUTTON_HL_TOP); + offX = 0; + offY = -1; + } else if (state.mask == 3) { + // Draw A + clip.translate(BUTTON_HL_LEFT, BUTTON_HL_TOP + BUTTON_HEIGHT); + offX = -1; + offY = 0; + } else if (state.mask == 4) { + // Draw X + clip.translate(BUTTON_HL_LEFT + BUTTON_WIDTH, BUTTON_HL_TOP + BUTTON_HEIGHT); + offX = -2; + offY = -1; + } else { return; - } else if (-dX > abs(dY)) { - // left - state.main = Common::JOYSTICK_BUTTON_LEFT_STICK; - state.clip = Common::Rect(512, 0, 640, 128); + } + TouchControls::drawSurface(centerX, centerY, offX * clip.width() / 2, offY * clip.height() / 2, clip); +} + +void TouchControls::touchCenter(int dX, int dY, Action action, FunctionState &state) { + if (action == JACTION_CANCEL) { + state.reset(); return; } - static Common::JoystickButton buttons[5] = { - Common::JOYSTICK_BUTTON_Y, Common::JOYSTICK_BUTTON_B, // top zone - Common::JOYSTICK_BUTTON_INVALID, // center - Common::JOYSTICK_BUTTON_A, Common::JOYSTICK_BUTTON_X // bottom zone - }; + // norm 2 squared (to avoid square root) + unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY); + + if (sqNorm >= SVG_SQ_SCALED(BUTTON_DEAD) && sqNorm <= SVG_SQ_SCALED(BUTTON_END)) { + // We are far enough from the center + // For right we use angles -45,45 as a sensitive zone + // For left it's the same but mirrored in negative + // We must be between the two lines which are of tan(45),tan(-45) and this corrsponds to 1 + // For up down we use angles -45,45 as a sensitive zone + // We must be outside the two lines which are of tan(45),tan(-45) and this corrsponds to 1 + unsigned int adX = abs(dX); + unsigned int adY = abs(dY); + + if (adY <= adX) { + // X or B + if (dX < 0) { + state.mask = 4; + } else { + state.mask = 2; + } + } else { + // Y or A + if (dY < 0) { + state.mask = 1; + } else { + state.mask = 3; + } - static int16 clips[5][4] = { - { 0, 128, 128, 256 }, // y - { 128, 128, 256, 256 }, // b - { 0, 0, 0, 0 }, // center - { 256, 128, 384, 256 }, // a - { 384, 128, 512, 256 } // x + } + } else { + state.reset(); + } + + static const Common::JoystickButton buttons[] = { + Common::JOYSTICK_BUTTON_GUIDE, Common::JOYSTICK_BUTTON_RIGHT_STICK, + Common::JOYSTICK_BUTTON_START, Common::JOYSTICK_BUTTON_LEFT_STICK + }; + static const Common::JoystickButton modifiers[] = { + Common::JOYSTICK_BUTTON_INVALID, Common::JOYSTICK_BUTTON_INVALID, + Common::JOYSTICK_BUTTON_INVALID, Common::JOYSTICK_BUTTON_INVALID }; - static const uint offset = (ARRAYSIZE(buttons) - 1) / 2; + if (action == JACTION_UP && state.mask) { + buttonDown(modifiers[state.mask - 1]); + buttonPress(buttons[state.mask - 1]); + buttonUp(modifiers[state.mask - 1]); + } +} - int idx = (dY / 100) + offset; - if (idx < 0) { - idx = 0; +void TouchControls::drawCenter(int centerX, int centerY, const FunctionState &state) { + // Draw background + { + Common::Rect clip(BG_WIDTH * 2, 0, 3 * BG_WIDTH, BG_HEIGHT); + TouchControls::drawSurface(centerX, centerY, -clip.width() / 2, -clip.height() / 2, clip); } - if (idx >= ARRAYSIZE(buttons)) { - idx = ARRAYSIZE(buttons) - 1; + + if (state.mask == 0) { + return; } - state.main = buttons[idx]; - state.clip = Common::Rect(clips[idx][0], clips[idx][1], clips[idx][2], clips[idx][3]); + + Common::Rect clip(BUTTON_WIDTH, BUTTON_HEIGHT); + + int16 offX, offY; + + if (state.mask == 1) { + // Draw Y + clip.translate(BUTTON2_HL_LEFT, BUTTON2_HL_TOP); + offX = -1; + offY = -2; + } else if (state.mask == 2) { + // Draw B + clip.translate(BUTTON2_HL_LEFT + BUTTON_WIDTH, BUTTON2_HL_TOP); + offX = 0; + offY = -1; + } else if (state.mask == 3) { + // Draw A + clip.translate(BUTTON2_HL_LEFT, BUTTON2_HL_TOP + BUTTON_HEIGHT); + offX = -1; + offY = 0; + } else if (state.mask == 4) { + // Draw X + clip.translate(BUTTON2_HL_LEFT + BUTTON_WIDTH, BUTTON2_HL_TOP + BUTTON_HEIGHT); + offX = -2; + offY = -1; + } else { + return; + } + TouchControls::drawSurface(centerX, centerY, offX * clip.width() / 2, offY * clip.height() / 2, clip); } -TouchControls::FunctionBehavior TouchControls::functionBehaviors[TouchControls::kFunctionMax + 1] = +const TouchControls::FunctionBehavior TouchControls::functionBehaviors[TouchControls::kFunctionMax + 1] = { - { touchToJoystickState, false, .2f, .5f }, - { touchToCenterState, true, .5f, .5f }, - { touchToRightState, true, .8f, .5f } + { &TouchControls::touchLeft, &TouchControls::drawLeft }, + { &TouchControls::touchRight, &TouchControls::drawRight }, + { &TouchControls::touchCenter, &TouchControls::drawCenter } }; -void TouchControls::init(TouchControlsDrawer *drawer, int width, int height) { +void TouchControls::setDrawer(TouchControlsDrawer *drawer, int width, int height) { _drawer = drawer; - _screen_width = width; - _screen_height = height; + _screen_width = width * SCALE_FACTOR_FXP; + _screen_height = height * SCALE_FACTOR_FXP; + + if (drawer) { + drawer->touchControlInitSurface(*_svg); + } } TouchControls::Pointer *TouchControls::getPointerFromId(int ptrId, bool createNotFound) { @@ -190,20 +521,22 @@ TouchControls::Pointer *TouchControls::findPointerFromFunction(Function function void TouchControls::draw() { assert(_drawer != nullptr); - for (uint i = 0; i < kFunctionMax + 1; i++) { - FunctionState &state = _functionStates[i]; - FunctionBehavior behavior = functionBehaviors[i]; - - if (state.clip.isEmpty()) { + for (uint i = 0; i < kNumPointers; i++) { + Pointer &ptr = _pointers[i]; + if (!ptr.active) { continue; } - _drawer->touchControlDraw(_screen_width * behavior.xRatio, _screen_height * behavior.yRatio, - 64, 64, state.clip); - + if (ptr.function == kFunctionNone) { + continue; + } + const FunctionBehavior &behavior = functionBehaviors[ptr.function]; + (this->*behavior.draw)(ptr.startX, ptr.startY, ptr.state); } } void TouchControls::update(Action action, int ptrId, int x, int y) { + x *= SCALE_FACTOR_FXP; + y *= SCALE_FACTOR_FXP; if (action == JACTION_DOWN) { Pointer *ptr = getPointerFromId(ptrId, true); if (!ptr) { @@ -226,40 +559,25 @@ void TouchControls::update(Action action, int ptrId, int x, int y) { ptr->startX = ptr->currentX = x; ptr->startY = ptr->currentY = y; ptr->function = function; + + const FunctionBehavior &behavior = functionBehaviors[ptr->function]; + (this->*behavior.touch)(0, 0, action, ptr->state); + if (_drawer) { + _drawer->touchControlNotifyChanged(); + } } else if (action == JACTION_MOVE) { Pointer *ptr = getPointerFromId(ptrId, false); if (!ptr || ptr->function == kFunctionNone) { return; } - FunctionBehavior &behavior = functionBehaviors[ptr->function]; - ptr->currentX = x; ptr->currentY = y; int dX = x - ptr->startX; int dY = y - ptr->startY; - FunctionState newState; - functionBehaviors[ptr->function].touchToState(dX, dY, newState); - - FunctionState &oldState = _functionStates[ptr->function]; - - if (!behavior.pressOnRelease) { - // send key presses continuously - // first old remove main key, then update modifier, then press new main key - if (oldState.main != newState.main) { - buttonUp(oldState.main); - } - if (oldState.modifier != newState.modifier) { - buttonUp(oldState.modifier); - buttonDown(newState.modifier); - } - if (oldState.main != newState.main) { - buttonDown(newState.main); - } - } - oldState = newState; + (this->*functionBehaviors[ptr->function].touch)(dX, dY, action, ptr->state); if (_drawer) { _drawer->touchControlNotifyChanged(); } @@ -269,26 +587,10 @@ void TouchControls::update(Action action, int ptrId, int x, int y) { return; } - FunctionBehavior &behavior = functionBehaviors[ptr->function]; - FunctionState &functionState = _functionStates[ptr->function]; - - if (!behavior.pressOnRelease) { - // We sent key down continuously: buttonUp everything - buttonUp(functionState.main); - buttonUp(functionState.modifier); - } else { - int dX = x - ptr->startX; - int dY = y - ptr->startY; - - FunctionState newState; - functionBehaviors[ptr->function].touchToState(dX, dY, newState); - - buttonDown(newState.modifier); - buttonPress(newState.main); - buttonUp(newState.modifier); - } + int dX = x - ptr->startX; + int dY = y - ptr->startY; - functionState.reset(); + (this->*functionBehaviors[ptr->function].touch)(dX, dY, action, ptr->state); ptr->active = false; if (_drawer) { _drawer->touchControlNotifyChanged(); @@ -296,21 +598,16 @@ void TouchControls::update(Action action, int ptrId, int x, int y) { } else if (action == JACTION_CANCEL) { for (uint i = 0; i < kNumPointers; i++) { Pointer &ptr = _pointers[i]; - ptr.reset(); - } - for (uint i = 0; i < kFunctionMax + 1; i++) { - FunctionBehavior &behavior = functionBehaviors[i]; - FunctionState &functionState = _functionStates[i]; - - if (!behavior.pressOnRelease) { - // We sent key down continuously: buttonUp everything - buttonUp(functionState.main); - buttonUp(functionState.modifier); + if (!ptr.active) { + continue; } - - functionState.reset(); + if (ptr.function != kFunctionNone) { + (this->*functionBehaviors[ptr.function].touch)(0, 0, action, ptr.state); + } + ptr.reset(); } + if (_drawer) { _drawer->touchControlNotifyChanged(); } @@ -322,6 +619,7 @@ void TouchControls::buttonDown(Common::JoystickButton jb) { return; } + //LOGD("TouchControls::buttonDown: %d", jb); Common::Event ev; ev.type = Common::EVENT_JOYBUTTON_DOWN; ev.joystick.button = jb; @@ -333,6 +631,7 @@ void TouchControls::buttonUp(Common::JoystickButton jb) { return; } + //LOGD("TouchControls::buttonUp: %d", jb); Common::Event ev; ev.type = Common::EVENT_JOYBUTTON_UP; ev.joystick.button = jb; @@ -344,6 +643,7 @@ void TouchControls::buttonPress(Common::JoystickButton jb) { return; } + //LOGD("TouchControls::buttonPress: %d", jb); Common::Event ev1, ev2; ev1.type = Common::EVENT_JOYBUTTON_DOWN; ev1.joystick.button = jb; @@ -351,3 +651,11 @@ void TouchControls::buttonPress(Common::JoystickButton jb) { ev2.joystick.button = jb; dynamic_cast(g_system)->pushEvent(ev1, ev2); } + +void TouchControls::drawSurface(int x, int y, int offX, int offY, const Common::Rect &clip) { + Common::Rect clip_(SVG_PIXELS(clip.left), SVG_PIXELS(clip.top), + SVG_PIXELS(clip.right), SVG_PIXELS(clip.bottom)); + _drawer->touchControlDraw( + SCALED_PIXELS(x + SVG_SCALED(offX)), SCALED_PIXELS(y + SVG_SCALED(offY)), + clip_.width(), clip_.height(), clip_); +} diff --git a/backends/platform/android/touchcontrols.h b/backends/platform/android/touchcontrols.h index 09811a7c8af32..e8e2b342a23df 100644 --- a/backends/platform/android/touchcontrols.h +++ b/backends/platform/android/touchcontrols.h @@ -24,8 +24,13 @@ #include "common/events.h" +namespace Graphics { +class ManagedSurface; +} + class TouchControlsDrawer { public: + virtual void touchControlInitSurface(const Graphics::ManagedSurface &surf) = 0; virtual void touchControlNotifyChanged() = 0; virtual void touchControlDraw(int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) = 0; @@ -44,25 +49,39 @@ class TouchControls { }; TouchControls(); + ~TouchControls(); - void init(TouchControlsDrawer *drawer, int width, int height); + void init(float scale); + void setDrawer(TouchControlsDrawer *drawer, int width, int height); void draw(); void update(Action action, int ptr, int x, int y); private: TouchControlsDrawer *_drawer; - int _screen_width, _screen_height; + unsigned int _screen_width, _screen_height; + unsigned int _scale, _scale2; + + Graphics::ManagedSurface *_svg; enum Function { kFunctionNone = -1, - kFunctionJoystick = 0, - kFunctionCenter = 1, - kFunctionRight = 2, + kFunctionLeft = 0, + kFunctionRight = 1, + kFunctionCenter = 2, kFunctionMax = 2 }; Function getFunction(int x, int y); + struct FunctionState { + FunctionState() : mask(0) {} + void reset() { + mask = 0; + } + + uint32 mask; + }; + struct Pointer { Pointer() : id(-1), startX(-1), startY(-1), currentX(-1), currentY(-1), @@ -72,6 +91,8 @@ class TouchControls { startX = startY = currentX = currentY = -1; function = kFunctionNone; active = false; + + state.reset(); } int id; @@ -79,6 +100,8 @@ class TouchControls { uint16 currentX, currentY; Function function; bool active; + + FunctionState state; }; enum { kNumPointers = 5 }; @@ -87,38 +110,36 @@ class TouchControls { Pointer *getPointerFromId(int ptr, bool createNotFound); Pointer *findPointerFromFunction(Function function); - struct FunctionState { - FunctionState() : main(Common::JOYSTICK_BUTTON_INVALID), - modifier(Common::JOYSTICK_BUTTON_INVALID) {} - void reset() { - main = Common::JOYSTICK_BUTTON_INVALID; - modifier = Common::JOYSTICK_BUTTON_INVALID; - clip = Common::Rect(); - } - - Common::JoystickButton main; - Common::JoystickButton modifier; - Common::Rect clip; - }; - - FunctionState _functionStates[kFunctionMax + 1]; - void buttonDown(Common::JoystickButton jb); void buttonUp(Common::JoystickButton jb); void buttonPress(Common::JoystickButton jb); + /** + * Draws a part of the joystick surface on the screen + * + * @param x The left coordinate in fixed-point screen pixels + * @param y The top coordinate in fixed-point screen pixels + * @param offX The left offset in SVG pixels + * @param offY The top offset in SVG pixels + * @param clip The clipping rectangle in source surface in SVG pixels + */ + void drawSurface(int x, int y, int offX, int offY, const Common::Rect &clip); + /* Functions implementations */ struct FunctionBehavior { - void (*touchToState)(int, int, TouchControls::FunctionState &); - bool pressOnRelease; - float xRatio; - float yRatio; + void (TouchControls::*touch)(int, int, Action, TouchControls::FunctionState &); + void (TouchControls::*draw)(int, int, const TouchControls::FunctionState &); }; - static FunctionBehavior functionBehaviors[TouchControls::kFunctionMax + 1]; + static const FunctionBehavior functionBehaviors[TouchControls::kFunctionMax + 1]; + + void touchLeft(int dX, int dY, Action action, FunctionState &state); + void maskToLeftButtons(uint32 oldMask, uint32 newMask); + void drawLeft(int centerX, int centerY, const FunctionState &state); + void touchRight(int dX, int dY, Action action, FunctionState &state); + void drawRight(int centerX, int centerY, const FunctionState &state); + void touchCenter(int dX, int dY, Action action, FunctionState &state); + void drawCenter(int centerX, int centerY, const FunctionState &state); - static void touchToJoystickState(int dX, int dY, FunctionState &state); - static void touchToCenterState(int dX, int dY, FunctionState &state); - static void touchToRightState(int dX, int dY, FunctionState &state); }; #endif diff --git a/dists/android/gamepad.svg b/dists/android/gamepad.svg new file mode 100644 index 0000000000000..6595e5526d594 --- /dev/null +++ b/dists/android/gamepad.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dists/android/res/drawable/touch_arrows.png b/dists/android/res/drawable/touch_arrows.png deleted file mode 100644 index 816dabfd9984561ad644d54257f8609fb75f5abc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18028 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 if4IRO+W+!TeS663s@~_zd4Z?MFnGH9xvX