blob: dc9288eec0f6eb68cb7b9725b2ad36d44a82375a [file] [log] [blame]
/*
* Copyright 2022 Rive
*/
#include <rive/math/aabb.hpp>
#include <rive/math/hit_test.hpp>
#include <rive/nested_artboard.hpp>
#include <rive/animation/state_machine_instance.hpp>
#include <rive/animation/state_machine_input_instance.hpp>
#include <rive/animation/nested_state_machine.hpp>
#include <rive/animation/animation_state.hpp>
#include <rive/viewmodel/viewmodel_instance_number.hpp>
#include "rive_file_reader.hpp"
#include "utils/serializing_factory.hpp"
#include <catch.hpp>
#include <cstdio>
using namespace rive;
TEST_CASE("hittest-basics", "[hittest]")
{
HitTester tester;
tester.reset({10, 10, 12, 12});
tester.move({0, 0});
tester.line({20, 0});
tester.line({20, 20});
tester.line({0, 20});
tester.close();
REQUIRE(tester.test());
IAABB area = {81, 156, 84, 159};
Vec2D pts[] = {
{29.9785f, 32.5261f},
{231.102f, 32.5261f},
{231.102f, 269.898f},
{29.9785f, 269.898f},
};
tester.reset(area);
tester.move(pts[0]);
for (int i = 1; i < 4; ++i)
{
tester.line(pts[i]);
}
tester.close();
REQUIRE(tester.test());
}
TEST_CASE("hittest-mesh", "[hittest]")
{
const IAABB area{10, 10, 12, 12};
Vec2D verts[] = {
{0, 0},
{20, 10},
{0, 20},
};
uint16_t indices[] = {
0,
1,
2,
};
REQUIRE(
HitTester::testMesh(area, make_span(verts, 3), make_span(indices, 3)));
}
TEST_CASE("hit test on opaque target", "[hittest]")
{
// This artboard has two rects of size 200 x 200, "red-activate" at [0, 0,
// 200, 200] and "green-activate" at [0, 100, 200, 300] "red-activate" is
// above "green-activate" in drawing order Both targets are set as opaque
// for its listeners "red-activate" sets "toGreen" to false "green-activate"
// sets "toGreen" to true There is also a "gray-activate" above the other 2
// that is not opaque so events should traverse through the other targets
auto file = ReadRiveFile("assets/opaque_hit_test.riv");
auto artboard = file->artboard("main");
auto artboardInstance = artboard->instance();
auto stateMachine = artboard->stateMachine("main-state-machine");
REQUIRE(artboardInstance != nullptr);
REQUIRE(artboardInstance->stateMachineCount() == 1);
REQUIRE(stateMachine != nullptr);
rive::StateMachineInstance* stateMachineInstance =
new rive::StateMachineInstance(stateMachine, artboardInstance.get());
stateMachineInstance->advance(0.0f);
artboardInstance->advance(0.0f);
REQUIRE(stateMachineInstance->needsAdvance() == true);
stateMachineInstance->advance(0.0f);
auto toGreenToggle = stateMachineInstance->getBool("toGreen");
REQUIRE(toGreenToggle != nullptr);
auto grayToggle = stateMachineInstance->getBool("grayToggle");
REQUIRE(grayToggle != nullptr);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 50.0f));
// "gray-activate" is clicked
REQUIRE(grayToggle->value() == true);
// Pointer only over "red-activate"
REQUIRE(toGreenToggle->value() == false);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 250.0f));
// "gray-activate" is clicked
REQUIRE(grayToggle->value() == false);
// Pointer over "green-activate"
REQUIRE(toGreenToggle->value() == true);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 110.0f));
// "gray-activate" is clicked
REQUIRE(grayToggle->value() == true);
// Pointer over "red-activate" and "green-activate", but "red-activate" is
// opaque and above so green activate does not trigger
REQUIRE(toGreenToggle->value() == false);
delete stateMachineInstance;
}
TEST_CASE("hit test on opaque nested artboard", "[hittest]")
{
// This artboard (300x300) has a main rect at [0, 0, 300, 300]
// this rect has a listener that toggles "second-gray-toggle"
// and a nested artboard at [0, 0, 150, 150]
// the nested artboard and the rect have opaque targets
auto file = ReadRiveFile("assets/opaque_hit_test.riv");
auto artboard = file->artboard("second");
auto artboardInstance = artboard->instance();
auto stateMachine = artboard->stateMachine("second-state-machine");
REQUIRE(artboardInstance != nullptr);
REQUIRE(artboardInstance->stateMachineCount() == 1);
REQUIRE(stateMachine != nullptr);
rive::StateMachineInstance* stateMachineInstance =
new rive::StateMachineInstance(stateMachine, artboardInstance.get());
auto nestedArtboard =
stateMachineInstance->artboard()->find<rive::NestedArtboard>(
"second-nested");
REQUIRE(nestedArtboard != nullptr);
auto nestedArtboardStateMachine =
nestedArtboard->nestedAnimations()[0]->as<NestedStateMachine>();
REQUIRE(nestedArtboardStateMachine != nullptr);
auto nestedArtboardStateMachineInstance =
nestedArtboardStateMachine->stateMachineInstance();
auto secondNestedBoolTarget =
nestedArtboardStateMachineInstance->getBool("bool-target");
REQUIRE(secondNestedBoolTarget != nullptr);
artboardInstance->advance(0.0f);
stateMachineInstance->advanceAndApply(0.0f);
REQUIRE(secondNestedBoolTarget->value() == false);
auto secondGrayToggle = stateMachineInstance->getBool("second-gray-toggle");
REQUIRE(secondGrayToggle != nullptr);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 250.0f));
// toggle changes value because it is not under an opaque nested artboard
REQUIRE(secondGrayToggle->value() == true);
stateMachineInstance->pointerDown(rive::Vec2D(301.0f, 50.0f));
// toggle does not change because it is beyond the area of the square by 1
// pixel And the 2px padding is unly used after the coarse grained test
// passes
REQUIRE(secondGrayToggle->value() == true);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 50.0f));
// toggle does not change because it is under an opaque nested artboard
REQUIRE(secondGrayToggle->value() == true);
// nested toggle changes because it's on top of shape
REQUIRE(secondNestedBoolTarget->value() == true);
// A timeline switches draw order and the nested artboard is now below the
// rect
stateMachineInstance->advanceAndApply(1.0f);
stateMachineInstance->advance(0.0f);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 50.0f));
// So now the pointer down is captured by the rect
REQUIRE(secondGrayToggle->value() == false);
// nested toggle does not change because it's below shape
REQUIRE(secondNestedBoolTarget->value() == true);
delete stateMachineInstance;
}
TEST_CASE("early out on listeners", "[hittest]")
{
auto file = ReadRiveFile("assets/pointer_events.riv");
auto artboard = file->artboard("art-1");
auto artboardInstance = artboard->instance();
auto stateMachine = artboard->stateMachine("sm-1");
REQUIRE(artboardInstance != nullptr);
REQUIRE(artboardInstance->stateMachineCount() == 1);
REQUIRE(stateMachine != nullptr);
rive::StateMachineInstance* stateMachineInstance =
new rive::StateMachineInstance(stateMachine, artboardInstance.get());
stateMachineInstance->advance(0.0f);
artboardInstance->advance(0.0f);
REQUIRE(stateMachineInstance->needsAdvance() == true);
stateMachineInstance->advance(0.0f);
REQUIRE(stateMachineInstance->hitComponentsCount() == 4);
// Hit component with only pointer down and pointer up listeners
auto hitComponentWithEarlyOut = stateMachineInstance->hitComponent(0);
// Hit component that can't early out because it has a pointer enter event
auto hitComponentWithNoEarlyOut = stateMachineInstance->hitComponent(1);
// Hit component that can't early out because it is an opaque target
auto hitComponentOpaque = stateMachineInstance->hitComponent(2);
// Hit component that can early out on all and pointer up
auto hitComponentOnlyPointerDown = stateMachineInstance->hitComponent(3);
REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentOpaque->earlyOutCount == 0);
REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 0);
stateMachineInstance->pointerMove(rive::Vec2D(100.0f, 250.0f));
REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 1);
REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentOpaque->earlyOutCount == 0);
REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 1);
stateMachineInstance->pointerExit(rive::Vec2D(100.0f, 250.0f));
REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 2);
REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 2);
REQUIRE(hitComponentOpaque->earlyOutCount == 0);
stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 250.0f));
REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 2);
REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentOpaque->earlyOutCount == 0);
REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 2);
stateMachineInstance->pointerUp(rive::Vec2D(100.0f, 250.0f));
REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 2);
REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentOpaque->earlyOutCount == 0);
REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 3);
stateMachineInstance->pointerMove(rive::Vec2D(105.0f, 205.0f));
REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 3);
REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
REQUIRE(hitComponentOpaque->earlyOutCount == 0);
REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 4);
delete stateMachineInstance;
}
TEST_CASE("click event", "[hittest]")
{
// This test has two rectangles of size [200, 200]
// positioned at [100,100] and [200, 200]
// they overlap between coordinates [100,100]-[200, 200]
// they are inside a group that has a listener attached to it
// that listener should fire an event on "Click"
auto file = ReadRiveFile("assets/click_event.riv");
auto artboard = file->artboard("art-1");
auto artboardInstance = artboard->instance();
auto stateMachine = artboard->stateMachine("sm-1");
REQUIRE(artboardInstance != nullptr);
REQUIRE(artboardInstance->stateMachineCount() == 1);
REQUIRE(stateMachine != nullptr);
rive::StateMachineInstance* stateMachineInstance =
new rive::StateMachineInstance(stateMachine, artboardInstance.get());
stateMachineInstance->advance(0.0f);
artboardInstance->advance(0.0f);
REQUIRE(stateMachineInstance->needsAdvance() == true);
stateMachineInstance->advance(0.0f);
// There is a single listener with two shapes in it
REQUIRE(stateMachineInstance->hitComponentsCount() == 2);
auto layerCount = stateMachine->layerCount();
REQUIRE(layerCount == 1);
REQUIRE(stateMachineInstance->reportedEventCount() == 0);
// Click in place should trigger a click event
stateMachineInstance->pointerDown(rive::Vec2D(75.0f, 75.0f));
stateMachineInstance->pointerUp(rive::Vec2D(75.0f, 75.0f));
REQUIRE(stateMachineInstance->reportedEventCount() == 1);
// Pointer down inside shape but Pointer up outside the shape
// should not trigger a click event
stateMachineInstance->pointerDown(rive::Vec2D(75.0f, 75.0f));
stateMachineInstance->pointerUp(rive::Vec2D(300.0f, 75.0f));
REQUIRE(stateMachineInstance->reportedEventCount() == 1);
// Pointer down outside shape but Pointer up inside the shape
// should not trigger a click event
stateMachineInstance->pointerDown(rive::Vec2D(300.0f, 75.0f));
stateMachineInstance->pointerUp(rive::Vec2D(75.0f, 75.0f));
REQUIRE(stateMachineInstance->reportedEventCount() == 1);
// Pointer down in shape 1 Pointer up in shape 2 of the same group
// should trigger a click event
stateMachineInstance->pointerDown(rive::Vec2D(75.0f, 75.0f));
stateMachineInstance->pointerUp(rive::Vec2D(225.0f, 225.0f));
REQUIRE(stateMachineInstance->reportedEventCount() == 2);
// Pointer down and up in area where both shapes overlap
// should trigger a single click event
stateMachineInstance->pointerDown(rive::Vec2D(150.0f, 150.0f));
stateMachineInstance->pointerUp(rive::Vec2D(150.0f, 150.0f));
REQUIRE(stateMachineInstance->reportedEventCount() == 3);
delete stateMachineInstance;
}
TEST_CASE("multiple shapes with mouse movement behavior", "[hittest]")
{
// This test has two rectangles of size [200, 200]
// positioned at [100,100] and [100, 200]
// they overlap between coordinates [100,0]-[200, 200]
// they are inside a group that has a Pointer enter and a Pointer out
// listeners that toggle between two states (red and green)
// starting at "red"
auto file = ReadRiveFile("assets/click_event.riv");
auto artboard = file->artboard("art-2");
auto artboardInstance = artboard->instance();
auto stateMachine = artboard->stateMachine("sm-1");
REQUIRE(artboardInstance != nullptr);
REQUIRE(artboardInstance->stateMachineCount() == 1);
REQUIRE(stateMachine != nullptr);
rive::StateMachineInstance* stateMachineInstance =
new rive::StateMachineInstance(stateMachine, artboardInstance.get());
stateMachineInstance->advance(0.0f);
artboardInstance->advance(0.0f);
REQUIRE(stateMachineInstance->needsAdvance() == true);
stateMachineInstance->advance(0.0f);
// There is a single listener with two shapes in it
REQUIRE(stateMachineInstance->hitComponentsCount() == 2);
auto layerCount = stateMachine->layerCount();
REQUIRE(layerCount == 1);
// Move over the first shape
stateMachineInstance->pointerMove(rive::Vec2D(75.0f, 75.0f));
artboardInstance->advance(0.0f);
stateMachineInstance->advanceAndApply(0.0f);
{
auto state = stateMachineInstance->layerState(0);
REQUIRE(state->is<rive::AnimationState>());
auto animation = state->as<rive::AnimationState>()->animation();
REQUIRE(animation->name() == "green");
}
// Move over the second shape, nothing should change
stateMachineInstance->pointerMove(rive::Vec2D(200.0f, 75.0f));
artboardInstance->advance(0.0f);
stateMachineInstance->advanceAndApply(0.0f);
{
auto state = stateMachineInstance->layerState(0);
REQUIRE(state->is<rive::AnimationState>());
auto animation = state->as<rive::AnimationState>()->animation();
REQUIRE(animation->name() == "green");
}
// Move out of the second shape, should go back to red
stateMachineInstance->pointerMove(rive::Vec2D(400.0f, 75.0f));
artboardInstance->advance(0.0f);
stateMachineInstance->advanceAndApply(0.0f);
{
auto state = stateMachineInstance->layerState(0);
REQUIRE(state->is<rive::AnimationState>());
auto animation = state->as<rive::AnimationState>()->animation();
REQUIRE(animation->name() == "red");
}
// Move back into the second shape, should go to green
stateMachineInstance->pointerMove(rive::Vec2D(200.0f, 75.0f));
artboardInstance->advance(0.0f);
stateMachineInstance->advanceAndApply(0.0f);
{
auto state = stateMachineInstance->layerState(0);
REQUIRE(state->is<rive::AnimationState>());
auto animation = state->as<rive::AnimationState>()->animation();
REQUIRE(animation->name() == "green");
}
delete stateMachineInstance;
}
TEST_CASE("Shape clipped by parent layout", "[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/hit_test_test.riv", &silver);
auto artboard = file->artboardNamed("ab-1");
silver.frameSize(artboard->width(), artboard->height());
REQUIRE(artboard != nullptr);
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());
// Move over the shape
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(50.0f, 150.0f));
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
// Move within the shape but the wrapping layout is clipped
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(260.0f, 150.0f));
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
CHECK(silver.matches("hittest_ab1"));
}
TEST_CASE("Shape clipped by parent artboard", "[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/hit_test_test.riv", &silver);
auto artboard = file->artboardNamed("ab1-parent");
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());
// Move over the shape
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(370.0f, 110.0f));
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
// Move within the shape but the wrapping parent layout in the nested
// artboard is clipped
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(370.0f, 180.0f));
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
CHECK(silver.matches("hittest_ab1_parent"));
}
TEST_CASE("Shape clipped by parent and grand-parent artboard", "[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/hit_test_test.riv", &silver);
auto artboard = file->artboardNamed("ab1-grand-parent");
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());
// Move over the shape but outside the grand parent clipping area
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(370.0f, 250.0f));
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
// Move within the shape in a non-clipped area
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(370.0f, 190.0f));
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
// Move over the shape but outside the parent clipping area
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(510.0f, 190.0f));
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
CHECK(silver.matches("hittest_ab1_grand_parent"));
}
TEST_CASE("Artboard list component with scrolling behavior", "[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/hit_test_test.riv", &silver);
auto artboard = file->artboardNamed("ab-2-non-virtualized");
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);
auto scrollProp =
vmi->propertyValue("scroll-offset")->as<ViewModelInstanceNumber>();
stateMachine->bindViewModelInstance(vmi);
stateMachine->advanceAndApply(0.1f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
silver.addFrame();
scrollProp->propertyValue(-100);
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
auto initCoord = 0.0f;
initCoord = 200.0f;
// First move in an area of the artboard where there are listed components
// but they are clipped.
while (initCoord > 100.0f)
{
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(50.0f, initCoord));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
initCoord -= 10;
}
// Next jump to a position of the state to start scrolling the elements
// Should be noticed that the previous hovered components did not turn green
initCoord = 75.0f;
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(50.0f, initCoord));
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
while (initCoord > -500.0f)
{
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(50.0f, initCoord));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
initCoord -= 20;
}
silver.addFrame();
stateMachine->pointerUp(rive::Vec2D(50.0f, initCoord));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// After scroll has ended, move pointer over all visible elements that
// should turn green
initCoord = 110.0f;
while (initCoord > -5.0f)
{
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(50.0f, initCoord));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
initCoord -= 4;
}
CHECK(silver.matches("hittest_ab_2_non_virtualized"));
}
TEST_CASE(
"Artboard list component with scrolling behavior virtualized and carousel",
"[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/hit_test_test.riv", &silver);
auto artboard = file->artboardNamed("ab-2-virtualized");
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);
auto scrollProp =
vmi->propertyValue("scroll-offset")->as<ViewModelInstanceNumber>();
stateMachine->bindViewModelInstance(vmi);
stateMachine->advanceAndApply(0.1f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
silver.addFrame();
scrollProp->propertyValue(-100);
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
auto initCoord = 0.0f;
initCoord = 200.0f;
// First move in an area of the artboard where there are listed components
// but they are clipped In this test, since the scroll is virtualized, the
// pointer is not actually hovering over clipped components at all
while (initCoord > 100.0f)
{
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(50.0f, initCoord));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
initCoord -= 10;
}
// Next jump to a position of the state to start scrolling the elements
// Should be noticed that the previous hovered components did not turn green
initCoord = 75.0f;
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(50.0f, initCoord));
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
while (initCoord > -500.0f)
{
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(50.0f, initCoord));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
initCoord -= 20;
}
silver.addFrame();
stateMachine->pointerUp(rive::Vec2D(50.0f, initCoord));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// After scroll has ended, move pointer over all visible elements that
// should turn green
initCoord = 110.0f;
while (initCoord > -5.0f)
{
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(50.0f, initCoord));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
initCoord -= 4;
}
CHECK(silver.matches("hittest_ab_2_virtualized"));
}
TEST_CASE("Hit testing text in multiple layouts rotated and scaled", "[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/hit_test_test.riv", &silver);
auto artboard = file->artboardNamed("ab-text-parent");
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());
auto initCoord = 400.0f;
// First move the cursor from left to right through the text
// within a clipped and non-clipped area
while (initCoord < 550.0f)
{
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(initCoord, 320.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
initCoord += 10;
}
initCoord = 200.0f;
// First move the cursor from top to bottom through the text
// within a clipped and non-clipped area
while (initCoord < 450.0f)
{
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(500.0f, initCoord));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
initCoord += 10;
}
CHECK(silver.matches("hittest_ab_text_parent"));
}
TEST_CASE("Hit testing shapes in layouts", "[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/hit_test_test.riv", &silver);
auto artboard = file->artboardNamed("ab-shape-parent");
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());
auto initCoord = 0.0f;
// First move the cursor from left to right through the text
// within a clipped and non-clipped area
while (initCoord < 550.0f)
{
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(310.0f, initCoord));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
initCoord += 20;
}
initCoord = 220.0f;
// First move the cursor from top to bottom through the text
// within a clipped and non-clipped area
while (initCoord < 530.0f)
{
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(initCoord, 420.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
initCoord += 20;
}
CHECK(silver.matches("hittest_ab_shape_parent"));
}
TEST_CASE("Hit testing objects inside shapes", "[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/hit_test_nested.riv", &silver);
auto artboard = file->artboardNamed("Main");
REQUIRE(artboard != nullptr);
silver.frameSize(artboard->width(), artboard->height());
auto stateMachine = artboard->stateMachineAt(0);
stateMachine->advanceAndApply(0.1f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
// Hover shape in another shape with no path
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(150.0f, 150.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// Hover nested artboard in another shape with no path
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(300.0f, 200.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// Hover text in another shape with path
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(100.0f, 250.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
// Hover nested artboard in another shape with path
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(400.0f, 350.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
CHECK(silver.matches("hittest_nested"));
}
TEST_CASE("Pointer exit works correctly", "[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/pointer_exit.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());
// 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
float mousePos = 100.0f;
for (mousePos = 100.0f; mousePos <= 400; mousePos += 30)
{
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(mousePos, 250.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
}
// Move from [500.0, 250.0] to [100.0, 250.0]
// This movement will start at the opaque target and should only trigger the
// hover effect once it reaches the emousePosed sections of the nseted
// artboards
for (mousePos = 500.0f; mousePos > 100; mousePos -= 30)
{
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(mousePos, 250.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
}
// Move from [240.0, 390.0] to [240.0, 90.0]
// This movement will start at the opaque target and should only trigger the
// hover effect once it reaches the emousePosed sections of the nseted
// artboards
for (mousePos = 500.0f; mousePos > 100; mousePos -= 30)
{
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(240.0f, mousePos));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
}
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"));
}
TEST_CASE("Multitouch with nested artboard and pointer exit event", "[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/multitouch_enter.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());
// Script
// Advancing by 0.0000s
// Advancing by 0.0167s
// Touch (id: 9) began at {122.5845,443.8406}
// Advancing by 0.0167s
// Touch (id: 8) began at {459.5410,188.4058}
// Touch (id: 7) began at {333.3333,248.1884}
// Advancing by 0.0167s
// Touch (id: 8) ended at {459.5410,188.4058}
// Touch (id: 8) exited at {459.5410,188.4058}
// Touch (id: 9) ended at {123.7923,444.4445}
// Touch (id: 9) exited at {123.7923,444.4445}
// Touch (id: 7) ended at {333.3333,248.1884}
// Touch (id: 7) exited at {333.3333,248.1884}
// Advancing by 0.0167s
// Touch (id: 7) began at {118.9613,439.6135}
// Touch (id: 9) began at {346.6183,269.9276}
// Touch (id: 8) began at {459.5410,194.4444}
// Advancing by 0.0167s
// Touch (id: 9) ended at {346.6183,269.9276}
// Touch (id: 9) exited at {346.6183,269.9276}
// Touch (id: 7) ended at {122.5845,440.8212}
// Touch (id: 7) exited at {122.5845,440.8212}
// Touch (id: 8) ended at {459.5410,194.4444}
// Touch (id: 8) exited at {459.5410,194.4444}
// Advancing by 0.0167s
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(122.5845f, 443.8406f), 9);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(459.5410f, 188.4058f), 8);
stateMachine->pointerDown(rive::Vec2D(333.3333f, 248.1884f), 7);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerUp(rive::Vec2D(459.5410f, 188.4058f), 8);
stateMachine->pointerExit(rive::Vec2D(459.5410f, 188.4058f), 8);
stateMachine->pointerUp(rive::Vec2D(123.7923f, 444.4445f), 9);
stateMachine->pointerExit(rive::Vec2D(123.7923f, 444.4445f), 9);
stateMachine->pointerUp(rive::Vec2D(333.3333f, 248.1884f), 7);
stateMachine->pointerExit(rive::Vec2D(333.3333f, 248.1884f), 7);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(118.9613f, 439.6135f), 7);
stateMachine->pointerDown(rive::Vec2D(346.6183f, 269.9276f), 9);
stateMachine->pointerDown(rive::Vec2D(459.5410f, 194.4444f), 8);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerUp(rive::Vec2D(346.6183f, 269.9276f), 9);
stateMachine->pointerExit(rive::Vec2D(346.6183f, 269.9276f), 9);
stateMachine->pointerUp(rive::Vec2D(122.5845f, 440.8212f), 7);
stateMachine->pointerExit(rive::Vec2D(122.5845f, 440.8212f), 7);
stateMachine->pointerUp(rive::Vec2D(459.5410f, 194.4444f), 8);
stateMachine->pointerExit(rive::Vec2D(459.5410f, 194.4444f), 8);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
CHECK(silver.matches("multitouch_enter"));
}
TEST_CASE("Multitouch with list and pointer exit event", "[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/multitouch_enter.riv", &silver);
auto artboard = file->artboardNamed("MainList");
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());
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(122.5845f, 443.8406f), 9);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(459.5410f, 188.4058f), 8);
stateMachine->pointerDown(rive::Vec2D(333.3333f, 248.1884f), 7);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerUp(rive::Vec2D(459.5410f, 188.4058f), 8);
stateMachine->pointerExit(rive::Vec2D(459.5410f, 188.4058f), 8);
stateMachine->pointerUp(rive::Vec2D(123.7923f, 444.4445f), 9);
stateMachine->pointerExit(rive::Vec2D(123.7923f, 444.4445f), 9);
stateMachine->pointerUp(rive::Vec2D(333.3333f, 248.1884f), 7);
stateMachine->pointerExit(rive::Vec2D(333.3333f, 248.1884f), 7);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(118.9613f, 439.6135f), 7);
stateMachine->pointerDown(rive::Vec2D(346.6183f, 269.9276f), 9);
stateMachine->pointerDown(rive::Vec2D(459.5410f, 194.4444f), 8);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerUp(rive::Vec2D(346.6183f, 269.9276f), 9);
stateMachine->pointerExit(rive::Vec2D(346.6183f, 269.9276f), 9);
stateMachine->pointerUp(rive::Vec2D(122.5845f, 440.8212f), 7);
stateMachine->pointerExit(rive::Vec2D(122.5845f, 440.8212f), 7);
stateMachine->pointerUp(rive::Vec2D(459.5410f, 194.4444f), 8);
stateMachine->pointerExit(rive::Vec2D(459.5410f, 194.4444f), 8);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(50.0f, 300.0f), 0, 7);
stateMachine->pointerMove(rive::Vec2D(250.0f, 200.0f), 0, 8);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
float xOffset = 0;
while (xOffset < 300)
{
xOffset += 20;
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(50.0f + xOffset, 300.0f), 0, 7);
stateMachine->pointerMove(rive::Vec2D(250.0f + xOffset, 200.0f), 0, 8);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
}
CHECK(silver.matches("multitouch_enter-MainList"));
}
TEST_CASE("Multitouch with multi scroll", "[silver]")
{
SerializingFactory silver;
auto file = ReadRiveFile("assets/multitouch_enter.riv", &silver);
auto artboard = file->artboardNamed("MultiScroll");
REQUIRE(artboard != nullptr);
silver.frameSize(artboard->width(), artboard->height());
auto stateMachine = artboard->stateMachineAt(0);
stateMachine->advanceAndApply(0.1f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
float yOffset = 400;
stateMachine->pointerDown(rive::Vec2D(50.0f, yOffset), 7);
stateMachine->pointerDown(rive::Vec2D(350.0f, yOffset), 8);
while (yOffset > 0)
{
yOffset -= 20;
silver.addFrame();
stateMachine->pointerMove(rive::Vec2D(50.0f, yOffset), 0, 7);
stateMachine->pointerMove(rive::Vec2D(350.0f, yOffset), 0, 8);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
}
stateMachine->pointerUp(rive::Vec2D(50.0f, yOffset), 7);
stateMachine->pointerUp(rive::Vec2D(350.0f, yOffset), 8);
CHECK(silver.matches("multitouch_enter-MultiScroll"));
}