blob: e77d44bec537bfab686fa10eec51212b7979751e [file]
#include "rive/artboard_component_list.hpp"
#include "rive/constraints/scrolling/scroll_constraint.hpp"
#include "rive/layout/layout_component_style.hpp"
#include "rive/math/transform_components.hpp"
#include "rive/shapes/rectangle.hpp"
#include "rive/text/text.hpp"
#include "rive/viewmodel/viewmodel_instance_enum.hpp"
#include "utils/no_op_factory.hpp"
#include "utils/serializing_factory.hpp"
#include "rive_file_reader.hpp"
#include "rive_testing.hpp"
#include <catch.hpp>
#include <cstdio>
TEST_CASE("ScrollConstraint vertical offset", "[layoutscroll]")
{
auto file = ReadRiveFile("assets/layout/layout_scroll_vertical.riv");
auto artboard = file->artboard();
REQUIRE(artboard->find<rive::LayoutComponent>("Content") != nullptr);
REQUIRE(artboard->find<rive::ScrollConstraint>().size() == 1);
REQUIRE(artboard->find<rive::ScrollConstraint>()[0] != nullptr);
auto scroll = artboard->find<rive::ScrollConstraint>()[0];
REQUIRE(scroll->offsetY() == 0);
artboard->advance(0.0f);
// scrollPercentY
scroll->setScrollPercentY(1.0f);
REQUIRE(scroll->scrollPercentY() == 1.0f);
REQUIRE(scroll->offsetY() == -610.0f);
REQUIRE(scroll->clampedOffsetY() == -610.0f);
REQUIRE(scroll->scrollIndex() == Approx(5.54545f));
// scrollIndex
scroll->setScrollIndex(2);
REQUIRE(scroll->offsetY() == -220.0f);
REQUIRE(scroll->clampedOffsetY() == -220.0f);
REQUIRE(scroll->scrollIndex() == 2);
REQUIRE(scroll->contentHeight() == 1090.0f);
REQUIRE(scroll->viewportHeight() == 490.0f);
REQUIRE(scroll->maxOffsetY() == -610.0f);
REQUIRE(scroll->clampedOffsetY() == -220.0f);
}
TEST_CASE("ScrollConstraint vertical offset manual", "[layoutscroll]")
{
auto file = ReadRiveFile("assets/layout/layout_scroll_vertical.riv");
auto artboard = file->artboard();
REQUIRE(artboard->find<rive::LayoutComponent>("Content") != nullptr);
auto artboardInstance = artboard->instance();
auto stateMachine = artboard->stateMachine("State Machine 1");
REQUIRE(artboardInstance != nullptr);
REQUIRE(artboardInstance->stateMachineCount() == 1);
REQUIRE(stateMachine != nullptr);
rive::StateMachineInstance* stateMachineInstance =
new rive::StateMachineInstance(stateMachine, artboardInstance.get());
REQUIRE(artboardInstance->find<rive::ScrollConstraint>().size() == 1);
REQUIRE(artboardInstance->find<rive::ScrollConstraint>()[0] != nullptr);
auto scroll = artboardInstance->find<rive::ScrollConstraint>()[0];
REQUIRE(scroll->scrollPercentY() == 0.0f);
REQUIRE(scroll->offsetY() == 0.0f);
REQUIRE(scroll->scrollIndex() == Approx(0.0f));
REQUIRE(scroll->physics()->isRunning() == false);
artboardInstance->advance(0.0f);
stateMachineInstance->pointerMove(rive::Vec2D(50.0f, 250.0f));
// Start drag
stateMachineInstance->pointerDown(rive::Vec2D(50.0f, 250.0f));
artboardInstance->advance(0.1f);
stateMachineInstance->advanceAndApply(0.1f);
// Move up 200px in 0.1 seconds
stateMachineInstance->pointerMove(rive::Vec2D(50.0f, 50.0f));
artboardInstance->advance(0.0f);
stateMachineInstance->advanceAndApply(0.0f);
REQUIRE(scroll->scrollPercentY() == Approx(0.32787f));
REQUIRE(scroll->offsetY() == -200.0f);
REQUIRE(scroll->scrollIndex() == Approx(1.818182f));
// End drag
stateMachineInstance->pointerUp(rive::Vec2D(50.0f, 50.0f));
REQUIRE(scroll->physics()->isRunning() == true);
delete stateMachineInstance;
}
TEST_CASE("ScrollConstraint horizontal offset", "[layoutscroll]")
{
auto file = ReadRiveFile("assets/layout/layout_scroll_horizontal.riv");
auto artboard = file->artboard();
REQUIRE(artboard->find<rive::LayoutComponent>("Content") != nullptr);
REQUIRE(artboard->find<rive::ScrollConstraint>().size() == 1);
REQUIRE(artboard->find<rive::ScrollConstraint>()[0] != nullptr);
auto scroll = artboard->find<rive::ScrollConstraint>()[0];
REQUIRE(scroll->offsetX() == 0);
artboard->advance(0.0f);
// scrollPercentX
scroll->setScrollPercentX(1.0f);
REQUIRE(scroll->scrollPercentX() == 1.0f);
REQUIRE(scroll->offsetX() == -610.0f);
REQUIRE(scroll->clampedOffsetX() == -610.0f);
REQUIRE(scroll->scrollIndex() == Approx(5.54545f));
// scrollIndex
scroll->setScrollIndex(2);
REQUIRE(scroll->offsetX() == -220.0f);
REQUIRE(scroll->clampedOffsetX() == -220.0f);
REQUIRE(scroll->scrollIndex() == 2);
REQUIRE(scroll->contentWidth() == 1090.0f);
REQUIRE(scroll->viewportWidth() == 490.0f);
REQUIRE(scroll->maxOffsetX() == -610.0f);
REQUIRE(scroll->clampedOffsetX() == -220.0f);
}
TEST_CASE("ScrollConstraint list", "[layoutscroll]")
{
auto file = ReadRiveFile("assets/layout/layout_scroll_list.riv");
auto artboard = file->artboard("Main")->instance();
REQUIRE(artboard != nullptr);
auto viewModelInstance =
file->createDefaultViewModelInstance(artboard.get());
REQUIRE(viewModelInstance != nullptr);
artboard->bindViewModelInstance(viewModelInstance);
REQUIRE(artboard->find<rive::LayoutComponent>("Content") != nullptr);
REQUIRE(artboard->find<rive::ArtboardComponentList>("List") != nullptr);
REQUIRE(artboard->find<rive::ScrollConstraint>().size() == 1);
REQUIRE(artboard->find<rive::ScrollConstraint>()[0] != nullptr);
auto scroll = artboard->find<rive::ScrollConstraint>()[0];
auto list = artboard->find<rive::ArtboardComponentList>("List");
REQUIRE(scroll->offsetY() == 0);
artboard->advance(0.0f);
REQUIRE(list->numLayoutNodes() == 20);
for (int i = 0; i < list->numLayoutNodes(); i++)
{
auto bounds = list->layoutBoundsForNode(i);
REQUIRE(bounds.top() == i * 48.0f);
}
// scrollIndex
scroll->setScrollIndex(2);
REQUIRE(scroll->scrollItemCount() == 20);
REQUIRE(scroll->offsetY() == -96.0f);
REQUIRE(scroll->clampedOffsetY() == -96.0f);
REQUIRE(scroll->scrollIndex() == 2);
REQUIRE(scroll->contentHeight() == 960.0f);
REQUIRE(scroll->viewportHeight() == 500.0f);
REQUIRE(scroll->maxOffsetY() == -460.0f);
}
TEST_CASE("Carousel snap swipe right settles past index 0", "[silver]")
{
rive::File::deterministicMode = true;
rive::SerializingFactory silver;
auto file = ReadRiveFile("assets/layout/layout_scroll_snap.riv", &silver);
auto artboard = file->artboard("main")->instance();
REQUIRE(artboard != nullptr);
silver.frameSize(artboard->width(), artboard->height());
auto viewModelInstance =
file->createDefaultViewModelInstance(artboard.get());
REQUIRE(viewModelInstance != nullptr);
artboard->bindViewModelInstance(viewModelInstance);
auto smi = artboard->defaultStateMachine();
REQUIRE(smi != nullptr);
auto renderer = silver.makeRenderer();
smi->advanceAndApply(0.0f);
artboard->draw(renderer.get());
float centerY = artboard->height() / 2.0f;
float startX = artboard->width() / 2.0f;
float dt = 0.016f;
// Swipe right: use whole-second timestamps so they survive long long
// truncation in the physics accumulator.
float swipeDistance = 1500.0f;
float time = 1.0f;
smi->pointerMove(rive::Vec2D(startX, centerY), time);
smi->pointerDown(rive::Vec2D(startX, centerY));
time += 1.0f;
int steps = 4;
for (int i = 1; i <= steps; i++)
{
silver.addFrame();
float x = startX + (swipeDistance * i / steps);
smi->pointerMove(rive::Vec2D(x, centerY), time);
time += 1.0f;
smi->advanceAndApply(dt);
artboard->draw(renderer.get());
}
smi->pointerUp(rive::Vec2D(startX + swipeDistance, centerY));
// Capture frames while physics settles.
for (int i = 0; i < 300; i++)
{
silver.addFrame();
smi->advanceAndApply(dt);
artboard->draw(renderer.get());
if (!smi->artboard()
->find<rive::ScrollConstraint>()[0]
->physics()
->isRunning())
{
break;
}
}
CHECK(silver.matches("layout_scroll_snap_carousel"));
rive::File::deterministicMode = false;
}
TEST_CASE("ScrollConstraint nearestSnapOffsetInDirection", "[layoutscroll]")
{
auto file = ReadRiveFile("assets/layout/layout_scroll_vertical.riv");
auto artboard = file->artboard()->instance();
artboard->advance(0.0f);
REQUIRE(artboard->find<rive::ScrollConstraint>().size() == 1);
REQUIRE(artboard->find<rive::ScrollConstraint>()[0] != nullptr);
auto scroll = artboard->find<rive::ScrollConstraint>()[0];
// Confirm fixture: index→offset step is 110, so snap offsets are
// 0, -110, -220, -330, -440, -550.
scroll->setScrollIndex(2);
REQUIRE(scroll->offsetY() == -220.0f);
// Snap disabled: target returned unchanged on both axes.
REQUIRE(scroll->snap() == false);
rive::Vec2D passthrough =
scroll->nearestSnapOffsetInDirection(rive::Vec2D(0.0f, 0.0f),
rive::Vec2D(42.0f, -150.0f));
REQUIRE(passthrough.x == 42.0f);
REQUIRE(passthrough.y == -150.0f);
scroll->snap(true);
// Scrolling forward (target more negative): pick closest snap ≤ target.
// target=-150 → candidates {-220,-330,-440,-550} → nearest is -220.
rive::Vec2D forward =
scroll->nearestSnapOffsetInDirection(rive::Vec2D(0.0f, 0.0f),
rive::Vec2D(0.0f, -150.0f));
REQUIRE(forward.y == -220.0f);
// Scrolling back (target less negative): pick closest snap ≥ target.
// target=-150 → candidates {0,-110} → nearest is -110.
rive::Vec2D backward =
scroll->nearestSnapOffsetInDirection(rive::Vec2D(0.0f, -500.0f),
rive::Vec2D(0.0f, -150.0f));
REQUIRE(backward.y == -110.0f);
// current == target: returned unchanged (no direction to search).
rive::Vec2D noop =
scroll->nearestSnapOffsetInDirection(rive::Vec2D(0.0f, -330.0f),
rive::Vec2D(0.0f, -330.0f));
REQUIRE(noop.y == -330.0f);
// Target already on a snap: stays on the same snap.
rive::Vec2D onSnap =
scroll->nearestSnapOffsetInDirection(rive::Vec2D(0.0f, 0.0f),
rive::Vec2D(0.0f, -220.0f));
REQUIRE(onSnap.y == -220.0f);
}
TEST_CASE("ScrollConstraint scrollIndex with hidden items", "[silver]")
{
rive::File::deterministicMode = true;
rive::SerializingFactory silver;
auto file =
ReadRiveFile("assets/layout/layout_scroll_visibility.riv", &silver);
auto artboard = file->artboard()->instance();
REQUIRE(artboard != nullptr);
silver.frameSize(artboard->width(), artboard->height());
auto vmi = file->createDefaultViewModelInstance(artboard.get());
REQUIRE(vmi != nullptr);
artboard->bindViewModelInstance(vmi);
auto smi = artboard->defaultStateMachine();
REQUIRE(smi != nullptr);
auto vis2 = vmi->propertyValue("vis2")->as<rive::ViewModelInstanceEnum>();
auto vis3 = vmi->propertyValue("vis3")->as<rive::ViewModelInstanceEnum>();
auto vis4 = vmi->propertyValue("vis4")->as<rive::ViewModelInstanceEnum>();
auto renderer = silver.makeRenderer();
float dt = 1.0f / 60.0f;
smi->advanceAndApply(0.0f);
artboard->draw(renderer.get());
// Run 300 frames, toggling visibility at key moments.
for (int frame = 0; frame < 300; frame++)
{
// Hide item 2 at frame 30.
if (frame == 30)
{
vis2->value(1);
}
// Hide item 3 at frame 90.
if (frame == 90)
{
vis3->value(1);
}
// Show item 2 again at frame 150.
if (frame == 150)
{
vis2->value(0);
}
// Hide item 4 at frame 210.
if (frame == 210)
{
vis4->value(1);
}
// Show all at frame 270.
if (frame == 270)
{
vis2->value(0);
vis3->value(0);
vis4->value(0);
}
silver.addFrame();
smi->advanceAndApply(dt);
artboard->draw(renderer.get());
}
CHECK(silver.matches("layout_scroll_visibility"));
rive::File::deterministicMode = false;
}