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();