Browse Source

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.
pull/651/head
Vladimír Vondruš 2 years ago
parent
commit
755a5519a8
  1. 8
      doc/changelog.dox
  2. 367
      src/Magnum/Platform/EmscriptenApplication.cpp
  3. 309
      src/Magnum/Platform/EmscriptenApplication.h
  4. 9
      src/Magnum/Platform/Sdl2Application.cpp
  5. 3
      src/Magnum/Platform/Sdl2Application.h
  6. 49
      src/Magnum/Platform/Test/EmscriptenApplicationTest.cpp

8
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

367
src/Magnum/Platform/EmscriptenApplication.cpp

@ -552,6 +552,58 @@ EmscriptenApplication::Pointers buttonsToPointers(const std::uint32_t buttons) {
return pointers;
}
template<class T> 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<class T> 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<class T> Vector2 updatePreviousTouch(T(&previousTouches)[32], const Int id, const Containers::Optional<Vector2>& 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<EmscriptenApplication*>(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<EmscriptenApplication*>(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<EmscriptenApplication*>(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<EmscriptenApplication*>(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<EmscriptenApplication*>(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<EmscriptenApplication*>(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<EmscriptenMouseEvent>()};
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<void>(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<EmscriptenMouseEvent>()};
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<void>(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<EmscriptenMouseEvent>()};
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<EmscriptenMouseEvent>(),
/* 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<class T> 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);

309
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<class T> 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<EmscriptenMouseEvent>() const {
CORRADE_ASSERT(_source == PointerEventSource::Mouse,
"Platform::EmscriptenApplication::PointerEvent::event(): not a mouse event",
*static_cast<const EmscriptenMouseEvent*>(_event));
return *static_cast<const EmscriptenMouseEvent*>(_event);
}
#if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027
template<> inline const EmscriptenTouchEvent& EmscriptenApplication::PointerEvent::event<EmscriptenTouchEvent>() const {
CORRADE_ASSERT(_source == PointerEventSource::Touch,
"Platform::EmscriptenApplication::PointerEvent::event(): not a touch event",
*static_cast<const EmscriptenTouchEvent*>(_event));
return *static_cast<const EmscriptenTouchEvent*>(_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<class T> const T& event() const;
private:
friend EmscriptenApplication;
explicit PointerMoveEvent(const EmscriptenMouseEvent& event, Containers::Optional<Pointer> pointer, Pointers pointers, const Vector2& relativePosition): _event(event), _pointer{pointer}, _pointers{pointers}, _relativePosition{relativePosition} {}
explicit PointerMoveEvent(const EmscriptenMouseEvent& event, Containers::Optional<Pointer> 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> _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<EmscriptenMouseEvent>() const {
CORRADE_ASSERT(_source == PointerEventSource::Mouse,
"Platform::EmscriptenApplication::PointerEvent::event(): not a mouse event",
*static_cast<const EmscriptenMouseEvent*>(_event));
return *static_cast<const EmscriptenMouseEvent*>(_event);
}
#if __EMSCRIPTEN_major__*10000 + __EMSCRIPTEN_minor__*100 + __EMSCRIPTEN_tiny__ >= 20027
template<> inline const EmscriptenTouchEvent& EmscriptenApplication::PointerMoveEvent::event<EmscriptenTouchEvent>() const {
CORRADE_ASSERT(_source == PointerEventSource::Touch,
"Platform::EmscriptenApplication::PointerEvent::event(): not a touch event",
*static_cast<const EmscriptenTouchEvent*>(_event));
return *static_cast<const EmscriptenTouchEvent*>(_event);
}
#endif
#endif
#ifdef MAGNUM_BUILD_DEPRECATED
/**
@brief Mouse move event

9
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;

3
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

49
src/Magnum/Platform/Test/EmscriptenApplicationTest.cpp

@ -27,6 +27,7 @@
#include <Corrade/Containers/EnumSet.hpp>
#include <Corrade/Utility/Arguments.h>
#include <emscripten/html5.h>
#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<EmscriptenMouseEvent>().timestamp :
event.event<EmscriptenTouchEvent>().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<EmscriptenMouseEvent>().timestamp :
event.event<EmscriptenTouchEvent>().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<EmscriptenMouseEvent>().timestamp :
event.event<EmscriptenTouchEvent>().timestamp)
#endif
;
}
void scrollEvent(ScrollEvent& event) override {
Debug{} << "scroll:" << event.modifiers() << Debug::packed << event.offset() << Debug::packed << event.position();

Loading…
Cancel
Save