diff --git a/doc/changelog.dox b/doc/changelog.dox index 5e6f45333..1593dfd24 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -352,6 +352,8 @@ See also: 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). + There's also a new @ref Platform::TwoFingerGesture helper for recognition + of common two-finger gestures for zoom, rotation and pan. @subsubsection changelog-latest-new-scenegraph SceneGraph library diff --git a/doc/platform.dox b/doc/platform.dox index 1cdbe0a6c..1315bba95 100644 --- a/doc/platform.dox +++ b/doc/platform.dox @@ -90,6 +90,12 @@ In addition to the above generalized interface, there's @relativeref{Platform::Sdl2Application,scrollEvent()} providing 2D scroll events from a mouse wheel, trackball or touchpad. +See @ref Platform-Sdl2Application-touch, +@ref Platform-EmscriptenApplication-touch and +@ref Platform-AndroidApplication-touch for additional tookit-specific +information. A @ref Platform::TwoFingerGesture helper provides recognition of +common two-finger gestures for zoom, rotation and pan. + @subsection platform-windowed-key-events Attaching to keyboard and text input The @relativeref{Platform::Sdl2Application,keyPressEvent()} and diff --git a/doc/snippets/Platform.cpp b/doc/snippets/Platform.cpp index e2a2af6b6..d870d5713 100644 --- a/doc/snippets/Platform.cpp +++ b/doc/snippets/Platform.cpp @@ -31,6 +31,8 @@ #include #endif +#include "Magnum/Platform/Gesture.h" + /* [windowed] */ #include #include @@ -346,3 +348,50 @@ void MyApplication::tickEvent() { } #endif + +namespace K { + +struct MyApplication: Platform::Application { + void pointerPressEvent(PointerEvent& event) override; + void pointerReleaseEvent(PointerEvent& event) override; + void pointerMoveEvent(PointerMoveEvent& event) override; + + void translateSomething(const Vector2&) {} + void rotateSomething(const Complex&) {} + void scaleSomething(Float) {} + + Platform::TwoFingerGesture _gesture; +}; + +/* [TwoFingerGesture] */ +void MyApplication::pointerPressEvent(PointerEvent& event) { + _gesture.pressEvent(event); + + DOXYGEN_ELLIPSIS() +} + +void MyApplication::pointerReleaseEvent(PointerEvent& event) { + _gesture.releaseEvent(event); + + DOXYGEN_ELLIPSIS() +} + +void MyApplication::pointerMoveEvent(PointerMoveEvent& event) { + _gesture.moveEvent(event); + + /* A gesture is recognized, perform appropriate action */ + if(_gesture) { + translateSomething(_gesture.relativeTranslation()); + rotateSomething(_gesture.relativeRotation()); + scaleSomething(_gesture.relativeScaling()); + + event.setAccepted(); + redraw(); + return; + } + + DOXYGEN_ELLIPSIS() +} +/* [TwoFingerGesture] */ + +} diff --git a/src/Magnum/Platform/AndroidApplication.h b/src/Magnum/Platform/AndroidApplication.h index b461c4863..b4017866b 100644 --- a/src/Magnum/Platform/AndroidApplication.h +++ b/src/Magnum/Platform/AndroidApplication.h @@ -194,7 +194,9 @@ and @ref PointerEventSource::Pen the ID is a constant, as there's always just a single mouse cursor or a pen stylus. See also @ref platform-windowed-pointer-events for general information about -handling pointer input in a portable way. +handling pointer input in a portable way. There's also a +@ref Platform::TwoFingerGesture helper for recognition of common two-finger +gestures for zoom, rotation and pan. */ class AndroidApplication { public: diff --git a/src/Magnum/Platform/CMakeLists.txt b/src/Magnum/Platform/CMakeLists.txt index 724780b12..27bbf1587 100644 --- a/src/Magnum/Platform/CMakeLists.txt +++ b/src/Magnum/Platform/CMakeLists.txt @@ -33,6 +33,7 @@ set(CMAKE_FOLDER "Magnum/Platform") set(MagnumPlatform_SRCS ) set(MagnumPlatform_HEADERS + Gesture.h Platform.h Screen.h ScreenedApplication.h diff --git a/src/Magnum/Platform/EmscriptenApplication.h b/src/Magnum/Platform/EmscriptenApplication.h index f40874383..d3595c0ca 100644 --- a/src/Magnum/Platform/EmscriptenApplication.h +++ b/src/Magnum/Platform/EmscriptenApplication.h @@ -219,7 +219,9 @@ only for the period given finger is pressed. For @ref PointerEventSource::Mouse the ID is a constant, as there's always just a single mouse cursor. See also @ref platform-windowed-pointer-events for general information about -handling pointer input in a portable way. +handling pointer input in a portable way. There's also a +@ref Platform::TwoFingerGesture helper for recognition of common two-finger +gestures for zoom, rotation and pan. @section Platform-EmscriptenApplication-browser Browser-specific behavior diff --git a/src/Magnum/Platform/Gesture.h b/src/Magnum/Platform/Gesture.h new file mode 100644 index 000000000..96ebedd9a --- /dev/null +++ b/src/Magnum/Platform/Gesture.h @@ -0,0 +1,361 @@ +#ifndef Magnum_Platform_Gesture_h +#define Magnum_Platform_Gesture_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023, 2024 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/** @file + * @brief Class @ref Magnum::Platform::TwoFingerGesture + * @m_since_latest + */ + +#include "Magnum/Magnum.h" +#include "Magnum/Math/Functions.h" /* Math::sqrt() */ +#include "Magnum/Math/Complex.h" + +namespace Magnum { namespace Platform { + +/** +@brief Two-finger gesture recognition +@m_since_latest + +Tracks position of a primary finger and an arbitrary secondary finger based on +pointer events passed to @ref pressEvent(), @ref releaseEvent() and +@ref moveEvent(). Once two fingers are pressed, the instance is contextually +convertible to @cpp true @ce, and @ref position(), @ref direction(), +@ref relativeTranslation(), @ref relativeRotation() and @ref relativeScaling() +contain gesture properties. Example usage: + +@snippet Platform.cpp TwoFingerGesture + +The interface is designed primarily for @ref Sdl2Application "*Application" +subclasses and their @relativeref{Sdl2Application,PointerEvent} and +@relativeref{Sdl2Application,PointerMoveEvent} instances, but works also with +any other types that provide appropriate +@relativeref{Sdl2Application::PointerEvent,source()}, +@relativeref{Sdl2Application::PointerEvent,isPrimary()}, +@relativeref{Sdl2Application::PointerEvent,id()} and @relativeref{Sdl2Application::PointerEvent,position()} members. + +@experimental +*/ +class TwoFingerGesture { + public: + /** + * @brief Handle a press event + * + * Accepts a pointer event instance such as the one coming from + * @ref Platform::Sdl2Application::pointerPressEvent(). If the event + * comes from a primary finger, replaces the internal state with it, + * waiting for the secondary finger press to happen. If the event comes + * from a secondary finger, it's used only a primary finger is known, + * there's no known secondary finger ID yet. or the ID matches the + * known secondary finger ID. Events that don't come from a touch + * source are ignored. Returns @cpp true @ce if the event was used, + * @cpp false @ce if not. + * + * The function doesn't modify the event in any way. If needed, it's up + * to the caller to call + * @relativeref{Sdl2Application::PointerEvent,setAccepted()}. + */ + template bool pressEvent(const PointerEvent& event); + + /** + * @brief Handle a release event + * + * Accepts a pointer event instance such as the one coming from + * @ref Platform::Sdl2Application::pointerReleaseEvent(). If the + * release comes from a primary finger whose ID is known, resets the + * state for both the primary and secondary touch, waiting for a + * primary finger press to happen again. Otherwise, if the release + * comes from a secondary finger whose ID is known. resets just the + * secondary finger state, waiting for a different secondary finger + * press to happen. Events that don't come from a touch source are + * ignored. Returns @cpp true @ce if the event was used, @cpp false @ce + * if not. + * + * The function doesn't modify the event in any way. If needed, it's up + * to the caller to call + * @relativeref{Sdl2Application::PointerEvent,setAccepted()}. + */ + template bool releaseEvent(const PointerEvent& event); + + /** + * @brief Handle a move event + * + * Accepts a pointer move event instance such as the one coming from + * @ref Platform::Sdl2Application::pointerMoveEvent(). If the move + * comes from a primary finger whose ID is known or from a secondary + * finger whose ID is known, updates given finger state. Events that + * don't come from a touch source are ignored. Returns @cpp true @ce if + * the event was used, @cpp false @ce if not. + * + * The function doesn't modify the event in any way. If needed, it's up + * to the caller to call + * @relativeref{Sdl2Application::PointerEvent,setAccepted()}. + */ + template bool moveEvent(const PointerMoveEvent& event); + + /** + * @brief Count of known pressed fingers + * + * Is @cpp 0 @ce if @ref pressEvent() wasn't called yet or + * @ref releaseEvent() happened for the primary finger, @cpp 1 @ce + * if only the primary finger is pressed or a secondary finger was + * released and @cpp 2 @ce if both the primary and a secondary finger + * is currently pressed. + * @see @ref isGesture() + */ + UnsignedInt fingerCount() const { + return (_primaryTouchId != NoTouchId) + + (_secondaryTouchId != NoTouchId); + } + + /** + * @brief Whether the internal state represents a two-finger gesture + * + * Returns @cpp true @ce if both the primary and a secondary finger + * are pressed, @cpp false @ce otherwise. Same as @ref operator bool(). + */ + bool isGesture() const { + return fingerCount() == 2; + } + + /** + * @brief Whether the internal state represents a two-finger gesture + * + * Returns @cpp true @ce if both the primary and a secondary finger + * are pressed, @cpp false @ce otherwise. Same as @ref operator bool(). + */ + explicit operator bool() const { + return fingerCount() == 2; + } + + /** + * @brief Centroid between the two known pressed finger positions + * + * If only one or no fingers are pressed --- i.e., @ref isGesture() is + * @cpp false @ce --- returns a NaN vector. + * @see @ref direction(), @ref relativeTranslation(), + * @ref Math::isNan() + */ + Vector2 position() const { + return (_primaryTouchPosition + _secondaryTouchPosition)*0.5f; + } + + /** + * @brief Direction from the center to the primary finger position + * + * Negate the return value to get direction from the center to the + * secondary finger. If only one or no fingers are pressed --- i.e., + * @ref isGesture() is @cpp false @ce --- returns a NaN vector. + * @see @ref position(), @ref relativeRotation(), + * @ref relativeScaling(), @ref Math::isNan() + */ + Vector2 direction() const { + return (_primaryTouchPosition - _secondaryTouchPosition)*0.5f; + } + + /** + * @brief Translation of the centroid relative to the previous finger positions + * + * If there was no movement since the press, returns a zero vector. If + * only one or no fingers are pressed --- i.e., @ref isGesture() is + * @cpp false @ce --- returns a NaN vector. + * @see @ref position(), @ref relativeRotation(), + * @ref relativeScaling(), @ref Math::isNan() + */ + Vector2 relativeTranslation() const { + return (_primaryTouchPosition - _primaryPreviousTouchPosition + + _secondaryTouchPosition - _secondaryPreviousTouchPosition)*0.5f; + } + + /** + * @brief Rotation relative to the previous finger positions + * + * Note that given the event coordinates are in a Y down coordinate, + * positive rotation angle is clockwise. If there was no movement since + * the press, returns an identity rotation. If only one or no fingers + * are pressed --- i.e., @ref isGesture() is @cpp false @ce --- returns + * a complex NaN. + * + * The function returns a @relativeref{Math,Complex} instead of an + * angle as the angle would likely be converted back to a rotation + * representation anyway. Use @ref Complex::toMatrix(), + * @ref Complex::transformVector() or @ref Complex::angle() if a + * different representation is needed. + * @see @ref direction(), @ref relativeTranslation(), + * @ref relativeScaling(), @ref Math::isNan() + */ + Complex relativeRotation() const { + /* prev * rot = cur + prev^-1 * prev * rot = prev^-1 * cur + rot = prev^-1 * cur */ + return Complex{(_primaryPreviousTouchPosition - _secondaryPreviousTouchPosition).normalized()}.inverted()*Complex{(_primaryTouchPosition - _secondaryTouchPosition).normalized()}; + } + + /** + * @brief Scaling relative to the previous finger positions + * + * The returned value is always positive. Values less than + * @cpp 1.0f @ce are when the points are getting closer, values larger + * than @cpp 1.0f @ce are when the points are getting further apart. If + * there was no movement since the press, returns @cpp 1.0f @ce. If + * only one or no fingers are pressed --- i.e., @ref isGesture() is + * @cpp false @ce --- returns a NaN. + * @see @ref direction(), @ref relativeTranslation(), + * @ref relativeRotation(), @ref Math::isNan() + */ + Float relativeScaling() const { + return Math::sqrt( + (_secondaryTouchPosition - _primaryTouchPosition).dot()/ + (_secondaryPreviousTouchPosition - _primaryPreviousTouchPosition).dot()); + } + + private: + struct Touch { + Long id; + Vector2 position; + }; + + /* ~Long{} is -1, which may collide with actual pointer IDs (SDL uses + it to denote a mouse, for example) */ + constexpr static Long NoTouchId = 1ll << 63; + + Long _primaryTouchId = NoTouchId; + Vector2 _primaryTouchPosition{Constants::nan()}, + _primaryPreviousTouchPosition{Constants::nan()}; + + Long _secondaryTouchId = NoTouchId; + Vector2 _secondaryTouchPosition{Constants::nan()}, + _secondaryPreviousTouchPosition{Constants::nan()}; +}; + +namespace Implementation { + +/* Not all Application::PointerEventSource enums have a Touch member (e.g. + GlfwApplication doesn't), this makes it return false for such types, instead + of failing to compile */ +template constexpr bool isTouchPointerEventSource(PointerEventSource p, decltype(PointerEventSource::Touch)* = nullptr) { + return p == PointerEventSource::Touch; +} +constexpr bool isTouchPointerEventSource(...) { return false; } + +} + +template bool TwoFingerGesture::pressEvent(const PointerEvent& event) { + /* Filter away non-touch sources. Other than that just assume it's a finger + or something equivalent, capable of multi-touch -- i.e., don't even + check pointers(). */ + if(!Implementation::isTouchPointerEventSource(event.source())) + return false; + + /* If this is the primary finger, unconditionally replace the primary touch + with it, and reset everything else */ + if(event.isPrimary()) { + _primaryTouchId = event.id(); + _primaryTouchPosition = event.position(); + _primaryPreviousTouchPosition = event.position(); + _secondaryTouchId = NoTouchId; + _secondaryTouchPosition = Vector2{Constants::nan()}; + _secondaryPreviousTouchPosition = Vector2{Constants::nan()}; + return true; + } + + /* If this is a secondary finger and a primary finger is already known, + remember it either if it has a matching ID or there's no recorded second + touch yet */ + if(_primaryTouchId != NoTouchId && (_secondaryTouchId == NoTouchId || event.id() == _secondaryTouchId)) { + _secondaryTouchId = event.id(); + _secondaryTouchPosition = event.position(); + _secondaryPreviousTouchPosition = event.position(); + return true; + } + + /* If this is a secondary finger and a primary finger is not known yet, or + if this is a secondary finger with a different ID, do nothing */ + return false; +} + +template bool TwoFingerGesture::releaseEvent(const PointerEvent& event) { + /* Filter away non-touch sources. Other than that just assume it's a finger + or something equivalent, capable of multi-touch -- i.e., don't even + check pointers(). */ + if(!Implementation::isTouchPointerEventSource(event.source())) + return false; + + /* If the primary finger is lifted, reset everything and wait for the next + time a primary finger is pressed again */ + if(event.isPrimary() && event.id() == _primaryTouchId) { + _primaryTouchId = NoTouchId; + _primaryTouchPosition = Vector2{Constants::nan()}; + _primaryPreviousTouchPosition = Vector2{Constants::nan()}; + _secondaryTouchId = NoTouchId; + _secondaryTouchPosition = Vector2{Constants::nan()}; + _secondaryPreviousTouchPosition = Vector2{Constants::nan()}; + return true; + } + + /* If this is a secondary finger, reset just that one, and wait for another + secondary finger press to take up its place */ + if(!event.isPrimary() && event.id() == _secondaryTouchId) { + _secondaryTouchId = NoTouchId; + _secondaryTouchPosition = Vector2{Constants::nan()}; + _secondaryPreviousTouchPosition = Vector2{Constants::nan()}; + return true; + } + + /* If the IDs don't match or their primary/secondary state doesn't match, + do nothing */ + return false; +} + +template bool TwoFingerGesture::moveEvent(const PointerMoveEvent& event) { + /* Filter away non-touch sources. Other than that just assume it's a finger + or something equivalent, capable of multi-touch -- i.e., don't even + check pointers(). */ + if(!Implementation::isTouchPointerEventSource(event.source())) + return false; + + /* If the event matches any of the recorded IDs, update the corresponding + values */ + if(event.isPrimary() && event.id() == _primaryTouchId) { + _primaryPreviousTouchPosition = _primaryTouchPosition; + _primaryTouchPosition = event.position(); + return true; + } + if(!event.isPrimary() && event.id() == _secondaryTouchId) { + _secondaryPreviousTouchPosition = _secondaryTouchPosition; + _secondaryTouchPosition = event.position(); + return true; + } + + /* If the IDs don't match or their primary/secondary state doesn't match, + do nothing */ + return false; +} + +}} + +#endif diff --git a/src/Magnum/Platform/Platform.h b/src/Magnum/Platform/Platform.h index 87342e4a9..cbc7a35fc 100644 --- a/src/Magnum/Platform/Platform.h +++ b/src/Magnum/Platform/Platform.h @@ -45,6 +45,8 @@ namespace Implementation { } #endif +class TwoFingerGesture; + #ifdef MAGNUM_TARGET_GL class GLContext; #endif diff --git a/src/Magnum/Platform/Sdl2Application.h b/src/Magnum/Platform/Sdl2Application.h index 94bf3f66c..93f0ae5b6 100644 --- a/src/Magnum/Platform/Sdl2Application.h +++ b/src/Magnum/Platform/Sdl2Application.h @@ -331,7 +331,9 @@ lifted, and then never again. For @ref PointerEventSource::Mouse the ID is a constant, as there's always just a single mouse cursor. See also @ref platform-windowed-pointer-events for general information about -handling pointer input in a portable way. +handling pointer input in a portable way. There's also a +@ref Platform::TwoFingerGesture helper for recognition of common two-finger +gestures for zoom, rotation and pan. @section Platform-Sdl2Application-platform-specific Platform-specific behavior diff --git a/src/Magnum/Platform/Test/CMakeLists.txt b/src/Magnum/Platform/Test/CMakeLists.txt index ffd3834f5..6992b0bbf 100644 --- a/src/Magnum/Platform/Test/CMakeLists.txt +++ b/src/Magnum/Platform/Test/CMakeLists.txt @@ -31,6 +31,8 @@ set(CMAKE_FOLDER "Magnum/Platform/Test") find_package(Corrade REQUIRED Main) +corrade_add_test(PlatformGestureTest GestureTest.cpp LIBRARIES Magnum) + # Icons for SDL/GLFW if(NOT CORRADE_TARGET_EMSCRIPTEN AND (MAGNUM_WITH_SDL2APPLICATION OR MAGNUM_WITH_GLFWAPPLICATION)) corrade_add_resource(Platform_RESOURCES resources.conf) diff --git a/src/Magnum/Platform/Test/GestureTest.cpp b/src/Magnum/Platform/Test/GestureTest.cpp new file mode 100644 index 000000000..220cd56ce --- /dev/null +++ b/src/Magnum/Platform/Test/GestureTest.cpp @@ -0,0 +1,444 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023, 2024 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include + +#include "Magnum/Platform/Gesture.h" + +namespace Magnum { namespace Platform { namespace Test { namespace { + +struct GestureTest: TestSuite::Tester { + explicit GestureTest(); + + void twoFinger(); + void twoFingerPressPrimaryAgain(); + void twoFingerPressPrimaryAfterSecondary(); + void twoFingerSecondaryWithoutPrimary(); + void twoFingerUnknownSecondary(); + void twoFingerReleasePrimary(); + void twoFingerReleaseSecondary(); + template void twoFingerNonTouchEvents(); +}; + +using namespace Math::Literals; + +enum class PointerEventSource { + Mouse = -1337, + Touch = 12 +}; + +class PointerEvent { + public: + explicit PointerEvent(bool primary, Long id, const Vector2& position): _primary{primary}, _id{id}, _position{position} {} + + PointerEventSource source() const { return PointerEventSource::Touch; } + bool isPrimary() const { return _primary; } + Long id() const { return _id; } + Vector2 position() const { return _position; } + + private: + bool _primary; + Long _id; + Vector2 _position; +}; + +enum class PointerEventSourceMouseOnly { + Mouse = -1337 +}; + +GestureTest::GestureTest() { + addTests({&GestureTest::twoFinger, + &GestureTest::twoFingerPressPrimaryAgain, + &GestureTest::twoFingerPressPrimaryAfterSecondary, + &GestureTest::twoFingerSecondaryWithoutPrimary, + &GestureTest::twoFingerUnknownSecondary, + &GestureTest::twoFingerReleasePrimary, + &GestureTest::twoFingerReleaseSecondary, + + &GestureTest::twoFingerNonTouchEvents, + &GestureTest::twoFingerNonTouchEvents}); +} + +void GestureTest::twoFinger() { + /* Initially there's nothing */ + TwoFingerGesture g; + CORRADE_COMPARE(g.fingerCount(), 0); + CORRADE_VERIFY(!g.isGesture()); + CORRADE_VERIFY(!g); + CORRADE_COMPARE(Math::isNan(g.position()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.direction()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.relativeTranslation()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(Vector2{g.relativeRotation()}), BitVector2{3}); + CORRADE_VERIFY(Math::isNan(g.relativeScaling())); + + /* After pressing just the primary there's no gesture yet. Using large IDs + to verify they're stored as full 64-bit numbers */ + CORRADE_VERIFY(g.pressEvent(PointerEvent{true, 1ll << 37, {10.0f, 20.0f}})); + CORRADE_COMPARE(g.fingerCount(), 1); + CORRADE_VERIFY(!g.isGesture()); + CORRADE_VERIFY(!g); + CORRADE_COMPARE(Math::isNan(g.position()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.direction()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.relativeTranslation()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(Vector2{g.relativeRotation()}), BitVector2{3}); + CORRADE_VERIFY(Math::isNan(g.relativeScaling())); + + /* With a secondary press there's a gesture. We don't check the ID in this + case, just the primary/secondary distinction, so it's fine if both are + the same. */ + CORRADE_VERIFY(g.pressEvent(PointerEvent{false, 1ll << 37, {20.0f, 10.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), Vector2{15.0f}); + CORRADE_COMPARE(g.direction(), (Vector2{-5.0f, 5.0f})); + /* Positive direction should point to the primary event, negative to the + secondary */ + CORRADE_COMPARE(g.position() + g.direction(), (Vector2{10.0f, 20.0f})); + CORRADE_COMPARE(g.position() - g.direction(), (Vector2{20.0f, 10.0f})); + /* No movement yet, so default values */ + CORRADE_COMPARE(g.relativeTranslation(), Vector2{}); + CORRADE_COMPARE(g.relativeRotation(), Complex{}); + CORRADE_COMPARE(g.relativeScaling(), 1.0f); + + /* Move primary finger to the other side of the secondary, forming a + translation and a 180° rotation */ + CORRADE_VERIFY(g.moveEvent(PointerEvent{true, 1ll << 37, {30.0f, 0.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), (Vector2{25.0f, 5.0f})); + CORRADE_COMPARE(g.direction(), (Vector2{5.0f, -5.0f})); + CORRADE_COMPARE(g.relativeTranslation(), (Vector2{10.0f, -10.0f})); + CORRADE_COMPARE(g.relativeRotation(), Complex::rotation(180.0_degf)); + CORRADE_COMPARE(g.relativeScaling(), 1.0f); + + /* Move secondary finger slightly to the right, forming a counterclockwise + rotation, thus less than 180° */ + CORRADE_VERIFY(g.moveEvent(PointerEvent{false, 1ll << 37, {25.0f, 10.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), (Vector2{27.5f, 5.0f})); + CORRADE_COMPARE(g.direction(), (Vector2{2.5f, -5.0f})); + CORRADE_COMPARE(g.relativeTranslation(), (Vector2{12.5f, -10.0f})); + CORRADE_COMPARE(g.relativeRotation(), Complex::rotation(161.565001424104_degf)); + CORRADE_COMPARE(g.relativeScaling(), 0.790569f); + + /* Moving primary and secondary fingers back results in the same absolute + values as initially, and relative values inverted compared to above */ + CORRADE_VERIFY(g.moveEvent(PointerEvent{true, 1ll << 37, {10.0f, 20.0f}})); + CORRADE_VERIFY(g.moveEvent(PointerEvent{false, 1ll << 37, {20.0f, 10.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), Vector2{15.0f}); + CORRADE_COMPARE(g.direction(), (Vector2{-5.0f, 5.0f})); + CORRADE_COMPARE(g.relativeTranslation(), (Vector2{-12.5f, 10.0f})); + CORRADE_COMPARE(g.relativeRotation(), Complex::rotation(-161.565001424104_degf)); + CORRADE_COMPARE(g.relativeScaling(), 1.0f/0.790569f); +} + +void GestureTest::twoFingerPressPrimaryAgain() { + TwoFingerGesture g; + + CORRADE_VERIFY(g.pressEvent(PointerEvent{true, 37, {10.0f, 20.0f}})); + CORRADE_COMPARE(g.fingerCount(), 1); + CORRADE_VERIFY(!g.isGesture()); + CORRADE_VERIFY(!g); + CORRADE_COMPARE(Math::isNan(g.position()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.direction()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.relativeTranslation()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(Vector2{g.relativeRotation()}), BitVector2{3}); + CORRADE_VERIFY(Math::isNan(g.relativeScaling())); + + /* Another primary press replaces the original */ + CORRADE_VERIFY(g.pressEvent(PointerEvent{true, 76, {10.0f, 20.0f}})); + CORRADE_COMPARE(g.fingerCount(), 1); + CORRADE_VERIFY(!g.isGesture()); + CORRADE_VERIFY(!g); + CORRADE_COMPARE(Math::isNan(g.position()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.direction()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.relativeTranslation()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(Vector2{g.relativeRotation()}), BitVector2{3}); + CORRADE_VERIFY(Math::isNan(g.relativeScaling())); +} + +void GestureTest::twoFingerPressPrimaryAfterSecondary() { + TwoFingerGesture g; + + CORRADE_VERIFY(g.pressEvent(PointerEvent{true, 37, {10.0f, 20.0f}})); + CORRADE_VERIFY(g.pressEvent(PointerEvent{false, 26, {20.0f, 10.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), Vector2{15.0f}); + CORRADE_COMPARE(g.direction(), (Vector2{-5.0f, 5.0f})); + CORRADE_COMPARE(g.relativeTranslation(), Vector2{}); + CORRADE_COMPARE(g.relativeRotation(), Complex{}); + CORRADE_COMPARE(g.relativeScaling(), 1.0f); + + /* Another primary press replaces both */ + CORRADE_VERIFY(g.pressEvent(PointerEvent{true, 76, {10.0f, 20.0f}})); + CORRADE_COMPARE(g.fingerCount(), 1); + CORRADE_VERIFY(!g.isGesture()); + CORRADE_VERIFY(!g); + CORRADE_COMPARE(Math::isNan(g.position()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.direction()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.relativeTranslation()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(Vector2{g.relativeRotation()}), BitVector2{3}); + CORRADE_VERIFY(Math::isNan(g.relativeScaling())); +} + +void GestureTest::twoFingerSecondaryWithoutPrimary() { + TwoFingerGesture g; + + /* Pressing a secondary pointer without a primary being recorded first + does nothing, neither does move or release */ + CORRADE_VERIFY(!g.pressEvent(PointerEvent{false, 26, {20.0f, 10.0f}})); + CORRADE_COMPARE(g.fingerCount(), 0); + CORRADE_VERIFY(!g.isGesture()); + CORRADE_VERIFY(!g); + CORRADE_COMPARE(Math::isNan(g.position()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.direction()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.relativeTranslation()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(Vector2{g.relativeRotation()}), BitVector2{3}); + CORRADE_VERIFY(Math::isNan(g.relativeScaling())); + + CORRADE_VERIFY(!g.moveEvent(PointerEvent{false, 26, {20.0f, 10.0f}})); + CORRADE_COMPARE(g.fingerCount(), 0); + CORRADE_VERIFY(!g.isGesture()); + CORRADE_VERIFY(!g); + CORRADE_COMPARE(Math::isNan(g.position()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.direction()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.relativeTranslation()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(Vector2{g.relativeRotation()}), BitVector2{3}); + CORRADE_VERIFY(Math::isNan(g.relativeScaling())); + + CORRADE_VERIFY(!g.releaseEvent(PointerEvent{false, 26, {20.0f, 10.0f}})); + CORRADE_COMPARE(g.fingerCount(), 0); + CORRADE_VERIFY(!g.isGesture()); + CORRADE_VERIFY(!g); + CORRADE_COMPARE(Math::isNan(g.position()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.direction()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.relativeTranslation()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(Vector2{g.relativeRotation()}), BitVector2{3}); + CORRADE_VERIFY(Math::isNan(g.relativeScaling())); +} + +void GestureTest::twoFingerUnknownSecondary() { + TwoFingerGesture g; + + /* Using large IDs to verify they're stored as full 64-bit numbers */ + CORRADE_VERIFY(g.pressEvent(PointerEvent{true, 1ll << 39, {10.0f, 20.0f}})); + CORRADE_VERIFY(g.pressEvent(PointerEvent{false, 1ll << 37, {20.0f, 10.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), Vector2{15.0f}); + CORRADE_COMPARE(g.direction(), (Vector2{-5.0f, 5.0f})); + CORRADE_COMPARE(g.relativeTranslation(), Vector2{}); + CORRADE_COMPARE(g.relativeRotation(), Complex{}); + CORRADE_COMPARE(g.relativeScaling(), 1.0f); + + /* Neither of these should affect the internal state in any way as it has a + different ID than the first recorded secondary press */ + CORRADE_VERIFY(!g.pressEvent(PointerEvent{false, 1ll << 39, {0.0f, 0.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), Vector2{15.0f}); + CORRADE_COMPARE(g.direction(), (Vector2{-5.0f, 5.0f})); + CORRADE_COMPARE(g.relativeTranslation(), Vector2{}); + CORRADE_COMPARE(g.relativeRotation(), Complex{}); + CORRADE_COMPARE(g.relativeScaling(), 1.0f); + + CORRADE_VERIFY(!g.moveEvent(PointerEvent{false, 1ll << 39, {0.0f, 0.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), Vector2{15.0f}); + CORRADE_COMPARE(g.direction(), (Vector2{-5.0f, 5.0f})); + CORRADE_COMPARE(g.relativeTranslation(), Vector2{}); + CORRADE_COMPARE(g.relativeRotation(), Complex{}); + CORRADE_COMPARE(g.relativeScaling(), 1.0f); + + CORRADE_VERIFY(!g.releaseEvent(PointerEvent{false, 1ll << 39, {0.0f, 0.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), Vector2{15.0f}); + CORRADE_COMPARE(g.direction(), (Vector2{-5.0f, 5.0f})); + CORRADE_COMPARE(g.relativeTranslation(), Vector2{}); + CORRADE_COMPARE(g.relativeRotation(), Complex{}); + CORRADE_COMPARE(g.relativeScaling(), 1.0f); +} + +void GestureTest::twoFingerReleasePrimary() { + TwoFingerGesture g; + + /* Using large IDs to verify they're stored as full 64-bit numbers */ + CORRADE_VERIFY(g.pressEvent(PointerEvent{true, 1ll << 37, {10.0f, 20.0f}})); + CORRADE_VERIFY(g.pressEvent(PointerEvent{false, 1ll << 26, {20.0f, 10.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), Vector2{15.0f}); + CORRADE_COMPARE(g.direction(), (Vector2{-5.0f, 5.0f})); + CORRADE_COMPARE(g.relativeTranslation(), Vector2{}); + CORRADE_COMPARE(g.relativeRotation(), Complex{}); + CORRADE_COMPARE(g.relativeScaling(), 1.0f); + + /* Releasing a primary pointer with a different ID shouldn't affect + anything */ + CORRADE_VERIFY(!g.releaseEvent(PointerEvent{true, 1ll << 26, {10.0f, 20.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), Vector2{15.0f}); + CORRADE_COMPARE(g.direction(), (Vector2{-5.0f, 5.0f})); + CORRADE_COMPARE(g.relativeTranslation(), Vector2{}); + CORRADE_COMPARE(g.relativeRotation(), Complex{}); + CORRADE_COMPARE(g.relativeScaling(), 1.0f); + + /* Releasing the recorded primary pointer resets everything, it'll wait for + a new primary touch */ + CORRADE_VERIFY(g.releaseEvent(PointerEvent{true, 1ll << 37, {10.0f, 20.0f}})); + CORRADE_COMPARE(g.fingerCount(), 0); + CORRADE_VERIFY(!g.isGesture()); + CORRADE_VERIFY(!g); + CORRADE_COMPARE(Math::isNan(g.position()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.direction()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.relativeTranslation()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(Vector2{g.relativeRotation()}), BitVector2{3}); + CORRADE_VERIFY(Math::isNan(g.relativeScaling())); +} + +void GestureTest::twoFingerReleaseSecondary() { + TwoFingerGesture g; + + /* Using large IDs to verify they're stored as full 64-bit numbers */ + CORRADE_VERIFY(g.pressEvent(PointerEvent{true, 1ll << 37, {10.0f, 20.0f}})); + CORRADE_VERIFY(g.pressEvent(PointerEvent{false, 1ll << 26, {20.0f, 10.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), Vector2{15.0f}); + CORRADE_COMPARE(g.direction(), (Vector2{-5.0f, 5.0f})); + CORRADE_COMPARE(g.relativeTranslation(), Vector2{}); + CORRADE_COMPARE(g.relativeRotation(), Complex{}); + CORRADE_COMPARE(g.relativeScaling(), 1.0f); + + /* Releasing a secondary pointer with a different ID shouldn't affect + anything */ + CORRADE_VERIFY(!g.releaseEvent(PointerEvent{false, 1ll << 37, {10.0f, 20.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), Vector2{15.0f}); + CORRADE_COMPARE(g.direction(), (Vector2{-5.0f, 5.0f})); + CORRADE_COMPARE(g.relativeTranslation(), Vector2{}); + CORRADE_COMPARE(g.relativeRotation(), Complex{}); + CORRADE_COMPARE(g.relativeScaling(), 1.0f); + + /* Releasing the recorded secondary pointer resets just the secondary + pointer */ + CORRADE_VERIFY(g.releaseEvent(PointerEvent{false, 1ll << 26, {10.0f, 20.0f}})); + CORRADE_COMPARE(g.fingerCount(), 1); + CORRADE_VERIFY(!g.isGesture()); + CORRADE_VERIFY(!g); + CORRADE_COMPARE(Math::isNan(g.position()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.direction()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.relativeTranslation()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(Vector2{g.relativeRotation()}), BitVector2{3}); + CORRADE_VERIFY(Math::isNan(g.relativeScaling())); + + /* Press another secondary pointer (though with the same ID), but at other + side of the primary */ + CORRADE_VERIFY(g.pressEvent(PointerEvent{false, 1ll << 26, {0.0f, 30.0f}})); + CORRADE_COMPARE(g.fingerCount(), 2); + CORRADE_VERIFY(g.isGesture()); + CORRADE_VERIFY(g); + CORRADE_COMPARE(g.position(), (Vector2{5.0f, 25.0f})); + CORRADE_COMPARE(g.direction(), (Vector2{5.0f, -5.0f})); + /* Positive direction should point to the primary event, negative to the + secondary */ + CORRADE_COMPARE(g.position() + g.direction(), (Vector2{10.0f, 20.0f})); + CORRADE_COMPARE(g.position() - g.direction(), (Vector2{0.0f, 30.0f})); + /* The relative values shouldn't take the previous press into account, + should be identities. */ + CORRADE_COMPARE(g.relativeTranslation(), Vector2{}); + CORRADE_COMPARE(g.relativeRotation(), Complex{}); + CORRADE_COMPARE(g.relativeScaling(), 1.0f); +} + +template void GestureTest::twoFingerNonTouchEvents() { + setTestCaseTemplateName(std::is_same::value ? "PointerEventSourceMouseOnly" : "PointerEventSource"); + + TwoFingerGesture g; + + struct { + T source() const { return T::Mouse; } + bool isPrimary() const { return true; } + Long id() const { return 0; } + Vector2 position() const { return {}; } + } event; + + /* The event should be ignored by all APIs because it's not a touch one */ + CORRADE_VERIFY(!g.pressEvent(event)); + CORRADE_COMPARE(g.fingerCount(), 0); + CORRADE_VERIFY(!g.isGesture()); + CORRADE_VERIFY(!g); + CORRADE_COMPARE(Math::isNan(g.position()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.direction()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.relativeTranslation()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(Vector2{g.relativeRotation()}), BitVector2{3}); + CORRADE_VERIFY(Math::isNan(g.relativeScaling())); + + CORRADE_VERIFY(!g.moveEvent(event)); + CORRADE_COMPARE(g.fingerCount(), 0); + CORRADE_VERIFY(!g.isGesture()); + CORRADE_VERIFY(!g); + CORRADE_COMPARE(Math::isNan(g.position()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.direction()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.relativeTranslation()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(Vector2{g.relativeRotation()}), BitVector2{3}); + CORRADE_VERIFY(Math::isNan(g.relativeScaling())); + + CORRADE_VERIFY(!g.releaseEvent(event)); + CORRADE_COMPARE(g.fingerCount(), 0); + CORRADE_VERIFY(!g.isGesture()); + CORRADE_VERIFY(!g); + CORRADE_COMPARE(Math::isNan(g.position()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.direction()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(g.relativeTranslation()), BitVector2{3}); + CORRADE_COMPARE(Math::isNan(Vector2{g.relativeRotation()}), BitVector2{3}); + CORRADE_VERIFY(Math::isNan(g.relativeScaling())); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::Platform::Test::GestureTest)