blob: 978cc508c7793fdf9df6442c728dde1141ec6d63 [file]
#include <rive/animation/listener_types/listener_input_type.hpp>
#include <rive/animation/listener_types/listener_input_type_pointer_button.hpp>
#include <rive/animation/state_machine_instance.hpp>
#include <rive/animation/state_machine_listener.hpp>
#include <rive/animation/linear_animation_instance.hpp>
#include <rive/animation/nested_state_machine.hpp>
#include <rive/animation/state_machine_input_instance.hpp>
#include <rive/constraints/scrolling/scroll_constraint.hpp>
#include <rive/nested_artboard.hpp>
#include <rive/pointer_button.hpp>
#include <rive/text/text_input.hpp>
#include <rive/viewmodel/viewmodel_instance_number.hpp>
#include "rive_file_reader.hpp"
#include <catch.hpp>
using namespace rive;
namespace
{
// click_event's "art-1" has two 200x200 rects under one group with a single
// legacy (StateMachineListenerSingle) click listener that reports an event.
struct ClickScene
{
rcp<File> file;
std::unique_ptr<ArtboardInstance> artboard;
std::unique_ptr<StateMachineInstance> stateMachine;
ClickScene()
{
file = ReadRiveFile("assets/click_event.riv");
auto sourceArtboard = file->artboard("art-1");
auto machine = sourceArtboard->stateMachine("sm-1");
REQUIRE(machine != nullptr);
artboard = sourceArtboard->instance();
stateMachine =
std::make_unique<StateMachineInstance>(machine, artboard.get());
stateMachine->advance(0.0f);
artboard->advance(0.0f);
stateMachine->advance(0.0f);
REQUIRE(stateMachine->hitComponentsCount() == 2);
REQUIRE(stateMachine->reportedEventCount() == 0);
}
void down(PointerButton button, Vec2D position = Vec2D(75.0f, 75.0f))
{
stateMachine->pointerDown(position, 0, button);
}
void up(PointerButton button, Vec2D position = Vec2D(75.0f, 75.0f))
{
stateMachine->pointerUp(position, 0, button);
}
size_t events() const { return stateMachine->reportedEventCount(); }
};
// listener_input_values (see assets/rml/listener_input_values.rml) has a
// full-artboard shape whose first listener is a ListenerInputType "down" that
// writes the pointer position into the px/py view model numbers. Passing a
// button swaps that input for a ListenerInputTypePointerButton bound to it.
struct DownScene
{
rcp<File> file;
std::unique_ptr<ArtboardInstance> artboard;
std::unique_ptr<StateMachineInstance> stateMachine;
rcp<ViewModelInstance> vmi;
using Input = std::pair<ListenerType, PointerButton>;
DownScene(std::vector<Input> inputs = {})
{
file = ReadRiveFile("assets/listener_input_values.riv");
auto machine = file->artboard()->stateMachine(0);
REQUIRE(machine != nullptr);
REQUIRE(machine->listenerCount() > 0);
auto listener = machine->listener(0);
REQUIRE(listener->listenerInputTypeCount() == 1);
auto inputType = listener->listenerInputType(0);
REQUIRE(inputType->listenerTypeValue() == (uint32_t)ListenerType::down);
REQUIRE(inputType->pointerButton() == PointerButton::primary);
for (size_t i = 0; i < inputs.size(); i++)
{
auto buttonInput =
std::make_unique<ListenerInputTypePointerButton>();
buttonInput->listenerTypeValue((uint32_t)inputs[i].first);
buttonInput->pointerButtonValue((uint8_t)inputs[i].second);
auto mutableListener = const_cast<StateMachineListener*>(listener);
if (i == 0)
{
mutableListener->replaceListenerInputTypeForTesting(
0,
std::move(buttonInput));
}
else
{
mutableListener->addListenerInputTypeForTesting(
std::move(buttonInput));
}
REQUIRE(listener->listenerInputType(i)->pointerButton() ==
inputs[i].second);
}
artboard = file->artboardDefault();
stateMachine = artboard->stateMachineAt(0);
vmi = file->createDefaultViewModelInstance(artboard.get());
REQUIRE(vmi != nullptr);
stateMachine->bindViewModelInstance(vmi);
advance();
}
void advance() { stateMachine->advanceAndApply(0.016f); }
void down(PointerButton button, Vec2D position)
{
stateMachine->pointerDown(position, 0, button);
advance();
}
void move(Vec2D position)
{
stateMachine->pointerMove(position);
advance();
}
void up(PointerButton button, Vec2D position)
{
stateMachine->pointerUp(position, 0, button);
advance();
}
float px()
{
auto value = vmi->propertyValue("px");
REQUIRE(value != nullptr);
return value->as<ViewModelInstanceNumber>()->propertyValue();
}
};
} // namespace
TEST_CASE("plain listener input types are primary button listeners",
"[pointer_button]")
{
auto file = ReadRiveFile("assets/listener_input_values.riv");
auto machine = file->artboard()->stateMachine(0);
auto listener = machine->listener(0);
REQUIRE(listener->listenerInputTypeCount() == 1);
REQUIRE(listener->listenerInputType(0)->pointerButton() ==
PointerButton::primary);
REQUIRE(listener->hasListener(ListenerType::down));
REQUIRE(listener->hasListener(ListenerType::down, PointerButton::primary));
REQUIRE_FALSE(
listener->hasListener(ListenerType::down, PointerButton::secondary));
REQUIRE_FALSE(
listener->hasListener(ListenerType::down, PointerButton::middle));
}
TEST_CASE("pointer button input types listen to their button only",
"[pointer_button]")
{
ListenerInputTypePointerButton input;
input.listenerTypeValue((uint32_t)ListenerType::click);
REQUIRE(input.is<ListenerInputType>());
REQUIRE(input.pointerButton() == PointerButton::primary);
input.pointerButtonValue((uint8_t)PointerButton::secondary);
REQUIRE(input.pointerButton() == PointerButton::secondary);
input.pointerButtonValue((uint8_t)PointerButton::middle);
REQUIRE(input.pointerButton() == PointerButton::middle);
}
TEST_CASE("legacy single listeners only respond to the primary button",
"[pointer_button]")
{
auto file = ReadRiveFile("assets/click_event.riv");
auto machine = file->artboard("art-1")->stateMachine("sm-1");
auto listener = machine->listener(0);
REQUIRE(listener->listenerInputTypeCount() == 0);
REQUIRE(listener->hasListener(ListenerType::click));
REQUIRE(listener->hasListener(ListenerType::click, PointerButton::primary));
REQUIRE_FALSE(
listener->hasListener(ListenerType::click, PointerButton::secondary));
}
TEST_CASE("primary down listener ignores other buttons", "[pointer_button]")
{
DownScene scene;
REQUIRE(scene.px() == 0.0f);
scene.down(PointerButton::secondary, Vec2D(120.0f, 310.0f));
CHECK(scene.px() == 0.0f);
scene.down(PointerButton::middle, Vec2D(120.0f, 310.0f));
CHECK(scene.px() == 0.0f);
scene.down(PointerButton::primary, Vec2D(120.0f, 310.0f));
CHECK(scene.px() == 120.0f);
}
TEST_CASE("secondary down listener fires only on the secondary button",
"[pointer_button]")
{
DownScene scene({{ListenerType::down, PointerButton::secondary}});
REQUIRE(scene.px() == 0.0f);
scene.down(PointerButton::primary, Vec2D(120.0f, 310.0f));
CHECK(scene.px() == 0.0f);
scene.down(PointerButton::middle, Vec2D(120.0f, 310.0f));
CHECK(scene.px() == 0.0f);
scene.down(PointerButton::secondary, Vec2D(120.0f, 310.0f));
CHECK(scene.px() == 120.0f);
scene.down(PointerButton::secondary, Vec2D(200.0f, 310.0f));
CHECK(scene.px() == 200.0f);
}
TEST_CASE("listensToButtonAt reports the buttons wanted under a point",
"[pointer_button]")
{
Vec2D inside(120.0f, 310.0f);
Vec2D outside(-50.0f, -50.0f);
DownScene primary;
CHECK(primary.stateMachine->listensToButtonAt(inside,
PointerButton::primary));
CHECK_FALSE(
primary.stateMachine->listensToButtonAt(inside,
PointerButton::secondary));
CHECK_FALSE(
primary.stateMachine->listensToButtonAt(outside,
PointerButton::primary));
DownScene scene({{ListenerType::down, PointerButton::secondary}});
CHECK(scene.stateMachine->listensToButtonAt(inside,
PointerButton::secondary));
CHECK_FALSE(
scene.stateMachine->listensToButtonAt(inside, PointerButton::primary));
CHECK_FALSE(
scene.stateMachine->listensToButtonAt(inside, PointerButton::middle));
CHECK_FALSE(
scene.stateMachine->listensToButtonAt(outside,
PointerButton::secondary));
ClickScene legacy;
CHECK(legacy.stateMachine->listensToButtonAt(Vec2D(75.0f, 75.0f),
PointerButton::primary));
CHECK_FALSE(
legacy.stateMachine->listensToButtonAt(Vec2D(75.0f, 75.0f),
PointerButton::secondary));
CHECK_FALSE(legacy.stateMachine->listensToButtonAt(Vec2D(300.0f, 75.0f),
PointerButton::primary));
}
TEST_CASE("synthetic drag events carry the dragging button", "[pointer_button]")
{
Vec2D start(100.0f, 100.0f);
Vec2D dragged(150.0f, 150.0f);
Vec2D released(200.0f, 200.0f);
SECTION("a secondary drag end reaches a secondary drag end listener")
{
DownScene scene({{ListenerType::drag, PointerButton::secondary},
{ListenerType::dragEnd, PointerButton::secondary}});
scene.down(PointerButton::secondary, start);
CHECK(scene.px() == 0.0f);
scene.move(dragged);
CHECK(scene.px() == 150.0f);
scene.up(PointerButton::secondary, released);
CHECK(scene.px() == 200.0f);
}
SECTION("a secondary drag end skips a primary drag end listener")
{
DownScene scene({{ListenerType::drag, PointerButton::secondary},
{ListenerType::dragEnd, PointerButton::primary}});
scene.down(PointerButton::secondary, start);
scene.move(dragged);
CHECK(scene.px() == 150.0f);
scene.up(PointerButton::secondary, released);
CHECK(scene.px() == 150.0f);
}
SECTION("a secondary drag start reaches a secondary drag start listener")
{
DownScene scene({{ListenerType::drag, PointerButton::secondary},
{ListenerType::dragStart, PointerButton::secondary}});
scene.down(PointerButton::secondary, start);
scene.move(dragged);
CHECK(scene.px() == 150.0f);
scene.up(PointerButton::secondary, released);
CHECK(scene.px() == 150.0f);
}
SECTION("a primary drag still drives primary drag listeners")
{
DownScene scene({{ListenerType::drag, PointerButton::primary},
{ListenerType::dragEnd, PointerButton::primary}});
scene.down(PointerButton::primary, start);
scene.move(dragged);
CHECK(scene.px() == 150.0f);
scene.up(PointerButton::primary, released);
CHECK(scene.px() == 200.0f);
}
SECTION("a primary drag ignores secondary drag listeners")
{
DownScene scene({{ListenerType::drag, PointerButton::secondary},
{ListenerType::dragEnd, PointerButton::secondary}});
scene.down(PointerButton::primary, start);
scene.move(dragged);
scene.up(PointerButton::primary, released);
CHECK(scene.px() == 0.0f);
}
}
TEST_CASE("primary click listener ignores other buttons", "[pointer_button]")
{
ClickScene scene;
scene.down(PointerButton::primary);
scene.up(PointerButton::primary);
REQUIRE(scene.events() == 1);
scene.down(PointerButton::secondary);
scene.up(PointerButton::secondary);
REQUIRE(scene.events() == 1);
scene.down(PointerButton::middle);
scene.up(PointerButton::middle);
REQUIRE(scene.events() == 1);
scene.down(PointerButton::primary);
scene.up(PointerButton::primary);
REQUIRE(scene.events() == 2);
}
TEST_CASE("a click needs the same button down and up", "[pointer_button]")
{
ClickScene scene;
scene.down(PointerButton::primary);
scene.up(PointerButton::secondary);
REQUIRE(scene.events() == 0);
scene.up(PointerButton::primary);
REQUIRE(scene.events() == 1);
scene.down(PointerButton::secondary);
scene.up(PointerButton::primary);
REQUIRE(scene.events() == 1);
}
TEST_CASE("a held press survives another button pressed over it",
"[pointer_button]")
{
ClickScene scene;
scene.down(PointerButton::primary);
scene.down(PointerButton::secondary);
scene.up(PointerButton::secondary);
REQUIRE(scene.events() == 0);
scene.up(PointerButton::primary);
REQUIRE(scene.events() == 1);
}
TEST_CASE("releasing another button outside keeps the held press",
"[pointer_button]")
{
ClickScene scene;
scene.down(PointerButton::primary);
scene.up(PointerButton::secondary, Vec2D(300.0f, 75.0f));
scene.up(PointerButton::primary);
REQUIRE(scene.events() == 1);
scene.down(PointerButton::primary);
scene.up(PointerButton::primary, Vec2D(300.0f, 75.0f));
scene.up(PointerButton::primary);
REQUIRE(scene.events() == 1);
}
// pointer_button_secondary.riv is exported by rive_core's
// pointer_button_export_test.dart: a full-artboard shape with a secondary click
// listener that reports "Boom" and sets the "clicked" bool input.
TEST_CASE("an exported secondary click fixture loads as a pointer button input",
"[pointer_button]")
{
auto file = ReadRiveFile("assets/pointer_button_secondary.riv");
auto machine = file->artboard()->stateMachine(0);
REQUIRE(machine != nullptr);
REQUIRE(machine->listenerCount() == 1);
auto listener = machine->listener(0);
REQUIRE(listener->listenerInputTypeCount() == 1);
auto input = listener->listenerInputType(0);
REQUIRE(input->is<ListenerInputTypePointerButton>());
REQUIRE(input->pointerButton() == PointerButton::secondary);
REQUIRE(input->listenerTypeValue() == (uint32_t)ListenerType::click);
auto artboard = file->artboardDefault();
auto stateMachine = artboard->stateMachineAt(0);
stateMachine->advanceAndApply(0.0f);
auto clicked = stateMachine->getBool("clicked");
REQUIRE(clicked != nullptr);
REQUIRE(clicked->value() == false);
Vec2D center(250.0f, 250.0f);
stateMachine->pointerDown(center, 0, PointerButton::primary);
stateMachine->pointerUp(center, 0, PointerButton::primary);
CHECK(stateMachine->reportedEventCount() == 0);
CHECK(clicked->value() == false);
stateMachine->pointerDown(center, 0, PointerButton::secondary);
stateMachine->pointerUp(center, 0, PointerButton::secondary);
CHECK(stateMachine->reportedEventCount() == 1);
CHECK(clicked->value() == true);
}
// pointer_button_nested.riv is exported by the same Dart test: a host artboard
// holding "Inner" at (100, 100), whose full shape has a secondary click
// listener ("Boom") and a secondary drag + dragEnd listener ("Drag").
TEST_CASE("nested pointer button listeners dispatch through the host",
"[pointer_button]")
{
auto file = ReadRiveFile("assets/pointer_button_nested.riv");
auto artboard = file->artboardDefault();
auto stateMachine = artboard->stateMachineAt(0);
stateMachine->advanceAndApply(0.0f);
auto nestedArtboards = artboard->find<NestedArtboard>();
REQUIRE(nestedArtboards.size() == 1);
auto nestedArtboard = nestedArtboards[0];
REQUIRE(nestedArtboard->nestedAnimations().size() == 1);
auto inner = nestedArtboard->nestedAnimations()[0]
->as<NestedStateMachine>()
->stateMachineInstance();
REQUIRE(inner != nullptr);
Vec2D overInner(200.0f, 200.0f);
Vec2D besideInner(20.0f, 20.0f);
CHECK(stateMachine->listensToButtonAt(overInner, PointerButton::secondary));
CHECK_FALSE(
stateMachine->listensToButtonAt(overInner, PointerButton::primary));
CHECK_FALSE(
stateMachine->listensToButtonAt(besideInner, PointerButton::secondary));
stateMachine->pointerDown(overInner, 0, PointerButton::primary);
stateMachine->pointerUp(overInner, 0, PointerButton::primary);
CHECK(inner->reportedEventCount() == 0);
stateMachine->pointerDown(overInner, 0, PointerButton::secondary);
stateMachine->pointerUp(overInner, 0, PointerButton::secondary);
CHECK(inner->reportedEventCount() == 1);
stateMachine->advanceAndApply(0.016f);
SECTION("a secondary drag reaches the nested drag listeners")
{
stateMachine->pointerDown(Vec2D(150.0f, 150.0f),
0,
PointerButton::secondary);
stateMachine->pointerMove(Vec2D(170.0f, 170.0f));
auto afterMove = inner->reportedEventCount();
CHECK(afterMove >= 1);
stateMachine->pointerUp(Vec2D(180.0f, 180.0f),
0,
PointerButton::secondary);
CHECK(inner->reportedEventCount() > afterMove);
}
SECTION("a primary drag leaves the nested secondary drag listeners alone")
{
stateMachine->pointerDown(Vec2D(150.0f, 150.0f),
0,
PointerButton::primary);
stateMachine->pointerMove(Vec2D(170.0f, 170.0f));
stateMachine->pointerUp(Vec2D(180.0f, 180.0f),
0,
PointerButton::primary);
CHECK(inner->reportedEventCount() == 0);
}
}
TEST_CASE("scroll views only drag with the primary button", "[pointer_button]")
{
auto file = ReadRiveFile("assets/layout/layout_scroll_vertical.riv");
auto artboard = file->artboard();
auto artboardInstance = artboard->instance();
auto machine = artboard->stateMachine("State Machine 1");
REQUIRE(machine != nullptr);
StateMachineInstance smi(machine, artboardInstance.get());
auto scroll = artboardInstance->find<ScrollConstraint>()[0];
smi.advanceAndApply(0.0f);
Vec2D over(artboardInstance->width() / 2.0f,
artboardInstance->height() / 2.0f);
Vec2D dragged(over.x, over.y - 50.0f);
smi.pointerDown(over, 0, PointerButton::secondary);
smi.pointerMove(dragged);
smi.pointerUp(dragged, 0, PointerButton::secondary);
smi.advanceAndApply(0.0f);
CHECK(scroll->offsetY() == 0.0f);
smi.pointerDown(over, 0, PointerButton::primary);
smi.pointerMove(dragged);
smi.advanceAndApply(0.0f);
CHECK(scroll->offsetY() != 0.0f);
smi.pointerUp(dragged, 0, PointerButton::primary);
}
TEST_CASE("text inputs only select with the primary button", "[pointer_button]")
{
auto file = ReadRiveFile("assets/text_input.riv");
auto artboard = file->artboardNamed("Text Input - Multiline");
REQUIRE(artboard != nullptr);
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine != nullptr);
stateMachine->advanceAndApply(0.0f);
auto textInput = artboard->objects<TextInput>().first();
REQUIRE(textInput != nullptr);
textInput->rawTextInput()->text("hello world");
stateMachine->advanceAndApply(0.0f);
Vec2D press(8.0f, 8.0f);
stateMachine->pointerDown(press, 0, PointerButton::secondary);
CHECK(textInput->isDragging() == false);
stateMachine->pointerUp(press, 0, PointerButton::secondary);
stateMachine->pointerDown(press, 0, PointerButton::primary);
CHECK(textInput->isDragging() == true);
stateMachine->pointerUp(press, 0, PointerButton::primary);
}
TEST_CASE("scenes without listeners ignore pointer buttons", "[pointer_button]")
{
auto file = ReadRiveFile("assets/listener_input_values.riv");
auto artboard = file->artboardDefault();
REQUIRE(artboard->animationCount() > 0);
auto animation = artboard->animationAt(0);
Scene* scene = animation.get();
CHECK(scene->pointerDown(Vec2D(1.0f, 1.0f), 0, PointerButton::secondary) ==
HitResult::none);
CHECK(scene->pointerUp(Vec2D(1.0f, 1.0f), 0, PointerButton::secondary) ==
HitResult::none);
}