From 005d4c224fcf4e6eae972046d6a7fe43ce3172d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 19 Oct 2024 22:25:46 +0200 Subject: [PATCH] Platform: handle (multi-)touch input in AndroidApplication. As usual, the most trash fire platform of them all. Ugh. I chose to ignore certain aspects and suggestions and made it behave more like Emscripten and SDL2, because that makes more sense to me. Co-authored-by: nodoteve --- doc/changelog.dox | 17 +- doc/credits.dox | 2 + src/Magnum/Platform/AndroidApplication.cpp | 345 +++++++++++++++--- src/Magnum/Platform/AndroidApplication.h | 179 ++++++++- src/Magnum/Platform/EmscriptenApplication.cpp | 2 +- src/Magnum/Platform/EmscriptenApplication.h | 2 +- src/Magnum/Platform/Sdl2Application.cpp | 8 +- src/Magnum/Platform/Sdl2Application.h | 2 +- .../Platform/Test/AndroidApplicationTest.cpp | 21 +- 9 files changed, 492 insertions(+), 86 deletions(-) diff --git a/doc/changelog.dox b/doc/changelog.dox index 6e245070f3..b29041dd29 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -339,14 +339,17 @@ See also: @relativeref{Platform::GlfwApplication,tickEvent()} to match the interface of @ref Platform::Sdl2Application (see [mosra/magnum#577](https://github.com/mosra/magnum/issues/577) and [mosra/magnum#580](https://github.com/mosra/magnum/pull/580)) -- Multi-touch support in @ref Platform::Sdl2Application and - @ref Platform::EmscriptenApplication through new +- Multi-touch support in @ref Platform::Sdl2Application, + @ref Platform::EmscriptenApplication and + @ref Platform::AndroidApplication through new @relativeref{Platform::Sdl2Application,PointerEvent} and - @relativeref{Platform::Sdl2Application,PointerMoveEvent} that unify mouse - and touch input events. This also means @ref Platform::EmscriptenApplication - finally supports touch drag ([mosra/magnum#532](https://github.com/mosra/magnum/issues/532)) - --- no touch-to-mouse event passthrough needs to be implemented, it works - out of the box with the new pointer events. + @relativeref{Platform::Sdl2Application,PointerMoveEvent} that unify mouse, + pen and touch input events. This also means + @ref Platform::EmscriptenApplication finally supports touch drag + ([mosra/magnum#532](https://github.com/mosra/magnum/issues/532)) --- no + touch-to-mouse event passthrough needs to be implemented, it works out of + the box with the new pointer events. The Android implementation is + building upon [mosra/magnum#527](https://github.com/mosra/magnum/pull/527). @subsubsection changelog-latest-new-scenegraph SceneGraph library diff --git a/doc/credits.dox b/doc/credits.dox index d3a3c686f9..a7facb3eee 100644 --- a/doc/credits.dox +++ b/doc/credits.dox @@ -205,6 +205,8 @@ Are the below lists missing your name or something's wrong? library additions - **Nick Skelsey** ([\@NSkelsey](https://github.com/NSkelsey)) --- documentation copy-editing +- **[\@nodoteve](https://github.com/nodoteve)** --- initial multi-touch + support in @ref Platform::AndroidApplication - **[\@LB--](https://github.com/LB--)** --- warning fixes, Windows buildsystem improvements - **Olga Turanksaya** ([\@olga-python](https://github.com/olga-python)) --- diff --git a/src/Magnum/Platform/AndroidApplication.cpp b/src/Magnum/Platform/AndroidApplication.cpp index fccd148162..0a4df9a695 100644 --- a/src/Magnum/Platform/AndroidApplication.cpp +++ b/src/Magnum/Platform/AndroidApplication.cpp @@ -4,6 +4,7 @@ Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024 Vladimír Vondruš + Copyright © 2021 nodoteve Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -256,25 +257,79 @@ AndroidApplication::Pointers motionEventButtons(AInputEvent* event) { return pointers; } -AndroidApplication::Pointers motionEventPointers(AInputEvent* event, const AndroidApplication::Pointers pressedButtons) { - switch(AMotionEvent_getToolType(event, 0)) { +Containers::Pair motionEventPointers(AInputEvent* event, std::size_t i, const AndroidApplication::Pointers pressedButtons) { + switch(AMotionEvent_getToolType(event, i)) { case AMOTION_EVENT_TOOL_TYPE_MOUSE: /** @todo MouseButton4 / MouseButton5, once they're added & tested */ - return (AndroidApplication::Pointer::MouseLeft| - AndroidApplication::Pointer::MouseMiddle| - AndroidApplication::Pointer::MouseRight) & pressedButtons; + return {AndroidApplication::PointerEventSource::Mouse, + (AndroidApplication::Pointer::MouseLeft| + AndroidApplication::Pointer::MouseMiddle| + AndroidApplication::Pointer::MouseRight) & pressedButtons}; case AMOTION_EVENT_TOOL_TYPE_FINGER: - return AndroidApplication::Pointer::Finger; + return {AndroidApplication::PointerEventSource::Touch, + AndroidApplication::Pointer::Finger}; case AMOTION_EVENT_TOOL_TYPE_STYLUS: /** @todo use pressedButtonsPointers once there's additional pen button enum values */ - return AndroidApplication::Pointer::Pen; + return {AndroidApplication::PointerEventSource::Pen, + AndroidApplication::Pointer::Pen}; case AMOTION_EVENT_TOOL_TYPE_ERASER: - return AndroidApplication::Pointer::Eraser; + return {AndroidApplication::PointerEventSource::Touch, + AndroidApplication::Pointer::Eraser}; case AMOTION_EVENT_TOOL_TYPE_UNKNOWN: default: - return AndroidApplication::Pointer::Unknown; + return {AndroidApplication::PointerEventSource::Unknown, + AndroidApplication::Pointer::Unknown}; + } +} + +template Vector2 updatePreviousTouch(T(&previousTouches)[32], const std::int32_t id, const Containers::Optional& position) { + std::size_t firstFree = ~std::size_t{}; + for(std::size_t i = 0; i != Containers::arraySize(previousTouches); ++i) { + /* Previous position found */ + if(previousTouches[i].id == id) { + /* Update with the current position, return delta to previous */ + if(position) { + const Vector2 relative = *position - previousTouches[i].position; + previousTouches[i].position = *position; + return relative; + /* Clear previous position */ + } else { + previousTouches[i].id = ~Int{}; + return {}; + } + /* Unused slot, remember in case there won't be any previous position + found */ + } else if(previousTouches[i].id == ~Int{} && firstFree == ~std::size_t{}) { + firstFree = i; + } + } + + /* If we're not resetting the position and there's a place where to put the + new one, save. Otherwise don't do anything -- the touch that didn't fit + will always report as having no relative position. */ + if(position && firstFree != ~std::size_t{}) { + previousTouches[firstFree].id = id; + previousTouches[firstFree].position = *position; + } + + return {}; +} + +/* Unlike e.g. SDL, which guarantees that pointer IDs are unique among all + pointer types, here they of course don't care. So use the reported ID only + for touches and artificial constants for the rest. */ +std::int32_t pointerIdForSource(AndroidApplication::PointerEventSource source, std::int32_t id) { + switch(source) { + case AndroidApplication::PointerEventSource::Touch: + return id; + case AndroidApplication::PointerEventSource::Mouse: + return -1; + case AndroidApplication::PointerEventSource::Pen: + return -2; + case AndroidApplication::PointerEventSource::Unknown: + return -3; } } @@ -287,9 +342,46 @@ std::int32_t AndroidApplication::inputEvent(android_app* state, AInputEvent* eve const std::int32_t action = AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_MASK; switch(action) { case AMOTION_EVENT_ACTION_DOWN: - case AMOTION_EVENT_ACTION_UP: { - const Vector2 position{AMotionEvent_getX(event, 0), - AMotionEvent_getY(event, 0)}; + case AMOTION_EVENT_ACTION_POINTER_DOWN: + case AMOTION_EVENT_ACTION_UP: + case AMOTION_EVENT_ACTION_POINTER_UP: { + /* Figure out which pointer actually changed in given event, + because OF COURSE the API is so horrible that this is + non-trivial. For AMOTION_EVENT_ACTION_DOWN we assume it's + the first ever pointer being pressed, and thus the count + being 1, thus the pointer that changed is the first and + only. */ + std::int32_t pointerChanged; + if(action == AMOTION_EVENT_ACTION_DOWN) { + CORRADE_INTERNAL_ASSERT(AMotionEvent_getPointerCount(event) == 1); + pointerChanged = 0; + /* For AMOTION_EVENT_ACTION_UP it's ... apparently the last + remaining pointer going up. Not the primary one (see below + for the `primary` bit decision tree). The docs make it look + like the event also contains any other pointers, but it's + probably just mentioning the AMotionEvent_getHistoricalX() + etc. fields? Er? Why is it not mentioning that for for + AMOTION_EVENT_ACTION_POINTER_UP then? + https://developer.android.com/reference/android/view/MotionEvent#ACTION_UP */ + } else if(action == AMOTION_EVENT_ACTION_UP) { + CORRADE_INTERNAL_ASSERT(AMotionEvent_getPointerCount(event) == 1); + pointerChanged = 0; + /* The AMOTION_EVENT_ACTION_POINTER_DOWN/_UP actually mean a + secondary pointer was pressed or released. Who would have + thought. In that case, the actual changed pointer is given + to us with this fucking atrocity of a bitmask. Well, + alright, what can I do, but why such a bitmask couldn't be + done above as well, huh??? */ + } else if(action == AMOTION_EVENT_ACTION_POINTER_DOWN || + action == AMOTION_EVENT_ACTION_POINTER_UP) { + pointerChanged = (AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT; + } else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + + const bool press = + action == AMOTION_EVENT_ACTION_DOWN || + action == AMOTION_EVENT_ACTION_POINTER_DOWN; + const Vector2 position{AMotionEvent_getX(event, pointerChanged), + AMotionEvent_getY(event, pointerChanged)}; /* Query the currently pressed buttons. If this is not a mouse event, it'll give back garbage, but that's fine as we won't @@ -297,10 +389,67 @@ std::int32_t AndroidApplication::inputEvent(android_app* state, AInputEvent* eve a release, use the previously recorded pointers to figure out what was actually pressed. */ const Pointers pressedButtons = motionEventButtons(event); - const Pointers pointers = motionEventPointers(event, - action == AMOTION_EVENT_ACTION_DOWN ? + const Containers::Pair sourcePointers = + motionEventPointers(event, pointerChanged, press ? pressedButtons & ~app._previousPressedButtons : ~pressedButtons & app._previousPressedButtons); + const std::int32_t pointerId = pointerIdForSource(sourcePointers.first(), AMotionEvent_getPointerId(event, pointerChanged)); + + /* Decide whether this is a primary pointer. It's tempting to + use the distinction between _DOWN and _POINTER_DOWN to + distinguish a primary pointer from a secondary one, but + that'd be giving too much credit to this damn API. The + problem is that, if multiple fingers is pressed, _POINTER_UP + is fired if any of the secondary fingers are lifted, but + also if the primary finger is lifted. Which in turn means + the primary finger would be treated as secondary for the + up event, which is wrong. Second, then none of the remaining + fingers then have any reasonable way to get promoted to a + primary one, so they all stay secondary. BUT THEN, if the + last one of the secondary fingers gets lifted, _UP is fired + for it, so it suddenly becomes primary. Which a total trash + fire of a broken behavior. A mention worth a laugh is the + official Android developer blog, where in 2010 they + suggested the same thing --- in particular, when the primary + pointer is lifted, *an arbitrary one* from the rest is + chosen as primary. + https://android-developers.googleblog.com/2010/06/making-sense-of-multitouch.html + Eh. Maybe it makes sense for just two touches, but + definitely not for multiple. So let's just ignore all that + and do it by hand like in Sdl2Application and + EmscriptenApplication, to have consistent behavior across + all. + + Mouse and pen is always a primary pointer. */ + bool primary; + if(sourcePointers.first() == PointerEventSource::Mouse || + sourcePointers.first() == PointerEventSource::Pen) { + primary = true; + + /* For touch update primary finger info */ + } else if(sourcePointers.first() == PointerEventSource::Touch) { + /* If there's no primary finger yet and this is the first + finger pressed (i.e., what AMOTION_EVENT_ACTION_DOWN + implies), it becomes the primary finger. If the primary + finger is lifted, no other finger becomes primary until + all others are lifted as well. Again, this is the same + as in Sdl2Application and EmscriptenApplication. */ + if(app._primaryFingerId == ~Int{} && action == AMOTION_EVENT_ACTION_DOWN) { + CORRADE_INTERNAL_ASSERT(AMotionEvent_getPointerCount(event) == 1); + primary = true; + app._primaryFingerId = pointerId; + /* Otherwise, if this is the primary finger, mark it as + such */ + } else if(app._primaryFingerId == pointerId) { + primary = true; + /* ... but if it's a release, it's no longer primary */ + if(!press) + app._primaryFingerId = ~Int{}; + /* Otherwise this is not the primary finger */ + } else primary = false; + + /* Unknown pointer is probably not a primary one */ + } else primary = false; /* The expectation is that the difference betweeen the previously recorded set of pointers and current one will be @@ -312,8 +461,8 @@ std::int32_t AndroidApplication::inputEvent(android_app* state, AInputEvent* eve button caused the event. */ Pointer pointer; /* http://www.graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2 */ - if(pointers && !(UnsignedByte(pointers) & (UnsignedByte(pointers) - 1))) - pointer = Pointer(UnsignedByte(pointers)); + if(sourcePointers.second() && !(UnsignedByte(sourcePointers.second()) & (UnsignedByte(sourcePointers.second()) - 1))) + pointer = Pointer(UnsignedByte(sourcePointers.second())); else pointer = Pointer::Unknown; @@ -325,41 +474,102 @@ std::int32_t AndroidApplication::inputEvent(android_app* state, AInputEvent* eve multiple buttons being pressed didn't even trigger a press or release event, so this scenario is seemingly impossible to happen. */ - PointerEvent e{event, pointer}; - action == AMOTION_EVENT_ACTION_DOWN ? - app.pointerPressEvent(e) : app.pointerReleaseEvent(e); + + /* Assuming there's never more than 256 pointers in a single + event. Even that feels like a lot. */ + PointerEvent e{event, UnsignedByte(pointerChanged), sourcePointers.first(), pointer, primary, pointerId}; + press ? app.pointerPressEvent(e) : app.pointerReleaseEvent(e); /* Remember the currently pressed pointers for the next time */ app._previousPressedButtons = pressedButtons; - /* A touch screen doesn't have hover events, so remember the - position here as well. See below for why this has to be - remembered at all. */ - app._previousPointerPosition = position; + + /* If this is a touch press, remember its position for next + events. If this is a touch release, free the slot used by + this identifier for next events. Mouse and pen supports + hover and thus is updated only in AMOTION_EVENT_ACTION_MOVE. + See below for why this has to be remembered at all. */ + if(sourcePointers.first() == PointerEventSource::Touch) { + if(press) + updatePreviousTouch(app._previousTouches, pointerId, position); + else + updatePreviousTouch(app._previousTouches, pointerId, {}); + } return e.isAccepted(); } case AMOTION_EVENT_ACTION_MOVE: { const Pointers pressedButtons = motionEventButtons(event); - const Pointers pointers = motionEventPointers(event, pressedButtons); - const Vector2 position{AMotionEvent_getX(event, 0), - AMotionEvent_getY(event, 0)}; - const Vector2 relativePosition = - Math::isNan(app._previousPointerPosition).all() ? - Vector2{} : position - app._previousPointerPosition; - - /* The thing fires move events right after press events, with - the exact same position, for (emulated?) events at least. I - suppose that's some sort of unasked-for misfeature for - "improving" UX or fixing broken apps. Not interested, filter - those out if the relative position is zero and the set of - pressed buttons is the same. Hopefully not accepting those - doesn't lead to some strange behavior. */ + + /* Unlike AMOTION_EVENT_ACTION_DOWN / AMOTION_EVENT_ACTION_UP, + the move event can contain multiple moving pointers so + there's no mask telling which pointer moved. Go through all + and emit a move event only for those that changed. */ bool accepted = false; - if(relativePosition != Vector2{} || pressedButtons != app._previousPressedButtons) { - PointerMoveEvent e{event, {}, pointers, relativePosition}; - app.pointerMoveEvent(e); - accepted = e.isAccepted(); + const std::size_t pointerCount = AMotionEvent_getPointerCount(event); + for(std::size_t i = 0; i != pointerCount; ++i) { + const Containers::Pair sourcePointers = motionEventPointers(event, i, pressedButtons); + const std::int32_t pointerId = pointerIdForSource(sourcePointers.first(), AMotionEvent_getPointerId(event, i)); + const Vector2 position{AMotionEvent_getX(event, i), + AMotionEvent_getY(event, i)}; + + /* Query position relative to the previous one for the same + pointer type and identifier, update it with current. + Ideally I would get it somewhere from the platform APIs. + There's AMotionEvent_getHistoricalX()/Y(), but those are + coalesced events between the previous and currently + fired events, i.e. not the full delta. Documented here: + https://developer.android.com/reference/android/view/MotionEvent#batching + There's also AMOTION_EVENT_AXIS_RELATIVE_X/_Y, but + according to + https://developer.android.com/reference/android/view/MotionEvent#AXIS_X + the coordinate system is different for each event type, + and the last thing I want to do is adding special + handling for things the damn platform API should be + doing for me. */ + Vector2 relativePosition{NoInit}; + if(sourcePointers.first() == PointerEventSource::Mouse || + sourcePointers.first() == PointerEventSource::Pen) { + relativePosition = Math::isNan(app._previousHoverPointerPosition).all() ? + Vector2{} : position - app._previousHoverPointerPosition; + app._previousHoverPointerPosition = position; + } else if(sourcePointers.first() == PointerEventSource::Touch) { + relativePosition = updatePreviousTouch(app._previousTouches, pointerId, position); + } else { + /* No relative position for Unknown */ + relativePosition = {}; + } + + /* Decide whether this is a primary pointer. Mouse and pen + is always a primary pointer. */ + bool primary; + if(sourcePointers.first() == PointerEventSource::Mouse || + sourcePointers.first() == PointerEventSource::Pen) { + primary = true; + /* For touch, it's a primary finger only if it was + registered as such during the last press. If the primary + finger was lifted, no other finger will step into its + place until all others are lifted as well. */ + } else if(sourcePointers.first() == PointerEventSource::Touch) { + primary = app._primaryFingerId == pointerId; + /* Unknown pointer is probably not a primary one */ + } else primary = false; + + /* The thing fires move events right after press events, + with the exact same position, for (emulated?) events at + least. I suppose that's some sort of unasked-for + misfeature for "improving" UX or fixing broken apps. Not + interested, filter those out if the relative position is + zero and the set of pressed buttons is the same. + Hopefully not accepting those doesn't lead to some + strange behavior. */ + if(relativePosition != Vector2{} || pressedButtons != app._previousPressedButtons) { + /* Assuming there's never more than 256 pointers in a + single event. Even that feels like a lot. */ + PointerMoveEvent e{event, UnsignedByte(i), sourcePointers.first(), {}, sourcePointers.second(), primary, pointerId, relativePosition}; + app.pointerMoveEvent(e); + accepted = accepted || e.isAccepted(); + } } /* Remember the currently pressed buttons for the next time. @@ -368,34 +578,44 @@ std::int32_t AndroidApplication::inputEvent(android_app* state, AInputEvent* eve have a chance to resynchronize here. */ app._previousPressedButtons = pressedButtons; - /* Remember also the current position. There's - AMotionEvent_getHistoricalX()/Y(), but those are coalesced - events between the previous and currently fired move events, - i.e. not the full delta. Documented here: - https://developer.android.com/reference/android/view/MotionEvent#batching - There's also AMOTION_EVENT_AXIS_RELATIVE_X/_Y, but based on - https://developer.android.com/reference/android/view/MotionEvent#AXIS_X - the coordinate system is different for each event type, and - the last thing I want to do is adding special handling for - things the damn platform API should be doing for me. */ - app._previousPointerPosition = position; - return accepted; } /* Like AMOTION_EVENT_ACTION_MOVE, but without anything pressed */ case AMOTION_EVENT_ACTION_HOVER_MOVE: { - const Vector2 position{AMotionEvent_getX(event, 0), AMotionEvent_getY(event, 0)}; + /* Assuming there's just one pointer reported for a hover, and + it's either a mouse or a pen. Or something unknown. */ + CORRADE_INTERNAL_ASSERT(AMotionEvent_getPointerCount(event) == 1); + PointerEventSource source; + switch(AMotionEvent_getToolType(event, 0)) { + case AMOTION_EVENT_TOOL_TYPE_MOUSE: + source = AndroidApplication::PointerEventSource::Mouse; + break; + case AMOTION_EVENT_TOOL_TYPE_FINGER: + CORRADE_INTERNAL_ASSERT_UNREACHABLE(); + case AMOTION_EVENT_TOOL_TYPE_STYLUS: + case AMOTION_EVENT_TOOL_TYPE_ERASER: + source = AndroidApplication::PointerEventSource::Pen; + break; + case AMOTION_EVENT_TOOL_TYPE_UNKNOWN: + default: + source = AndroidApplication::PointerEventSource::Unknown; + break; + } + + const std::int32_t pointerId = pointerIdForSource(source, AMotionEvent_getPointerId(event, 0)); + const Vector2 position{AMotionEvent_getX(event, 0), + AMotionEvent_getY(event, 0)}; const Vector2 relativePosition = - Math::isNan(app._previousPointerPosition).all() ? - Vector2{} : position - app._previousPointerPosition; + Math::isNan(app._previousHoverPointerPosition).all() ? + Vector2{} : position - app._previousHoverPointerPosition; /* Similarly as with AMOTION_EVENT_ACTION_MOVE, the damn thing fires hover events with zero position delta when scrolling the mouse wheel. Useless, filter those away. */ bool accepted = false; if(relativePosition != Vector2{}) { - PointerMoveEvent e{event, {}, {}, relativePosition}; + PointerMoveEvent e{event, 0, source, {}, {}, true, pointerId, relativePosition}; app.pointerMoveEvent(e); accepted = e.isAccepted(); } @@ -405,7 +625,7 @@ std::int32_t AndroidApplication::inputEvent(android_app* state, AInputEvent* eve app._previousPressedButtons = {}; /* Remember the current position. See above for why AMotionEvent_getHistoricalX()/Y() is useless. */ - app._previousPointerPosition = position; + app._previousHoverPointerPosition = position; return accepted; } @@ -473,6 +693,9 @@ void AndroidApplication::exec(android_app* state, Containers::Pointer + Copyright © 2021 nodoteve Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), @@ -160,6 +161,37 @@ The application by default redirects @ref Corrade::Utility::Debug "Debug", output to Android log buffer with tag `"magnum"`, which can be then accessed through `logcat` utility. See also @ref Corrade::Utility::AndroidLogStreamBuffer for more information. + +@section Platform-AndroidApplication-touch Touch input + +The application recognizes touch and pen input and reports it as +@ref Pointer::Finger, @ref Pointer::Pen, @ref Pointer::Eraser with +@ref PointerEventSource::Touch and @ref PointerEventSource::Pen. + +In case of a multi-touch scenario, @ref PointerEvent::isPrimary() / +@ref PointerMoveEvent::isPrimary() can be used to distinguish the primary touch +from secondary. For example, if an application doesn't need to recognize +gestures like pinch to zoom or rotate, it can ignore all non-primary pointer +events. @ref PointerEventSource::Mouse and @ref PointerEventSource::Pen events +are always marked as primary, for touch input the first pressed finger is +marked as primary and all following pressed fingers are non-primary. Note that +there can be up to one primary pointer for each pointer event source, e.g. a +finger, pen and a mouse press may all be marked as primary. On the other hand, +in a multi-touch scenario, if the first (and thus primary) finger is lifted, no +other finger becomes primary until all others are lifted as well. This is +consistent with the logic in @ref Sdl2Application and @ref EmscriptenApplication +but may not necessarily match what other Android applications do. + +If gesture recognition is desirable, @ref PointerEvent::id() / +@ref PointerMoveEvent::id() contains a pointer ID that's unique among all +pointer event sources, which can be used to track movements of secondary, +tertiary and further touch points. The ID allocation isn't defined and you +can't rely on it to be contiguous or in any bounded range --- for example, +each new touch may generate a new ID that's only used until given finger is +lifted, and then never again, or the IDs may get heavily reused, being unique +only for the period given finger is pressed. For @ref PointerEventSource::Mouse +and @ref PointerEventSource::Pen the ID is a constant, as there's always just +a single mouse cursor or a pen stylus. */ class AndroidApplication { public: @@ -179,6 +211,7 @@ class AndroidApplication { /* The damn thing cannot handle forward enum declarations */ #ifndef DOXYGEN_GENERATING_OUTPUT + enum class PointerEventSource: UnsignedByte; enum class Pointer: UnsignedByte; #endif @@ -518,8 +551,22 @@ class AndroidApplication { EGLSurface _surface; EGLContext _glContext; - Vector2 _previousPointerPosition{Constants::nan()}; - /* Contains just the Mouse* values */ + /* We have no way to query previous pointer positions, so we have to + maintain them like this. For pointers capable of hover (mouse, pen) + the _previousHoverPointerPosition is used, NaN signalling that the + previous position is unknown. */ + Vector2 _previousHoverPointerPosition{Constants::nan()}; + /* For touches the _previousTouches array is used. The id is ~Int{} if + given slot is unused, 32 "should be enough" and is consistent with + what EmscriptenApplication does here. */ + struct { + Int id = ~Int{}; + Vector2 position; + } _previousTouches[32]; + Int _primaryFingerId = ~Int{}; + + /* In order to know which mouse button was pressed / released in + current event. Contains just the Mouse* values. */ Pointers _previousPressedButtons; /* Has to be in an Optional because it gets explicitly destroyed before @@ -530,6 +577,43 @@ class AndroidApplication { CORRADE_ENUMSET_FRIEND_OPERATORS(Flags) }; +/** +@brief Pointer event source +@m_since_latest + +@see @ref PointerEvent::source(), @ref PointerMoveEvent::source() +*/ +enum class AndroidApplication::PointerEventSource: UnsignedByte { + /** + * The event source is unknown. Corresponds to + * `AMOTION_EVENT_TOOL_TYPE_UNKNOWN` and other types not listed below. + * @see @ref Pointer::Unknown + */ + Unknown, + + /** + * The event is coming from a mouse. Corresponds to + * `AMOTION_EVENT_TOOL_TYPE_MOUSE`. + * @see @ref Pointer::MouseLeft, @ref Pointer::MouseMiddle, + * @ref Pointer::MouseRight + */ + Mouse, + + /** + * The event is coming from a touch contact, Corresponds to + * `AMOTION_EVENT_TOOL_TYPE_FINGER`. + * @see @ref Pointer::Finger + */ + Touch, + + /** + * The event is coming from a pen stylus. Corresponds to + * `AMOTION_EVENT_TOOL_TYPE_STYLUS` and `AMOTION_EVENT_TOOL_TYPE_ERASER`. + * @see @ref Pointer::Pen, @ref Pointer::Eraser + */ + Pen +}; + /** @brief Pointer type @m_since_latest @@ -541,24 +625,28 @@ enum class AndroidApplication::Pointer: UnsignedByte { /** * Unknown. Corresponds to `AMOTION_EVENT_TOOL_TYPE_UNKNOWN` and other * types not listed below. + * @see @ref PointerEventSource::Unknown */ Unknown = 1 << 0, /** * Left mouse button. Corresponds to `AMOTION_EVENT_TOOL_TYPE_MOUSE` and * `AMOTION_EVENT_BUTTON_PRIMARY`. + * @see @ref PointerEventSource::Mouse */ MouseLeft = 1 << 1, /** * Middle mouse button. Corresponds to `AMOTION_EVENT_TOOL_TYPE_MOUSE` and * `AMOTION_EVENT_BUTTON_SECONDARY`. + * @see @ref PointerEventSource::Mouse */ MouseMiddle = 1 << 2, /** * Right mouse button. Corresponds to `AMOTION_EVENT_TOOL_TYPE_MOUSE` and * `AMOTION_EVENT_BUTTON_TERTIARY`. + * @see @ref PointerEventSource::Mouse */ MouseRight = 1 << 3, @@ -566,14 +654,20 @@ enum class AndroidApplication::Pointer: UnsignedByte { possible to verify they match MouseButton4 / MouseButton5 in GlfwApplication and Sdl2Application */ - /** Finger. Corresponds to `AMOTION_EVENT_TOOL_TYPE_FINGER`. */ + /** + * Finger. Corresponds to `AMOTION_EVENT_TOOL_TYPE_FINGER`. + * @see @ref PointerEventSource::Touch + */ Finger = 1 << 4, /** @todo There's AMOTION_EVENT_TOOL_TYPE_PALM, but no corresponding constant on the Java MotionEvent class, and all links to it broken. Accidental omission? Some scrapped feature with leftover traces? */ - /** Pen. Corresponds to `AMOTION_EVENT_TOOL_TYPE_STYLUS`. */ + /** + * Pen. Corresponds to `AMOTION_EVENT_TOOL_TYPE_STYLUS`. + * @see @ref PointerEventSource::Pen + */ Pen = 1 << 5, /** @todo There's AMOTION_EVENT_BUTTON_STYLUS_PRIMARY and @@ -581,7 +675,10 @@ enum class AndroidApplication::Pointer: UnsignedByte { exist for EmscriptenApplication / Sdl3Application; implement chorded behavior for those like w/ mouse buttons */ - /** Eraser. Corresponds to `AMOTION_EVENT_TOOL_TYPE_ERASER`. */ + /** + * Eraser. Corresponds to `AMOTION_EVENT_TOOL_TYPE_ERASER`. + * @see @ref PointerEventSource::Pen + */ Eraser = 1 << 6 }; @@ -902,9 +999,32 @@ class AndroidApplication::PointerEvent: public InputEvent { /** @brief Moving is not allowed */ PointerEvent& operator=(PointerEvent&&) = delete; + /** @brief Pointer event source */ + PointerEventSource source() const { return _source; } + /** @brief Pointer type that was pressed or released */ Pointer pointer() const { return _pointer; } + /** + * @brief Whether the pointer is primary + * + * Useful to distinguish among multiple pointers in a multi-touch + * scenario. See @ref Platform-AndroidApplication-touch for more + * information. + */ + bool isPrimary() const { return _primary; } + + /** + * @brief Pointer ID + * + * Useful to distinguish among multiple pointers in a multi-touch + * scenario. See @ref Platform-AndroidApplication-touch for more + * information. + */ + /* Long is for consistency with Sdl2Application, Android uses just an + Int */ + Long id() const { return _id; } + /** * @brief Position * @@ -913,16 +1033,20 @@ class AndroidApplication::PointerEvent: public InputEvent { * pixel. */ Vector2 position() const { - return {AMotionEvent_getX(_event, 0), - AMotionEvent_getY(_event, 0)}; + return {AMotionEvent_getX(_event, _i), + AMotionEvent_getY(_event, _i)}; } private: friend AndroidApplication; - explicit PointerEvent(AInputEvent* event, Pointer pointer): InputEvent(event), _pointer{pointer} {} + explicit PointerEvent(AInputEvent* event, UnsignedByte i, PointerEventSource source, Pointer pointer, bool primary, Int id): InputEvent(event), _source{source}, _pointer{pointer}, _primary{primary}, _i{i}, _id{id} {} + const PointerEventSource _source; const Pointer _pointer; + const bool _primary; + const UnsignedByte _i; /* Pointer index, not ID */ + const Int _id; }; #ifdef MAGNUM_BUILD_DEPRECATED @@ -996,6 +1120,15 @@ class AndroidApplication::PointerMoveEvent: public InputEvent { /** @brief Moving is not allowed */ PointerMoveEvent& operator=(PointerMoveEvent&&) = delete; + /** + * @brief Pointer event source + * + * Can be used to distinguish which source the event is coming from in + * case it's a movement with both @ref pointer() and @ref pointers() + * being empty. + */ + PointerEventSource source() const { return _source; } + /** * @brief Pointer type that was added or removed from the set of pressed pointers * @@ -1016,6 +1149,26 @@ class AndroidApplication::PointerMoveEvent: public InputEvent { */ Pointers pointers() const { return _pointers; } + /** + * @brief Whether the pointer is primary + * + * Useful to distinguish among multiple pointers in a multi-touch + * scenario. See @ref Platform-AndroidApplication-touch for more + * information. + */ + bool isPrimary() const { return _primary; } + + /** + * @brief Pointer ID + * + * Useful to distinguish among multiple pointers in a multi-touch + * scenario. See @ref Platform-AndroidApplication-touch for more + * information. + */ + /* Long is for consistency with Sdl2Application, Android uses just an + Int */ + Long id() const { return _id; } + /** * @brief Position * @@ -1024,8 +1177,8 @@ class AndroidApplication::PointerMoveEvent: public InputEvent { * pixel. */ Vector2 position() const { - return {AMotionEvent_getX(_event, 0), - AMotionEvent_getY(_event, 0)}; + return {AMotionEvent_getX(_event, _i), + AMotionEvent_getY(_event, _i)}; } /** @@ -1042,10 +1195,14 @@ class AndroidApplication::PointerMoveEvent: public InputEvent { private: friend AndroidApplication; - explicit PointerMoveEvent(AInputEvent* event, Containers::Optional pointer, Pointers pointers, const Vector2& relativePosition): InputEvent{event}, _pointer{pointer}, _pointers{pointers}, _relativePosition{relativePosition} {} + explicit PointerMoveEvent(AInputEvent* event, UnsignedByte i, PointerEventSource source, Containers::Optional pointer, Pointers pointers, bool primary, Int id, const Vector2& relativePosition): InputEvent{event}, _source{source}, _pointer{pointer}, _pointers{pointers}, _primary{primary}, _i{i}, _id{id}, _relativePosition{relativePosition} {} + const PointerEventSource _source; const Containers::Optional _pointer; const Pointers _pointers; + const bool _primary; + const UnsignedByte _i; /* Pointer index, not ID */ + const Int _id; const Vector2 _relativePosition; }; diff --git a/src/Magnum/Platform/EmscriptenApplication.cpp b/src/Magnum/Platform/EmscriptenApplication.cpp index c8a739e1a3..c92b167d12 100644 --- a/src/Magnum/Platform/EmscriptenApplication.cpp +++ b/src/Magnum/Platform/EmscriptenApplication.cpp @@ -761,7 +761,7 @@ void EmscriptenApplication::setupCallbacks(bool resizable) { was empirically verified by looking at behavior of a mouse cursor on a multi-touch screen under X11, it's possible that other systems do it differently. The same logic is used in - Sdl2Application. */ + Sdl2Application and AndroidApplication. */ bool primary; if(app._primaryFingerId == ~Int{} && event->numTouches == 1) { primary = true; diff --git a/src/Magnum/Platform/EmscriptenApplication.h b/src/Magnum/Platform/EmscriptenApplication.h index dabbf1ab71..40d24a4f36 100644 --- a/src/Magnum/Platform/EmscriptenApplication.h +++ b/src/Magnum/Platform/EmscriptenApplication.h @@ -206,7 +206,7 @@ pointer for each pointer event source. For example, a finger and a mouse press may both be marked as primary. On the other hand, in a multi-touch scenario, if the first (and thus primary) finger is lifted, no other finger becomes primary until all others are lifted as well. This is consistent with the logic in -@ref Sdl2Application. +@ref Sdl2Application and @ref AndroidApplication. If gesture recognition is desirable, @ref PointerEvent::id() / @ref PointerMoveEvent::id() contains a pointer ID that's unique among all diff --git a/src/Magnum/Platform/Sdl2Application.cpp b/src/Magnum/Platform/Sdl2Application.cpp index 9a58201c6b..e4aaf6debd 100644 --- a/src/Magnum/Platform/Sdl2Application.cpp +++ b/src/Magnum/Platform/Sdl2Application.cpp @@ -1122,10 +1122,10 @@ bool Sdl2Application::mainLoopIteration() { was empirically verified by looking at behavior of a mouse cursor on a multi-touch screen under X11, it's possible that other systems do it differently. The same logic is used in - EmscriptenApplication. Also, right now there's an assumption - that there is just one touch device, fingers from different - touch devices would steal the primary bit from each other on - every press. */ + EmscriptenApplication and AndroidApplication. Also, right + now there's an assumption that there is just one touch + device, fingers from different touch devices would steal the + primary bit from each other on every press. */ bool primary; if(_primaryFingerId == ~Long{} && event.type == SDL_FINGERDOWN && SDL_GetNumTouchFingers(event.tfinger.touchId) == 1) { primary = true; diff --git a/src/Magnum/Platform/Sdl2Application.h b/src/Magnum/Platform/Sdl2Application.h index fab274e8de..c8636fe3b9 100644 --- a/src/Magnum/Platform/Sdl2Application.h +++ b/src/Magnum/Platform/Sdl2Application.h @@ -307,7 +307,7 @@ pointer for each pointer event source, e.g. a finger and a mouse press may both be marked as primary. On the other hand, in a multi-touch scenario, if the first (and thus primary) finger is lifted, no other finger becomes primary until all others are lifted as well. The same logic is implemented in -@ref EmscriptenApplication. +@ref EmscriptenApplication and @ref AndroidApplication. If gesture recognition is desirable, @ref PointerEvent::id() / @ref PointerMoveEvent::id() contains a pointer ID that's unique among all diff --git a/src/Magnum/Platform/Test/AndroidApplicationTest.cpp b/src/Magnum/Platform/Test/AndroidApplicationTest.cpp index b6c320d261..df440ec6ba 100644 --- a/src/Magnum/Platform/Test/AndroidApplicationTest.cpp +++ b/src/Magnum/Platform/Test/AndroidApplicationTest.cpp @@ -72,6 +72,21 @@ CORRADE_IGNORE_DEPRECATED_POP namespace Test { namespace { +static Debug& operator<<(Debug& debug, Application::PointerEventSource value) { + debug << "PointerEventSource" << Debug::nospace; + + switch(value) { + #define _c(value) case Application::PointerEventSource::value: return debug << "::" #value; + _c(Unknown) + _c(Mouse) + _c(Touch) + _c(Pen) + #undef _c + } + + return debug << "(" << Debug::nospace << UnsignedInt(value) << Debug::nospace << ")"; +} + Debug& operator<<(Debug& debug, Application::Pointers value) { return Containers::enumSetDebugOutput(debug, value, "Pointers{}", { Application::Pointer::Unknown, @@ -130,13 +145,13 @@ struct AndroidApplicationTest: Platform::Application { /* Set to 0 to test the deprecated mouse events instead */ #if 1 void pointerPressEvent(PointerEvent& event) override { - Debug{} << "pointer press:" << event.pointer() << Debug::packed << event.position(); + Debug{} << "pointer press:" << event.source() << event.pointer() << (event.isPrimary() ? "primary" : "secondary") << event.id() << Debug::packed << event.position(); } void pointerReleaseEvent(PointerEvent& event) override { - Debug{} << "pointer release:" << event.pointer() << Debug::packed << event.position(); + Debug{} << "pointer release:" << event.source() << event.pointer() << (event.isPrimary() ? "primary" : "secondary") << event.id() << Debug::packed << event.position(); } void pointerMoveEvent(PointerMoveEvent& event) override { - Debug{} << "pointer move:" << event.pointer() << event.pointers() << Debug::packed << event.position() << Debug::packed << event.relativePosition(); + Debug{} << "pointer move:" << event.source() << event.pointer() << event.pointers() << (event.isPrimary() ? "primary" : "secondary") << event.id() << Debug::packed << event.position() << Debug::packed << event.relativePosition(); } #else CORRADE_IGNORE_DEPRECATED_PUSH