From 755a5519a810605ffe2a63d1bc3ae9be22985831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 19 Oct 2024 02:03:37 +0200 Subject: [PATCH] Platform: handle (multi-)touch events in EmscriptenApplication. The impossible-to-reliably-disable behavior with compatibility mouse events is quite a headache. I wish Emscripten implemented pointer events already so I could ditch this mess -- especially the array of 32 touches where all of them but one will be unchanged is stupid. For the internals unfortunately, EmscriptenMouseEvent and EmscriptenTouchEvent have no common base, so I had to give up on the current way of querying the event struct directly from event getters, as that'd be too nasty with the branching and casts. Instead the relevant fields are put directly into the events themselves. HTML5 also doesn't provide any relative pointer position. For the mouse it was rather straightforward, but for the up-to-32 touches I have to maintain an array of per-finger positions and match them by ID. Hopefully the linear lookup is fine. I'll probably use the same approach for the AndroidApplication. --- doc/changelog.dox | 8 +- src/Magnum/Platform/EmscriptenApplication.cpp | 367 ++++++++++++++---- src/Magnum/Platform/EmscriptenApplication.h | 309 +++++++++++++-- src/Magnum/Platform/Sdl2Application.cpp | 9 +- src/Magnum/Platform/Sdl2Application.h | 3 +- .../Test/EmscriptenApplicationTest.cpp | 49 ++- 6 files changed, 628 insertions(+), 117 deletions(-) diff --git a/doc/changelog.dox b/doc/changelog.dox index 09a98c4274..6e245070f3 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -339,10 +339,14 @@ 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 through new +- Multi-touch support in @ref Platform::Sdl2Application and + @ref Platform::EmscriptenApplication through new @relativeref{Platform::Sdl2Application,PointerEvent} and @relativeref{Platform::Sdl2Application,PointerMoveEvent} that unify mouse - and touch input events + 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. @subsubsection changelog-latest-new-scenegraph SceneGraph library diff --git a/src/Magnum/Platform/EmscriptenApplication.cpp b/src/Magnum/Platform/EmscriptenApplication.cpp index c87c531a6d..c8a739e1a3 100644 --- a/src/Magnum/Platform/EmscriptenApplication.cpp +++ b/src/Magnum/Platform/EmscriptenApplication.cpp @@ -552,6 +552,58 @@ EmscriptenApplication::Pointers buttonsToPointers(const std::uint32_t buttons) { return pointers; } +template EmscriptenApplication::InputEvent::Modifiers eventModifiers(const T& event) { + EmscriptenApplication::InputEvent::Modifiers modifiers; + if(event.ctrlKey) + modifiers |= EmscriptenApplication::InputEvent::Modifier::Ctrl; + if(event.shiftKey) + modifiers |= EmscriptenApplication::InputEvent::Modifier::Shift; + if(event.altKey) + modifiers |= EmscriptenApplication::InputEvent::Modifier::Alt; + if(event.metaKey) + modifiers |= EmscriptenApplication::InputEvent::Modifier::Super; + return modifiers; +} + +template Vector2 eventTargetPosition(const T& event) { + /* Relies on the target being the canvas, which should be always true for + mouse events */ + return {Float(event.targetX), Float(event.targetY)}; +} + +template Vector2 updatePreviousTouch(T(&previousTouches)[32], const Int 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 {}; +} + } void EmscriptenApplication::setupCallbacks(bool resizable) { @@ -575,55 +627,99 @@ void EmscriptenApplication::setupCallbacks(bool resizable) { emscripten_set_resize_callback(target, this, false, cb); } - emscripten_set_mousedown_callback(_canvasTarget.data(), this, false, - ([](int, const EmscriptenMouseEvent* event, void* userData) -> EM_BOOL { + /* Done this way instead of passing the lambda inline so it can have the + #if inside. Because, apparently, emscripten_set_mousedown_callback() is + some crazy macro, and I get "warning: embedding a directive within macro + arguments has undefined behavior" when doing that. */ + /** @todo put back once support for Emscripten < 2.0.27 is dropped */ + const auto mousedown = + [](int, const EmscriptenMouseEvent* event, void* userData) -> EM_BOOL { auto& app = *static_cast(userData); + + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 + /* If the event timestamp is the same (bit-exact, in fact) as the + timestamp of the last touch event, it's a compatibility mouse + event. Ignore. On Chrome at least, the mouseup will have the + same timestamp and gets ignored as well. + + Touch events are available on older Emscripten as well, but the + events don't expose the timestamp field until 2.0.27. */ + if(event->timestamp == app._lastTouchEventTimestamp) + return false; + #endif + const Pointer pointer = buttonToPointer(event->button); const Pointers pointers = buttonsToPointers(event->buttons); + const InputEvent::Modifiers modifiers = eventModifiers(*event); + const Vector2 position = eventTargetPosition(*event); /* If an additional mouse button was pressed, call a move event instead */ if(pointers & ~pointer) { - PointerMoveEvent e{*event, pointer, pointers, {}}; + PointerMoveEvent e{*event, pointer, pointers, modifiers, position, {}}; app.pointerMoveEvent(e); return e.isAccepted(); } else { - PointerEvent e{*event, pointer}; + PointerEvent e{*event, pointer, modifiers, position}; app.pointerPressEvent(e); return e.isAccepted(); } - })); - - emscripten_set_mouseup_callback(_canvasTarget.data(), this, false, - ([](int, const EmscriptenMouseEvent* event, void* userData) -> EM_BOOL { + }; + emscripten_set_mousedown_callback(_canvasTarget.data(), this, false, mousedown); + + /* Done this way instead of passing the lambda inline so it can have the + #if inside. Because, apparently, emscripten_set_mousedown_callback() is + some crazy macro, and I get "warning: embedding a directive within macro + arguments has undefined behavior" when doing that. */ + /** @todo put back once support for Emscripten < 2.0.27 is dropped */ + const auto mouseup = + [](int, const EmscriptenMouseEvent* event, void* userData) -> EM_BOOL { auto& app = *static_cast(userData); + + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 + /* If the event timestamp is the same (bit-exact, in fact) as the + timestamp of the last touch event, it's a compatibility mouse + event. Ignore. On Chrome at least, the mouseup will have the + same timestamp and gets ignored as well. + + Touch events are available on older Emscripten as well, but the + events don't expose the timestamp field until 2.0.27. */ + if(event->timestamp == app._lastTouchEventTimestamp) + return false; + #endif + const Pointer pointer = buttonToPointer(event->button); const Pointers pointers = buttonsToPointers(event->buttons); + const InputEvent::Modifiers modifiers = eventModifiers(*event); + const Vector2 position = eventTargetPosition(*event); /* If some buttons are still left pressed after a release, call a move event instead */ if(pointers) { - PointerMoveEvent e{*event, pointer, pointers, {}}; + PointerMoveEvent e{*event, pointer, pointers, modifiers, position, {}}; app.pointerMoveEvent(e); return e.isAccepted(); } else { - PointerEvent e{*event, pointer}; + PointerEvent e{*event, pointer, modifiers, position}; app.pointerReleaseEvent(e); return e.isAccepted(); } - })); + }; + emscripten_set_mouseup_callback(_canvasTarget.data(), this, false, mouseup); emscripten_set_mousemove_callback(_canvasTarget.data(), this, false, ([](int, const EmscriptenMouseEvent* event, void* userData) -> EM_BOOL { auto& app = *static_cast(userData); - /* Relies on the target being the canvas, which should be always - true for mouse events */ - Vector2 position{Float(event->targetX), Float(event->targetY)}; - PointerMoveEvent e{*event, {}, buttonsToPointers(event->buttons), - /* Avoid bogus offset at first -- report 0 when the event is - calledĀ for the first time. */ - Math::isNan(app._previousMouseMovePosition).all() ? Vector2{} : - position - app._previousMouseMovePosition}; + const Pointers pointers = buttonsToPointers(event->buttons); + const InputEvent::Modifiers modifiers = eventModifiers(*event); + const Vector2 position = eventTargetPosition(*event); + /* Avoid bogus offset at first -- report 0 when the event is called + for the first time. */ + const Vector2 relativePosition = + Math::isNan(app._previousMouseMovePosition).all() ? + Vector2{} : position - app._previousMouseMovePosition; + + PointerMoveEvent e{*event, {}, pointers, modifiers, position, relativePosition}; app._previousMouseMovePosition = position; app.pointerMoveEvent(e); return e.isAccepted(); @@ -636,6 +732,147 @@ void EmscriptenApplication::setupCallbacks(bool resizable) { return e.isAccepted(); })); + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 + /* Touch events are available on older Emscripten as well, but the events + don't expose the timestamp field, which is *essential* for ignoring + compatibility mouse events synthesized from touch. Favoring correctness + over broad support and thus the touch support is not even available on + older versions. */ + emscripten_set_touchstart_callback(_canvasTarget.data(), this, false, + ([](int, const EmscriptenTouchEvent* event, void* userData) -> EM_BOOL { + auto& app = *static_cast(userData); + /** @todo somehow desktop Chrome doesn't populate these for touch + events, is that a browser bug? Emscripten seems to fill them in + https://github.com/emscripten-core/emscripten/blob/10cb9d46cdd17e7a96de68137c9649d9a630fbc7/src/library_html5.js#L1930-L1933 + correctly. */ + const InputEvent::Modifiers modifiers = eventModifiers(*event); + + bool accepted = false; + for(Int i = 0; i != event->numTouches; ++i) { + const EmscriptenTouchPoint& touch = event->touches[i]; + /* Don't report touches that didn't change */ + if(!touch.isChanged) + continue; + + /* Update primary finger info. If there's no primary finger yet + and this is the first finger pressed, it becomes the primary + finger. If the primary finger is lifted, no other finger + becomes primary until all others are lifted as well. This + 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. */ + bool primary; + if(app._primaryFingerId == ~Int{} && event->numTouches == 1) { + primary = true; + app._primaryFingerId = touch.identifier; + /* Otherwise, if this is the primary finger, mark it as such */ + } else if(app._primaryFingerId == touch.identifier) { + primary = true; + /* Otherwise this is not the primary finger */ + } else primary = false; + + const Vector2 position = eventTargetPosition(event->touches[i]); + /* Remember position of this identifier for next events */ + updatePreviousTouch(app._previousTouches, touch.identifier, position); + + PointerEvent e{*event, primary, touch.identifier, modifiers, position}; + app.pointerPressEvent(e); + accepted = accepted || e.isAccepted(); + } + + return accepted; + })); + + emscripten_set_touchend_callback(_canvasTarget.data(), this, false, + ([](int, const EmscriptenTouchEvent* event, void* userData) -> EM_BOOL { + auto& app = *static_cast(userData); + /** @todo somehow desktop Chrome doesn't populate these for touch + events, see above */ + const InputEvent::Modifiers modifiers = eventModifiers(*event); + + /* Remember the touch event timestamp. Chromium (at least) then + fires the compatibility mouse press and release event with the + same timestamp as the touch end, both after the touch actually + ends, and doesn't fire them if the touch becomes a drag. Not + sure about other browsers. + + The W3C-recommended way to deal with these is to + preventDefault(), i.e. return false from this function. But, + while that stops the mouse events from being emitted, it also + stops any further propagation of the touch event. I want to be + able to control both independently, ffs. + + In order to fire the deprecated MouseEvent from these, the + default pointerReleaseEvent() implementation then clears this + back to a NaN, thus letting the mouse events through. */ + app._lastTouchEventTimestamp = event->timestamp; + + bool accepted = false; + for(Int i = 0; i != event->numTouches; ++i) { + const EmscriptenTouchPoint& touch = event->touches[i]; + /* Don't report touches that didn't change */ + if(!touch.isChanged) + continue; + + /* Update primary finger info. If this is the primary finger + being released, mark it as such and reset. */ + bool primary; + if(app._primaryFingerId == touch.identifier) { + primary = true; + app._primaryFingerId = ~Int{}; + /* Otherwise this is not the primary finger */ + } else primary = false; + + const Vector2 position = eventTargetPosition(event->touches[i]); + /* Free the slot used by this identifier for next events */ + updatePreviousTouch(app._previousTouches, touch.identifier, {}); + + PointerEvent e{*event, primary, touch.identifier, modifiers, position}; + app.pointerReleaseEvent(e); + accepted = accepted || e.isAccepted(); + } + + return accepted; + })); + + emscripten_set_touchmove_callback(_canvasTarget.data(), this, false, + ([](int, const EmscriptenTouchEvent* event, void* userData) -> EM_BOOL { + auto& app = *static_cast(userData); + /** @todo somehow desktop Chrome doesn't populate these for touch + events, see above */ + const InputEvent::Modifiers modifiers = eventModifiers(*event); + + bool accepted = false; + for(Int i = 0; i != event->numTouches; ++i) { + const EmscriptenTouchPoint& touch = event->touches[i]; + /* Don't report touches that didn't change */ + if(!touch.isChanged) + continue; + + /* In this case, 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. */ + const bool primary = app._primaryFingerId == touch.identifier; + + const Vector2 position = eventTargetPosition(event->touches[i]); + /* Query position relative to the previous touch of the same + identifier, update it with current */ + const Vector2 relativePosition = updatePreviousTouch(app._previousTouches, touch.identifier, position); + + PointerMoveEvent e{*event, primary, touch.identifier, modifiers, position, relativePosition}; + app.pointerMoveEvent(e); + accepted = accepted || e.isAccepted(); + } + + return accepted; + })); + + /** @todo touch cancel, maybe reset previous touch moves or something + there? */ + #endif + /* document and window are 'specialEventTargets' in emscripten, matching EMSCRIPTEN_EVENT_TARGET_DOCUMENT and EMSCRIPTEN_EVENT_TARGET_WINDOW. As the lookup happens with the passed parameter and arrays support @@ -808,9 +1045,17 @@ void EmscriptenApplication::keyReleaseEvent(KeyEvent&) {} void EmscriptenApplication::pointerPressEvent(PointerEvent& event) { #ifdef MAGNUM_BUILD_DEPRECATED + /* Not skipping non-primary events because we're only handling Mouse, which + is always primary */ CORRADE_IGNORE_DEPRECATED_PUSH - MouseEvent mouseEvent{event.event()}; - mousePressEvent(mouseEvent); + if(event.source() == PointerEventSource::Mouse) { + MouseEvent mouseEvent{event.event()}; + mousePressEvent(mouseEvent); + } else { + /* Not doing anything, relying on the browser to fire a compatibility + mouse event after, which we then don't filter out. See + pointerReleaseEvent() below for the next step. */ + } CORRADE_IGNORE_DEPRECATED_POP #else static_cast(event); @@ -825,9 +1070,19 @@ CORRADE_IGNORE_DEPRECATED_POP void EmscriptenApplication::pointerReleaseEvent(PointerEvent& event) { #ifdef MAGNUM_BUILD_DEPRECATED + /* Not skipping non-primary events because we're only handling Mouse, which + is always primary */ CORRADE_IGNORE_DEPRECATED_PUSH - MouseEvent mouseEvent{event.event()}; - mouseReleaseEvent(mouseEvent); + if(event.source() == PointerEventSource::Mouse) { + MouseEvent mouseEvent{event.event()}; + mouseReleaseEvent(mouseEvent); + } else { + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 + /* Clear the recorded timestap of the last touch end event, which then + makes the compatibility mouse events go through */ + _lastTouchEventTimestamp = Constantsd::nan(); + #endif + } CORRADE_IGNORE_DEPRECATED_POP #else static_cast(event); @@ -842,22 +1097,35 @@ CORRADE_IGNORE_DEPRECATED_POP void EmscriptenApplication::pointerMoveEvent(PointerMoveEvent& event) { #ifdef MAGNUM_BUILD_DEPRECATED + /* Not skipping non-primary events because we're only handling Mouse, which + is always primary */ CORRADE_IGNORE_DEPRECATED_PUSH /* If the event is due to some button being additionally pressed or one button from a larger set being released, delegate to a press/release event instead */ if(event.pointer()) { /* Emscripten reports either a move or a press/release, so there - shouldn't be any move in this case */ - CORRADE_INTERNAL_ASSERT(event.relativePosition() == Vector2{}); - MouseEvent mouseEvent{event.event()}; + shouldn't be any move in this case. Also, only mouse events should + have a non-empty pointer(). */ + CORRADE_INTERNAL_ASSERT(event.relativePosition() == Vector2{} && event.source() == PointerEventSource::Mouse); + MouseEvent mouseEvent{event.event()}; event.pointers() >= *event.pointer() ? mousePressEvent(mouseEvent) : mouseReleaseEvent(mouseEvent); } else { - /* The positions are reported in integers in the first place, no need - to round anything */ - MouseMoveEvent mouseEvent{event.event(), Vector2i{event.relativePosition()}}; - mouseMoveEvent(mouseEvent); + if(event.source() == PointerEventSource::Mouse) { + MouseMoveEvent mouseEvent{event.event(), + /* The positions are reported in integers in the first place, + no need to round anything */ + Vector2i{event.relativePosition()}}; + mouseMoveEvent(mouseEvent); + } else { + /* Not doing anything here -- touch drag events for some reason + never had compatibility mouse events fired, resulting in bug + reports like https://github.com/mosra/magnum/issues/532 . So + by continuing to do nothing, preserve the backwards + compatibility. People who want touch drag to work should migrate + to the pointer events. */ + } } CORRADE_IGNORE_DEPRECATED_POP #else @@ -925,33 +1193,6 @@ void EmscriptenApplication::exit(int) { _flags |= Flag::ExitRequested; } -namespace { - -template EmscriptenApplication::InputEvent::Modifiers eventModifiers(const T& event) { - EmscriptenApplication::InputEvent::Modifiers modifiers; - if(event.ctrlKey) - modifiers |= EmscriptenApplication::InputEvent::Modifier::Ctrl; - if(event.shiftKey) - modifiers |= EmscriptenApplication::InputEvent::Modifier::Shift; - if(event.altKey) - modifiers |= EmscriptenApplication::InputEvent::Modifier::Alt; - if(event.metaKey) - modifiers |= EmscriptenApplication::InputEvent::Modifier::Super; - return modifiers; -} - -} - -Vector2 EmscriptenApplication::PointerEvent::position() const { - /* Relies on the target being the canvas, which should be always true for - mouse events */ - return {Float(_event.targetX), Float(_event.targetY)}; -} - -EmscriptenApplication::PointerEvent::Modifiers EmscriptenApplication::PointerEvent::modifiers() const { - return eventModifiers(_event); -} - #ifdef MAGNUM_BUILD_DEPRECATED CORRADE_IGNORE_DEPRECATED_PUSH EmscriptenApplication::MouseEvent::Button EmscriptenApplication::MouseEvent::button() const { @@ -969,19 +1210,7 @@ EmscriptenApplication::MouseEvent::Modifiers EmscriptenApplication::MouseEvent:: return eventModifiers(_event); } CORRADE_IGNORE_DEPRECATED_POP -#endif -Vector2 EmscriptenApplication::PointerMoveEvent::position() const { - /* Relies on the target being the canvas, which should be always true for - mouse events */ - return {Float(_event.targetX), Float(_event.targetY)}; -} - -EmscriptenApplication::PointerMoveEvent::Modifiers EmscriptenApplication::PointerMoveEvent::modifiers() const { - return eventModifiers(_event); -} - -#ifdef MAGNUM_BUILD_DEPRECATED CORRADE_IGNORE_DEPRECATED_PUSH EmscriptenApplication::MouseMoveEvent::Buttons EmscriptenApplication::MouseMoveEvent::buttons() const { return EmscriptenApplication::MouseMoveEvent::Button(_event.buttons); diff --git a/src/Magnum/Platform/EmscriptenApplication.h b/src/Magnum/Platform/EmscriptenApplication.h index 23e8e12012..dabbf1ab71 100644 --- a/src/Magnum/Platform/EmscriptenApplication.h +++ b/src/Magnum/Platform/EmscriptenApplication.h @@ -71,6 +71,7 @@ #ifndef DOXYGEN_GENERATING_OUTPUT struct EmscriptenKeyboardEvent; struct EmscriptenMouseEvent; +struct EmscriptenTouchEvent; struct EmscriptenWheelEvent; struct EmscriptenUiEvent; @@ -184,6 +185,39 @@ If no other application header is included, this class is also aliased to @cpp Platform::Application @ce and the macro is aliased to @cpp MAGNUM_APPLICATION_MAIN() @ce to simplify porting. +@section Platform-EmscriptenApplication-touch Touch input + +The application recognizes touch input and reports it as @ref Pointer::Finger +and @ref PointerEventSource::Touch. Because both mouse and touch events are +exposed through a unified @ref PointerEvent / @ref PointerMoveEvent interface, +there's no need for compatibility mouse events synthesized from touch events, +and thus they get ignored when fired right after the corresponding touch. +Emscripten so far [doesn't support pointer events](https://github.com/emscripten-core/emscripten/issues/7278), +so pen input isn't implemented yet. + +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 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. 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. + +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 is platform-specific 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 +the ID is a constant, as there's always just a single mouse cursor. + @section Platform-EmscriptenApplication-browser Browser-specific behavior Leaving a default (zero) size in @ref Configuration will cause the app to use a @@ -309,6 +343,7 @@ class EmscriptenApplication { /* The damn thing cannot handle forward enum declarations */ #ifndef DOXYGEN_GENERATING_OUTPUT + enum class PointerEventSource: UnsignedByte; enum class Pointer: UnsignedByte; #endif @@ -829,15 +864,17 @@ class EmscriptenApplication { * @brief Pointer press event * @m_since_latest * - * Called when a mouse is pressed. Note that if at least one mouse - * button is already pressed and another button gets pressed in - * addition, @ref pointerMoveEvent() with the new combination is - * called, not this function. + * Called when either a mouse or a finger is pressed. Note that if at + * least one mouse button is already pressed and another button gets + * pressed in addition, @ref pointerMoveEvent() with the new + * combination is called, not this function. * - * On builds with @ref MAGNUM_BUILD_DEPRECATED enabled, default - * implementation delegates to @ref mousePressEvent(). On builds with - * deprecated functionality disabled, default implementation does - * nothing. + * On builds with @ref MAGNUM_BUILD_DEPRECATED enabled, if the pointer + * is a mouse, default implementation delegates to + * @ref mousePressEvent(). Touch events rely on browser's implicit + * translation to compatibility mouse events in this case, which is + * otherwise disabled. On builds with deprecated functionality + * disabled, default implementation does nothing. */ virtual void pointerPressEvent(PointerEvent& event); @@ -857,14 +894,17 @@ class EmscriptenApplication { * @brief Pointer release event * @m_since_latest * - * Called when a mouse is released. Note that if multiple mouse buttons - * are pressed and one of these is released, @ref pointerMoveEvent() - * with the new combination is called, not this function. + * Called when either a mouse or a finger is released. Note that if + * multiple mouse buttons are pressed and one of these is released, + * @ref pointerMoveEvent() with the new combination is called, not this + * function. * - * On builds with @ref MAGNUM_BUILD_DEPRECATED enabled, default - * implementation delegates to @ref mouseReleaseEvent(). On builds with - * deprecated functionality disabled, default implementation does - * nothing. + * On builds with @ref MAGNUM_BUILD_DEPRECATED enabled, if the pointer + * is a mouse, default implementation delegates to + * @ref mouseReleaseEvent(). Touch events rely on browser's implicit + * translation to compatibility mouse events in this case, which is + * otherwise disabled. On builds with deprecated functionality + * disabled, default implementation does nothing. */ virtual void pointerReleaseEvent(PointerEvent& event); @@ -888,13 +928,15 @@ class EmscriptenApplication { * changes its properties. Gets called also if the set of pressed mouse * buttons changes. * - * On builds with @ref MAGNUM_BUILD_DEPRECATED enabled, default - * implementation delegates to @ref mouseMoveEvent(), or if - * @ref PointerMoveEvent::pointer() is not + * On builds with @ref MAGNUM_BUILD_DEPRECATED enabled, if the pointer + * is a mouse, default implementation delegates to + * @ref mouseMoveEvent(), or if @ref PointerMoveEvent::pointer() is not * @relativeref{Corrade,Containers::NullOpt}, to either - * @ref mousePressEvent() or @ref mouseReleaseEvent(). On builds with - * deprecated functionality disabled, default implementation does - * nothing. + * @ref mousePressEvent() or @ref mouseReleaseEvent(). Unlike touch + * press and release, touch drag events weren't translated to + * compatibility mouse events before, so they're not propagated now + * either. On builds with deprecated functionality disabled, default + * implementation does nothing. */ virtual void pointerMoveEvent(PointerMoveEvent& event); @@ -1020,6 +1062,21 @@ class EmscriptenApplication { Vector2 _previousMouseMovePosition{Constants::nan()}; Vector2 _lastKnownDevicePixelRatio; + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 + /* We have no way to query previous touch positions, so we have to + maintain them like this. The id is ~Int{} if given slot is unused, + 32 is what EmscriptenTouchEvent uses for the touch list. */ + struct { + Int id = ~Int{}; + Vector2 position; + } _previousTouches[32]; + Int _primaryFingerId = ~Int{}; + /* Timestamp of the last touch event, to detect and ignore + compatibility mouse events. There's no better way either, see the + source for details. */ + Double _lastTouchEventTimestamp = Constantsd::nan(); + #endif + Flags _flags; Cursor _cursor = Cursor::Arrow; @@ -1042,6 +1099,31 @@ class EmscriptenApplication { int (*_callback)(void*); }; +/** +@brief Pointer event source +@m_since_latest + +@see @ref PointerEvent::source(), @ref PointerMoveEvent::source() +*/ +enum class EmscriptenApplication::PointerEventSource: UnsignedByte { + /** + * The event is coming from a mouse + * @see @ref Pointer::MouseLeft, @ref Pointer::MouseMiddle, + * @ref Pointer::MouseRight, @ref Pointer::MouseButton4, + * @ref Pointer::MouseButton5 + */ + Mouse, + + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 || defined(DOXYGEN_GENERATING_OUTPUT) + /** + * The event is coming from a touch contact + * @note Available since Emscripten 2.0.27. + * @see @ref Pointer::Finger + */ + Touch + #endif +}; + /** @brief Pointer type @m_since_latest @@ -1050,20 +1132,47 @@ class EmscriptenApplication { @ref PointerMoveEvent::pointer(), @ref PointerMoveEvent::pointers() */ enum class EmscriptenApplication::Pointer: UnsignedByte { - /** Left mouse button */ + /** + * Left mouse button + * @see @ref PointerEventSource::Mouse + */ MouseLeft = 1 << 0, - /** Middle mouse button */ + /** + * Middle mouse button + * @see @ref PointerEventSource::Mouse + */ MouseMiddle = 1 << 1, - /** Right mouse button */ + /** + * Right mouse button + * @see @ref PointerEventSource::Mouse + */ MouseRight = 1 << 2, - /** Fourth mouse button, such as wheel left */ + /** + * Fourth mouse button, such as wheel left + * @see @ref PointerEventSource::Mouse + */ MouseButton4 = 1 << 3, - /** Fourth mouse button, such as wheel right */ + /** + * Fourth mouse button, such as wheel right + * @see @ref PointerEventSource::Mouse + */ MouseButton5 = 1 << 4, + + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 || defined(DOXYGEN_GENERATING_OUTPUT) + /** + * Finger + * @note Available since Emscripten 2.0.27. + * @see @ref PointerEventSource::Touch + */ + Finger = 1 << 5, + #endif + + /** @todo pen support, once there's any progress in + https://github.com/emscripten-core/emscripten/issues/7278 */ }; CORRADE_ENUMSET_OPERATORS(EmscriptenApplication::Pointers) @@ -1706,6 +1815,9 @@ class EmscriptenApplication::PointerEvent: public InputEvent { /** @brief Moving is not allowed */ PointerEvent& operator=(PointerEvent&&) = delete; + /** @brief Pointer event source */ + PointerEventSource source() { return _source; } + /** * @brief Pointer type that was pressed or released * @@ -1716,28 +1828,84 @@ class EmscriptenApplication::PointerEvent: public InputEvent { */ 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-EmscriptenApplication-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-EmscriptenApplication-touch for more + * information. + */ + /* Long is for consistency with Sdl2Application, Emscripten uses just + an Int */ + Long id() const { return _id; } + /** * @brief Position * * The position is always reported in whole pixels. */ - Vector2 position() const; + Vector2 position() const { return _position; } /** @brief Modifiers */ - Modifiers modifiers() const; + Modifiers modifiers() const { return _modifiers; } - /** @brief Underlying Emscripten event */ - const EmscriptenMouseEvent& event() const { return _event; } + /** + * @brief Underlying Emscripten event + * + * The @p T can only be `EmscriptenMouseEvent` for + * @ref PointerEventSource::Mouse and `EmscriptenTouchEvent` for + * @ref PointerEventSource::Touch. Note that in case of a multi-touch + * event, all emitted events point to the same `EmscriptenTouchEvent` + * instance. The concrete `EmscriptenTouchPoint` corresponding to given + * event is the one that has the @cpp touches[i].identifier @ce + * matching @ref id(). + */ + template const T& event() const; private: friend EmscriptenApplication; - explicit PointerEvent(const EmscriptenMouseEvent& event, Pointer pointer): _event(event), _pointer{pointer} {} + explicit PointerEvent(const EmscriptenMouseEvent& event, Pointer pointer, Modifiers modifiers, const Vector2& position): _event{&event}, _source{PointerEventSource::Mouse}, _primary{true}, _pointer{pointer}, _modifiers{modifiers}, _id{~Int{}}, _position{position} {} + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 + explicit PointerEvent(const EmscriptenTouchEvent& event, bool primary, Int id, Modifiers modifiers, const Vector2& position): _event{&event}, _source{PointerEventSource::Touch}, _primary{primary}, _pointer{Pointer::Finger}, _modifiers{modifiers}, _id{id}, _position{position} {} + #endif - const EmscriptenMouseEvent& _event; + const void* _event; + const PointerEventSource _source; + const bool _primary; const Pointer _pointer; + const Modifiers _modifiers; + const Int _id; + const Vector2 _position; }; +#ifndef DOXYGEN_GENERATING_OUTPUT +template<> inline const EmscriptenMouseEvent& EmscriptenApplication::PointerEvent::event() const { + CORRADE_ASSERT(_source == PointerEventSource::Mouse, + "Platform::EmscriptenApplication::PointerEvent::event(): not a mouse event", + *static_cast(_event)); + return *static_cast(_event); +} + +#if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 +template<> inline const EmscriptenTouchEvent& EmscriptenApplication::PointerEvent::event() const { + CORRADE_ASSERT(_source == PointerEventSource::Touch, + "Platform::EmscriptenApplication::PointerEvent::event(): not a touch event", + *static_cast(_event)); + return *static_cast(_event); +} +#endif +#endif + #ifdef MAGNUM_BUILD_DEPRECATED /** @brief Mouse event @@ -1803,6 +1971,15 @@ class EmscriptenApplication::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() { return _source; } + /** * @brief Pointer type that was added or removed from the set of pressed pointers * @@ -1823,12 +2000,32 @@ class EmscriptenApplication::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-EmscriptenApplication-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-EmscriptenApplication-touch for more + * information. + */ + /* Long is for consistency with Sdl2Application, Emscripten uses just + an Int */ + Long id() const { return _id; } + /** * @brief Position * * The position is always reported in whole pixels. */ - Vector2 position() const; + Vector2 position() const { return _position; } /** * @brief Position relative to the previous touch event @@ -1841,22 +2038,58 @@ class EmscriptenApplication::PointerMoveEvent: public InputEvent { Vector2 relativePosition() const { return _relativePosition; } /** @brief Modifiers */ - Modifiers modifiers() const; + Modifiers modifiers() const { return _modifiers; } - /** @brief Underlying Emscripten event */ - const EmscriptenMouseEvent& event() const { return _event; } + /** + * @brief Underlying Emscripten event + * + * The @p T can only be `EmscriptenMouseEvent` for + * @ref PointerEventSource::Mouse and `EmscriptenTouchEvent` for + * @ref PointerEventSource::Touch. Note that in case of a multi-touch + * event, all emitted events point to the same `EmscriptenTouchEvent` + * instance. The concrete `EmscriptenTouchPoint` corresponding to given + * event is the one that has the @cpp touches[i].identifier @ce + * matching @ref id(). + */ + template const T& event() const; private: friend EmscriptenApplication; - explicit PointerMoveEvent(const EmscriptenMouseEvent& event, Containers::Optional pointer, Pointers pointers, const Vector2& relativePosition): _event(event), _pointer{pointer}, _pointers{pointers}, _relativePosition{relativePosition} {} + explicit PointerMoveEvent(const EmscriptenMouseEvent& event, Containers::Optional pointer, Pointers pointers, Modifiers modifiers, const Vector2& position, const Vector2& relativePosition): _event{&event}, _source{PointerEventSource::Mouse}, _primary{true}, _pointer{pointer}, _pointers{pointers}, _modifiers{modifiers}, _id{~Int{}}, _position{position}, _relativePosition{relativePosition} {} + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 + explicit PointerMoveEvent(const EmscriptenTouchEvent& event, bool primary, Int id, Modifiers modifiers, const Vector2& position, const Vector2& relativePosition): _event{&event}, _source{PointerEventSource::Touch}, _primary{primary}, _pointer{}, _pointers{Pointer::Finger}, _modifiers{modifiers}, _id{id}, _position{position}, _relativePosition{relativePosition} {} + #endif - const EmscriptenMouseEvent& _event; + const void* _event; + const PointerEventSource _source; + const bool _primary; const Containers::Optional _pointer; const Pointers _pointers; + const Modifiers _modifiers; + const Int _id; + const Vector2 _position; const Vector2 _relativePosition; }; +#ifndef DOXYGEN_GENERATING_OUTPUT +template<> inline const EmscriptenMouseEvent& EmscriptenApplication::PointerMoveEvent::event() const { + CORRADE_ASSERT(_source == PointerEventSource::Mouse, + "Platform::EmscriptenApplication::PointerEvent::event(): not a mouse event", + *static_cast(_event)); + return *static_cast(_event); +} + +#if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 +template<> inline const EmscriptenTouchEvent& EmscriptenApplication::PointerMoveEvent::event() const { + CORRADE_ASSERT(_source == PointerEventSource::Touch, + "Platform::EmscriptenApplication::PointerEvent::event(): not a touch event", + *static_cast(_event)); + return *static_cast(_event); +} +#endif +#endif + #ifdef MAGNUM_BUILD_DEPRECATED /** @brief Mouse move event diff --git a/src/Magnum/Platform/Sdl2Application.cpp b/src/Magnum/Platform/Sdl2Application.cpp index 73f794977b..9a58201c6b 100644 --- a/src/Magnum/Platform/Sdl2Application.cpp +++ b/src/Magnum/Platform/Sdl2Application.cpp @@ -1121,10 +1121,11 @@ bool Sdl2Application::mainLoopIteration() { becomes primary until all others are lifted as well. This 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. 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. */ + 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. */ 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 404e460e91..fab274e8de 100644 --- a/src/Magnum/Platform/Sdl2Application.h +++ b/src/Magnum/Platform/Sdl2Application.h @@ -306,7 +306,8 @@ pressed fingers are non-primary. Note that there can be up to one primary 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. +until all others are lifted as well. The same logic is implemented in +@ref EmscriptenApplication. 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/EmscriptenApplicationTest.cpp b/src/Magnum/Platform/Test/EmscriptenApplicationTest.cpp index d6d82fbad1..8239f317bb 100644 --- a/src/Magnum/Platform/Test/EmscriptenApplicationTest.cpp +++ b/src/Magnum/Platform/Test/EmscriptenApplicationTest.cpp @@ -27,6 +27,7 @@ #include #include +#include #include "Magnum/Platform/EmscriptenApplication.h" #include "Magnum/GL/Renderer.h" @@ -75,6 +76,9 @@ static Debug& operator<<(Debug& debug, Application::Pointer value) { _c(MouseRight) _c(MouseButton4) _c(MouseButton5) + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 + _c(Finger) + #endif #undef _c } @@ -101,6 +105,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(Mouse) + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 + _c(Touch) + #endif + #undef _c + } + + return debug << "(" << Debug::nospace << UnsignedInt(value) << Debug::nospace << ")"; +} + Debug& operator<<(Debug& debug, Application::InputEvent::Modifiers value) { return Containers::enumSetDebugOutput(debug, value, "Modifiers{}", { Application::InputEvent::Modifier::Shift, @@ -117,6 +136,9 @@ Debug& operator<<(Debug& debug, Application::Pointers value) { Application::Pointer::MouseRight, Application::Pointer::MouseButton4, Application::Pointer::MouseButton5, + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 + Application::Pointer::Finger, + #endif }); } @@ -295,13 +317,34 @@ struct EmscriptenApplicationTest: Platform::Application { /* Set to 0 to test the deprecated mouse events instead */ #if 1 void pointerPressEvent(PointerEvent& event) override { - Debug{} << "pointer press:" << event.pointer() << event.modifiers() << Debug::packed << event.position(); + Debug{} << "pointer press:" << event.source() << event.pointer() << (event.isPrimary() ? "primary" : "secondary") << event.id() << event.modifiers() << Debug::packed << event.position() + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 + /* Just to verify the access works for both cases */ + << (event.source() == PointerEventSource::Mouse ? + event.event().timestamp : + event.event().timestamp) + #endif + ; } void pointerReleaseEvent(PointerEvent& event) override { - Debug{} << "pointer release:" << event.pointer() << event.modifiers() << Debug::packed << event.position(); + Debug{} << "pointer release:" << event.source() << event.pointer() << (event.isPrimary() ? "primary" : "secondary") << event.id() << event.modifiers() << Debug::packed << event.position() + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 + /* Just to verify the access works for both cases */ + << (event.source() == PointerEventSource::Mouse ? + event.event().timestamp : + event.event().timestamp) + #endif + ; } void pointerMoveEvent(PointerMoveEvent& event) override { - Debug{} << "pointer move:" << event.pointer() << event.pointers() << event.modifiers() << Debug::packed << event.position() << Debug::packed << event.relativePosition(); + Debug{} << "pointer move:" << event.source() << event.pointer() << event.pointers() << (event.isPrimary() ? "primary" : "secondary") << event.id() << event.modifiers() << Debug::packed << event.position() << Debug::packed << event.relativePosition() + #if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027 + /* Just to verify the access works for both cases */ + << (event.source() == PointerEventSource::Mouse ? + event.event().timestamp : + event.event().timestamp) + #endif + ; } void scrollEvent(ScrollEvent& event) override { Debug{} << "scroll:" << event.modifiers() << Debug::packed << event.offset() << Debug::packed << event.position();