Feature: add support for multitouch (#9581) 12764e9a3b

add support for multitouch

Co-authored-by: hernan <hernan@rive.app>
This commit is contained in:
bodymovin
2025-09-30 21:29:34 +00:00
parent d0faf6e011
commit 89fcbd1e25
11 changed files with 261 additions and 113 deletions

View File

@@ -1 +1 @@
0b069a99369e018723e72b703b306bfc6e4cd94a
12764e9a3bb3db1a8a9258478fa0c078e4c0ce11

View File

@@ -24,10 +24,12 @@ public:
void initializeAnimation(ArtboardInstance*) override;
StateMachineInstance* stateMachineInstance();
HitResult pointerMove(Vec2D position, float timeStamp = 0);
HitResult pointerDown(Vec2D position);
HitResult pointerUp(Vec2D position);
HitResult pointerExit(Vec2D position);
HitResult pointerMove(Vec2D position,
float timeStamp = 0,
int pointerId = 0);
HitResult pointerDown(Vec2D position, int pointerId = 0);
HitResult pointerUp(Vec2D position, int pointerId = 0);
HitResult pointerExit(Vec2D position, int pointerId = 0);
HitResult dragStart(Vec2D position, float timeStamp = 0);
HitResult dragEnd(Vec2D position, float timeStamp = 0);
bool tryChangeState();

View File

@@ -61,6 +61,7 @@ private:
/// pointer position too.
HitResult updateListeners(Vec2D position,
ListenerType hitListener,
int pointerId = 0,
float timeStamp = 0);
template <typename SMType, typename InstType>
@@ -131,10 +132,12 @@ public:
void advancedDataContext();
void reset();
std::string name() const override;
HitResult pointerMove(Vec2D position, float timeStamp = 0) override;
HitResult pointerDown(Vec2D position) override;
HitResult pointerUp(Vec2D position) override;
HitResult pointerExit(Vec2D position) override;
HitResult pointerMove(Vec2D position,
float timeStamp = 0,
int pointerId = 0) override;
HitResult pointerDown(Vec2D position, int pointerId = 0) override;
HitResult pointerUp(Vec2D position, int pointerId = 0) override;
HitResult pointerExit(Vec2D position, int pointerId = 0) override;
HitResult dragStart(Vec2D position,
float timeStamp = 0,
bool disablePointer = true);
@@ -256,8 +259,11 @@ public:
virtual HitResult processEvent(Vec2D position,
ListenerType hitType,
bool canHit,
float timeStamp = 0) = 0;
virtual void prepareEvent(Vec2D position, ListenerType hitType) = 0;
float timeStamp = 0,
int pointerId = 0) = 0;
virtual void prepareEvent(Vec2D position,
ListenerType hitType,
int pointerId) = 0;
virtual bool hitTest(Vec2D position) const = 0;
virtual void enablePointerEvents() {}
virtual void disablePointerEvents() {}

View File

@@ -52,10 +52,12 @@ public:
virtual void bindViewModelInstance(
rcp<ViewModelInstance> viewModelInstance);
virtual HitResult pointerDown(Vec2D);
virtual HitResult pointerMove(Vec2D position, float timeStamp = 0);
virtual HitResult pointerUp(Vec2D);
virtual HitResult pointerExit(Vec2D);
virtual HitResult pointerDown(Vec2D, int pointerId = 0);
virtual HitResult pointerMove(Vec2D position,
float timeStamp = 0,
int pointerId = 0);
virtual HitResult pointerUp(Vec2D, int pointerId = 0);
virtual HitResult pointerExit(Vec2D, int pointerId = 0);
virtual size_t inputCount() const;
virtual SMIInput* input(size_t index) const;

View File

@@ -48,38 +48,42 @@ bool NestedStateMachine::hitTest(Vec2D position) const
return false;
}
HitResult NestedStateMachine::pointerMove(Vec2D position, float timeStamp)
HitResult NestedStateMachine::pointerMove(Vec2D position,
float timeStamp,
int pointerId)
{
if (m_StateMachineInstance != nullptr)
{
return m_StateMachineInstance->pointerMove(position, timeStamp);
return m_StateMachineInstance->pointerMove(position,
timeStamp,
pointerId);
}
return HitResult::none;
}
HitResult NestedStateMachine::pointerDown(Vec2D position)
HitResult NestedStateMachine::pointerDown(Vec2D position, int pointerId)
{
if (m_StateMachineInstance != nullptr)
{
return m_StateMachineInstance->pointerDown(position);
return m_StateMachineInstance->pointerDown(position, pointerId);
}
return HitResult::none;
}
HitResult NestedStateMachine::pointerUp(Vec2D position)
HitResult NestedStateMachine::pointerUp(Vec2D position, int pointerId)
{
if (m_StateMachineInstance != nullptr)
{
return m_StateMachineInstance->pointerUp(position);
return m_StateMachineInstance->pointerUp(position, pointerId);
}
return HitResult::none;
}
HitResult NestedStateMachine::pointerExit(Vec2D position)
HitResult NestedStateMachine::pointerExit(Vec2D position, int pointerId)
{
if (m_StateMachineInstance != nullptr)
{
return m_StateMachineInstance->pointerExit(position);
return m_StateMachineInstance->pointerExit(position, pointerId);
}
return HitResult::none;
}

View File

@@ -517,38 +517,76 @@ private:
float m_holdTime = 0.0f;
};
class _PointerData
{
public:
bool isHovered = false;
bool isPrevHovered = false;
GestureClickPhase phase = GestureClickPhase::out;
Vec2D* previousPosition() { return &m_previousPosition; }
private:
Vec2D m_previousPosition = Vec2D(0, 0);
};
class ListenerGroup
{
public:
ListenerGroup(const StateMachineListener* listener) : m_listener(listener)
{}
virtual ~ListenerGroup() {}
void consume() { m_isConsumed = true; }
//
void hover() { m_isHovered = true; }
void unhover() { m_isHovered = false; }
void reset()
virtual ~ListenerGroup()
{
if (m_clickPhase != GestureClickPhase::disabled)
for (auto& pointerData : m_pointers)
{
m_isConsumed = false;
m_prevIsHovered = m_isHovered;
m_isHovered = false;
}
if (m_clickPhase == GestureClickPhase::clicked)
{
m_clickPhase = GestureClickPhase::out;
delete pointerData.second;
}
}
_PointerData* pointerData(int id)
{
if (m_pointers.find(id) == m_pointers.end())
{
m_pointers[id] = new _PointerData();
}
return m_pointers[id];
}
void consume() { m_isConsumed = true; }
//
void hover(int id)
{
auto pointer = pointerData(id);
pointer->isHovered = true;
}
void reset(int pointerId)
{
auto pointer = pointerData(pointerId);
if (pointer->phase != GestureClickPhase::disabled)
{
m_isConsumed = false;
pointer->isPrevHovered = pointer->isHovered;
pointer->isHovered = false;
}
if (pointer->phase == GestureClickPhase::clicked)
{
pointer->phase = GestureClickPhase::out;
}
}
virtual void enable()
{
for (auto& pointer : m_pointers)
{
pointer.second->phase = GestureClickPhase::out;
}
}
virtual void enable() { m_clickPhase = GestureClickPhase::out; }
virtual void disable()
{
m_clickPhase = GestureClickPhase::disabled;
for (auto& pointer : m_pointers)
{
pointer.second->phase = GestureClickPhase::disabled;
}
consume();
}
bool isConsumed() { return m_isConsumed; }
bool isHovered() { return m_isHovered; }
bool prevHovered() { return m_prevIsHovered; }
virtual bool canEarlyOut(Component* drawable)
{
@@ -571,10 +609,11 @@ public:
return listenerType == ListenerType::up ||
listenerType == ListenerType::click;
}
// Vec2D position, ListenerType hitType, bool canHit
virtual ProcessEventResult processEvent(
Component* component,
Vec2D position,
int pointerId,
ListenerType hitEvent,
bool canHit,
float timeStamp,
@@ -587,19 +626,21 @@ public:
// in isolation, it shouldn't be considered as hovered in the full
// context. In this case, we unhover the group so it is not marked as
// previously hovered.
if (!canHit && isHovered())
auto pointer = pointerData(pointerId);
if (!canHit && pointer->isHovered)
{
unhover();
pointer->isHovered = false;
}
bool isGroupHovered = canHit ? isHovered() : false;
bool hoverChange = prevHovered() != isGroupHovered;
bool isGroupHovered = canHit ? pointer->isHovered : false;
bool hoverChange = pointer->isPrevHovered != isGroupHovered;
// If hover has changes, it means that the element is hovered for the
// first time. Previous positions need to be reset to avoid jumps.
auto previousPosition = pointer->previousPosition();
if (hoverChange && isGroupHovered)
{
previousPosition.x = position.x;
previousPosition.y = position.y;
previousPosition->x = position.x;
previousPosition->y = position.y;
}
// Handle click gesture phases. A click gesture has two phases.
@@ -610,19 +651,19 @@ public:
{
if (hitEvent == ListenerType::down)
{
clickPhase(GestureClickPhase::down);
pointer->phase = GestureClickPhase::down;
}
else if (hitEvent == ListenerType::up &&
clickPhase() == GestureClickPhase::down)
pointer->phase == GestureClickPhase::down)
{
clickPhase(GestureClickPhase::clicked);
pointer->phase = GestureClickPhase::clicked;
}
}
else
{
if (hitEvent == ListenerType::down || hitEvent == ListenerType::up)
{
clickPhase(GestureClickPhase::out);
pointer->phase = GestureClickPhase::out;
}
}
auto _listener = listener();
@@ -636,9 +677,10 @@ public:
(!isGroupHovered &&
_listener->listenerType() == ListenerType::exit)))
{
_listener->performChanges(stateMachineInstance,
position,
previousPosition);
_listener->performChanges(
stateMachineInstance,
position,
Vec2D(previousPosition->x, previousPosition->y));
stateMachineInstance->markNeedsAdvance();
consume();
}
@@ -646,38 +688,29 @@ public:
// - the click gesture is complete and the listener is of type click
// - the event type matches the listener type and it is hovering the
// group
if ((clickPhase() == GestureClickPhase::clicked &&
if ((pointer->phase == GestureClickPhase::clicked &&
_listener->listenerType() == ListenerType::click) ||
(isGroupHovered && hitEvent == _listener->listenerType()))
{
_listener->performChanges(stateMachineInstance,
position,
previousPosition);
_listener->performChanges(
stateMachineInstance,
position,
Vec2D(previousPosition->x, previousPosition->y));
stateMachineInstance->markNeedsAdvance();
consume();
}
previousPosition.x = position.x;
previousPosition.y = position.y;
previousPosition->x = position.x;
previousPosition->y = position.y;
return ProcessEventResult::pointer;
}
void clickPhase(GestureClickPhase value) { m_clickPhase = value; }
GestureClickPhase clickPhase() { return m_clickPhase; }
const StateMachineListener* listener() const { return m_listener; };
// A vector storing the previous position for this specific listener gorup
Vec2D previousPosition;
private:
// Consumed listeners aren't processed again in the current frame
bool m_isConsumed = false;
// This variable holds the hover status of the the listener itself so it can
// be shared between all shapes that target it
bool m_isHovered = false;
// Variable storing the previous hovered state to check for hover changes
bool m_prevIsHovered = false;
// A click gesture is composed of three phases and is shared between all
// shapes
GestureClickPhase m_clickPhase = GestureClickPhase::out;
const StateMachineListener* m_listener;
std::unordered_map<int, _PointerData*> m_pointers;
};
class DraggableConstraintListenerGroup : public ListenerGroup
@@ -706,25 +739,27 @@ public:
bool needsDownListener(Component* drawable) override { return true; }
bool needsUpListener(Component* drawable) override { return true; }
// Vec2D position, ListenerType hitType, bool canHit
ProcessEventResult processEvent(
Component* component,
Vec2D position,
int pointerId,
ListenerType hitEvent,
bool canHit,
float timeStamp,
StateMachineInstance* stateMachineInstance) override
{
auto prevPhase = clickPhase();
auto pointer = pointerData(pointerId);
auto prevPhase = pointer->phase;
ListenerGroup::processEvent(component,
position,
pointerId,
hitEvent,
canHit,
timeStamp,
stateMachineInstance);
if (prevPhase == GestureClickPhase::down &&
(clickPhase() == GestureClickPhase::clicked ||
clickPhase() == GestureClickPhase::out))
(pointer->phase == GestureClickPhase::clicked ||
pointer->phase == GestureClickPhase::out))
{
m_draggable->endDrag(position, timeStamp);
if (m_hasScrolled)
@@ -735,13 +770,13 @@ public:
}
}
else if (prevPhase != GestureClickPhase::down &&
clickPhase() == GestureClickPhase::down)
pointer->phase == GestureClickPhase::down)
{
m_draggable->startDrag(position, timeStamp);
m_hasScrolled = false;
}
else if (hitEvent == ListenerType::move &&
clickPhase() == GestureClickPhase::down)
pointer->phase == GestureClickPhase::down)
{
auto hasDragged = m_draggable->drag(position, timeStamp);
if (hasDragged)
@@ -793,7 +828,9 @@ public:
bool hitTest(Vec2D position) const override { return false; }
void prepareEvent(Vec2D position, ListenerType hitType) override
void prepareEvent(Vec2D position,
ListenerType hitType,
int pointerId) override
{
if (canEarlyOut &&
(hitType != ListenerType::down || !hasDownListener) &&
@@ -812,7 +849,7 @@ public:
for (auto listenerGroup : listeners)
{
listenerGroup->hover();
listenerGroup->hover(pointerId);
}
}
}
@@ -820,7 +857,8 @@ public:
HitResult processEvent(Vec2D position,
ListenerType hitType,
bool canHit,
float timeStamp) override
float timeStamp,
int pointerId) override
{
// If the shape doesn't have any ListenerType::move / enter / exit and
// the event being processed is not of the type it needs to handle.
@@ -842,6 +880,7 @@ public:
}
if (listenerGroup->processEvent(m_component,
position,
pointerId,
hitType,
canHit,
timeStamp,
@@ -984,7 +1023,8 @@ public:
HitResult processEvent(Vec2D position,
ListenerType hitType,
bool canHit,
float timeStamp) override
float timeStamp,
int pointerId) override
{
auto nestedArtboard = m_component->as<NestedArtboard>();
HitResult hitResult = HitResult::none;
@@ -1011,16 +1051,19 @@ public:
{
case ListenerType::down:
hitResult =
nestedStateMachine->pointerDown(nestedPosition);
nestedStateMachine->pointerDown(nestedPosition,
pointerId);
break;
case ListenerType::up:
hitResult =
nestedStateMachine->pointerUp(nestedPosition);
nestedStateMachine->pointerUp(nestedPosition,
pointerId);
break;
case ListenerType::move:
hitResult =
nestedStateMachine->pointerMove(nestedPosition,
timeStamp);
timeStamp,
pointerId);
break;
case ListenerType::dragStart:
nestedStateMachine->dragStart(nestedPosition,
@@ -1068,7 +1111,10 @@ public:
}
return hitResult;
}
void prepareEvent(Vec2D position, ListenerType hitType) override {}
void prepareEvent(Vec2D position,
ListenerType hitType,
int pointerId) override
{}
};
class HitComponentList : public HitComponent
@@ -1106,7 +1152,8 @@ public:
HitResult processEvent(Vec2D position,
ListenerType hitType,
bool canHit,
float timeStamp) override
float timeStamp,
int pointerId) override
{
auto componentList = m_component->as<ArtboardComponentList>();
HitResult hitResult = HitResult::none;
@@ -1133,15 +1180,18 @@ public:
{
case ListenerType::down:
itemHitResult =
stateMachine->pointerDown(listPosition);
stateMachine->pointerDown(listPosition,
pointerId);
break;
case ListenerType::up:
itemHitResult =
stateMachine->pointerUp(listPosition);
stateMachine->pointerUp(listPosition,
pointerId);
break;
case ListenerType::move:
itemHitResult =
stateMachine->pointerMove(listPosition);
stateMachine->pointerMove(listPosition,
pointerId);
break;
case ListenerType::exit:
itemHitResult =
@@ -1170,7 +1220,7 @@ public:
case ListenerType::up:
case ListenerType::move:
case ListenerType::exit:
stateMachine->pointerExit(listPosition);
stateMachine->pointerExit(listPosition, pointerId);
break;
case ListenerType::dragStart:
case ListenerType::dragEnd:
@@ -1199,7 +1249,10 @@ public:
}
return hitResult;
}
void prepareEvent(Vec2D position, ListenerType hitType) override {}
void prepareEvent(Vec2D position,
ListenerType hitType,
int pointerId) override
{}
};
class ListenerViewModel : public Dirtyable
@@ -1253,6 +1306,7 @@ private:
HitResult StateMachineInstance::updateListeners(Vec2D position,
ListenerType hitType,
int pointerId,
float timeStamp)
{
if (m_artboardInstance->frameOrigin())
@@ -1264,12 +1318,12 @@ HitResult StateMachineInstance::updateListeners(Vec2D position,
// First reset all listener groups before processing the events
for (const auto& listenerGroup : m_listenerGroups)
{
listenerGroup.get()->reset();
listenerGroup.get()->reset(pointerId);
}
// Next prepare the event to set the common hover status for each group
for (const auto& hitShape : m_hitComponents)
{
hitShape->prepareEvent(position, hitType);
hitShape->prepareEvent(position, hitType, pointerId);
}
bool hitSomething = false;
bool hitOpaque = false;
@@ -1278,8 +1332,11 @@ HitResult StateMachineInstance::updateListeners(Vec2D position,
{
// TODO: quick reject.
HitResult hitResult =
hitShape->processEvent(position, hitType, !hitOpaque, timeStamp);
HitResult hitResult = hitShape->processEvent(position,
hitType,
!hitOpaque,
timeStamp,
pointerId);
if (hitResult != HitResult::none)
{
hitSomething = true;
@@ -1314,21 +1371,23 @@ bool StateMachineInstance::hitTest(Vec2D position) const
return false;
}
HitResult StateMachineInstance::pointerMove(Vec2D position, float timeStamp)
HitResult StateMachineInstance::pointerMove(Vec2D position,
float timeStamp,
int id)
{
return updateListeners(position, ListenerType::move, timeStamp);
return updateListeners(position, ListenerType::move, id, timeStamp);
}
HitResult StateMachineInstance::pointerDown(Vec2D position)
HitResult StateMachineInstance::pointerDown(Vec2D position, int id)
{
return updateListeners(position, ListenerType::down);
return updateListeners(position, ListenerType::down, id);
}
HitResult StateMachineInstance::pointerUp(Vec2D position)
HitResult StateMachineInstance::pointerUp(Vec2D position, int id)
{
return updateListeners(position, ListenerType::up);
return updateListeners(position, ListenerType::up, id);
}
HitResult StateMachineInstance::pointerExit(Vec2D position)
HitResult StateMachineInstance::pointerExit(Vec2D position, int id)
{
return updateListeners(position, ListenerType::exit);
return updateListeners(position, ListenerType::exit, id);
}
HitResult StateMachineInstance::dragStart(Vec2D position,
float timeStamp,

View File

@@ -2565,7 +2565,7 @@ bool CommandServer::processCommands()
{
Vec2D position =
cursorPosForPointerEvent(stateMachine, pointerEvent);
stateMachine->pointerDown(position);
stateMachine->pointerDown(position, 0);
}
else
{
@@ -2593,7 +2593,7 @@ bool CommandServer::processCommands()
{
Vec2D position =
cursorPosForPointerEvent(stateMachine, pointerEvent);
stateMachine->pointerUp(position);
stateMachine->pointerUp(position, 0);
}
else
{
@@ -2621,7 +2621,7 @@ bool CommandServer::processCommands()
{
Vec2D position =
cursorPosForPointerEvent(stateMachine, pointerEvent);
stateMachine->pointerExit(position);
stateMachine->pointerExit(position, 0);
}
else
{

View File

@@ -15,13 +15,13 @@ float Scene::height() const { return m_artboardInstance->height(); }
void Scene::draw(Renderer* renderer) { m_artboardInstance->draw(renderer); }
HitResult Scene::pointerDown(Vec2D) { return HitResult::none; }
HitResult Scene::pointerMove(Vec2D position, float timeStamp)
HitResult Scene::pointerDown(Vec2D, int) { return HitResult::none; }
HitResult Scene::pointerMove(Vec2D, float timeStamp, int)
{
return HitResult::none;
}
HitResult Scene::pointerUp(Vec2D) { return HitResult::none; }
HitResult Scene::pointerExit(Vec2D) { return HitResult::none; }
HitResult Scene::pointerUp(Vec2D, int) { return HitResult::none; }
HitResult Scene::pointerExit(Vec2D, int) { return HitResult::none; }
size_t Scene::inputCount() const { return 0; }
SMIInput* Scene::input(size_t index) const { return nullptr; }

Binary file not shown.

View File

@@ -815,7 +815,6 @@ TEST_CASE("Hit testing objects inside shapes", "[silver]")
CHECK(silver.matches("hittest_nested"));
}
TEST_CASE("Pointer exit works correctly", "[silver]")
{
SerializingFactory silver;
@@ -838,7 +837,6 @@ TEST_CASE("Pointer exit works correctly", "[silver]")
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
// Move from [100.0, 250.0] to [400.0, 250.0]
// This will hover over two nested artboards and should unhover once an
// opaque target is hit
@@ -876,4 +874,81 @@ TEST_CASE("Pointer exit works correctly", "[silver]")
}
CHECK(silver.matches("pointer_exit"));
}
TEST_CASE("Hit testing multi touch events", "[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/multitouch.riv", &silver);
auto artboard = file->artboardNamed("main");
REQUIRE(artboard != nullptr);
silver.frameSize(artboard->width(), artboard->height());
auto stateMachine = artboard->stateMachineAt(0);
int viewModelId = artboard.get()->viewModelId();
auto vmi = viewModelId == -1
? file->createViewModelInstance(artboard.get())
: file->createViewModelInstance(viewModelId, 0);
stateMachine->bindViewModelInstance(vmi);
stateMachine->advanceAndApply(0.1f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
// Simple click with single pointer
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(200.0f, 350.0f), 1);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerUp(rive::Vec2D(200.0f, 350.0f), 1);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// New click gesture started with pointer id 1
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(200.0f, 350.0f), 1);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// Pointer up with pointer id 0 should not complete the click gesture
silver.addFrame();
stateMachine->pointerUp(rive::Vec2D(200.0f, 350.0f), 0);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// Pointer up with pointer id 1 should complete the click gesture
silver.addFrame();
stateMachine->pointerUp(rive::Vec2D(200.0f, 350.0f), 1);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// Two click gestures interleaved: 1 down - 0 down - 0 up - 1 up
// should toggle color twice
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(200.0f, 350.0f), 1);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(200.0f, 350.0f), 0);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerUp(rive::Vec2D(200.0f, 350.0f), 0);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerUp(rive::Vec2D(200.0f, 350.0f), 1);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
CHECK(silver.matches("multitouch"));
}

Binary file not shown.