blob: bc5b6df393287b37cad024635bffdcdacad07e69 [file]
#include <rive/solo.hpp>
#include <rive/shapes/shape.hpp>
#include <rive/shapes/path.hpp>
#include <rive/shapes/clipping_shape.hpp>
#include <rive/constraints/translation_constraint.hpp>
#include <rive/focus_data.hpp>
#include <rive/semantic/semantic_data.hpp>
#include <rive/animation/state_machine_instance.hpp>
#include <rive/animation/state_machine_input_instance.hpp>
#include <rive/nested_artboard.hpp>
#include <rive/shapes/paint/fill.hpp>
#include <rive/shapes/paint/solid_color.hpp>
#include "rive_file_reader.hpp"
#include <rive/viewmodel/viewmodel_instance_number.hpp>
#include "rive/viewmodel/viewmodel_instance_enum.hpp"
#include "utils/no_op_factory.hpp"
#include "utils/serializing_factory.hpp"
#include <catch.hpp>
#include <cstdint>
#include <cstdio>
TEST_CASE("file with skins in solos loads correctly", "[solo]")
{
auto file = ReadRiveFile("assets/death_knight.riv");
auto artboard = file->artboard()->instance();
artboard->advance(0.0f);
auto solos = artboard->find<rive::Solo>();
REQUIRE(solos.size() == 2);
}
TEST_CASE("children load correctly", "[solo]")
{
auto file = ReadRiveFile("assets/solo_test.riv");
auto artboard = file->artboard()->instance();
artboard->advance(0.0f);
auto solos = artboard->find<rive::Solo>();
REQUIRE(solos.size() == 1);
auto solo = solos[0];
REQUIRE(solo != nullptr);
REQUIRE(solo->children().size() == 3);
REQUIRE(solo->children()[0]->is<rive::Shape>());
REQUIRE(solo->children()[0]->name() == "Blue");
REQUIRE(solo->children()[1]->is<rive::Shape>());
REQUIRE(solo->children()[1]->name() == "Green");
REQUIRE(solo->children()[2]->is<rive::Shape>());
REQUIRE(solo->children()[2]->name() == "Red");
auto blue = solo->children()[0]->as<rive::Shape>();
auto green = solo->children()[1]->as<rive::Shape>();
auto red = solo->children()[2]->as<rive::Shape>();
REQUIRE(!blue->isHidden());
REQUIRE(green->isHidden());
REQUIRE(red->isHidden());
REQUIRE(green->children().size() == 2);
REQUIRE(green->children()[0]->isCollapsed());
REQUIRE(green->children()[1]->isCollapsed());
REQUIRE(red->children().size() == 2);
REQUIRE(red->children()[0]->isCollapsed());
REQUIRE(red->children()[1]->isCollapsed());
auto machine = artboard->defaultStateMachine();
machine->advanceAndApply(0.0);
// Red visible at start
REQUIRE(blue->isHidden());
REQUIRE(green->isHidden());
REQUIRE(!red->isHidden());
machine->advanceAndApply(0.5);
// Green visible after 0.5 seconds.
REQUIRE(blue->isHidden());
REQUIRE(!green->isHidden());
REQUIRE(red->isHidden());
machine->advanceAndApply(0.5);
// Blue visible at end
REQUIRE(!blue->isHidden());
REQUIRE(green->isHidden());
REQUIRE(red->isHidden());
}
TEST_CASE("nested solos work", "[solo]")
{
auto file = ReadRiveFile("assets/nested_solo.riv");
auto artboard = file->artboard()->instance();
artboard->advance(0.0f);
auto s1 = artboard->find<rive::Solo>("Solo 1");
REQUIRE(s1 != nullptr);
auto s2 = artboard->find<rive::Solo>("Solo 2");
REQUIRE(s2 != nullptr);
auto s3 = artboard->find<rive::Solo>("Solo 3");
REQUIRE(s3 != nullptr);
auto a = artboard->find<rive::Shape>("A");
REQUIRE(a != nullptr);
auto b = artboard->find<rive::Shape>("B");
REQUIRE(b != nullptr);
auto c = artboard->find<rive::Shape>("C");
REQUIRE(c != nullptr);
auto d = artboard->find<rive::Shape>("D");
REQUIRE(d != nullptr);
auto e = artboard->find<rive::Shape>("E");
REQUIRE(e != nullptr);
auto f = artboard->find<rive::Shape>("F");
REQUIRE(f != nullptr);
auto g = artboard->find<rive::Shape>("G");
REQUIRE(g != nullptr);
auto h = artboard->find<rive::Shape>("H");
REQUIRE(h != nullptr);
auto i = artboard->find<rive::Shape>("I");
REQUIRE(i != nullptr);
s1->activeComponentId(artboard->idOf(a));
s2->activeComponentId(artboard->idOf(d));
s3->activeComponentId(artboard->idOf(h));
artboard->advance(0.0f);
REQUIRE(a->isCollapsed() == false);
REQUIRE(b->isCollapsed() == true);
REQUIRE(c->isCollapsed() == true);
REQUIRE(d->isCollapsed() == true);
REQUIRE(e->isCollapsed() == true);
REQUIRE(f->isCollapsed() == true);
REQUIRE(g->isCollapsed() == true);
REQUIRE(h->isCollapsed() == true);
REQUIRE(i->isCollapsed() == true);
// Changing active in a collapsed solo doesn't affect anything.
s3->activeComponentId(artboard->idOf(g));
artboard->advance(0.0f);
REQUIRE(a->isCollapsed() == false);
REQUIRE(b->isCollapsed() == true);
REQUIRE(c->isCollapsed() == true);
REQUIRE(d->isCollapsed() == true);
REQUIRE(e->isCollapsed() == true);
REQUIRE(f->isCollapsed() == true);
REQUIRE(g->isCollapsed() == true);
REQUIRE(h->isCollapsed() == true);
REQUIRE(i->isCollapsed() == true);
s1->activeComponentId(artboard->idOf(c));
artboard->advance(0.0);
// Now the rest of the nested solo items should be visible.
REQUIRE(a->isCollapsed() == true);
REQUIRE(b->isCollapsed() == true);
REQUIRE(c->isCollapsed() == false);
REQUIRE(d->isCollapsed() == false);
REQUIRE(e->isCollapsed() == true);
REQUIRE(f->isCollapsed() == true);
REQUIRE(g->isCollapsed() == false);
REQUIRE(h->isCollapsed() == true);
REQUIRE(i->isCollapsed() == true);
}
TEST_CASE("hit test on solos", "[solo]")
{
auto file = ReadRiveFile("assets/hit_test_solos.riv");
auto artboard = file->artboard()->instance();
REQUIRE(artboard != nullptr);
REQUIRE(artboard->stateMachineCount() == 1);
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine != nullptr);
stateMachine->advance(0.0f);
artboard->advance(0.0f);
auto toggle = stateMachine->getBool("hovered");
REQUIRE(toggle != nullptr);
// Inactive shape position
stateMachine->pointerMove(rive::Vec2D(200.0f, 100.0f));
REQUIRE(toggle->value() == true);
// // Active shape position
stateMachine->pointerMove(rive::Vec2D(200.0f, 300.0f));
REQUIRE(toggle->value() == false);
// // Inactive shape position
stateMachine->pointerMove(rive::Vec2D(200.0f, 400.0f));
REQUIRE(toggle->value() == false);
// Switches active shape to middle one
stateMachine->advance(1.5f);
artboard->advance(1.5f);
// Inactive shape position
stateMachine->pointerMove(rive::Vec2D(200.0f, 100.0f));
REQUIRE(toggle->value() == false);
// // Active shape position
stateMachine->pointerMove(rive::Vec2D(200.0f, 300.0f));
REQUIRE(toggle->value() == true);
// // Inactive shape position
stateMachine->pointerMove(rive::Vec2D(200.0f, 400.0f));
REQUIRE(toggle->value() == false);
// Switches active shape to last one
stateMachine->advance(1.0f);
artboard->advance(1.0f);
// Inactive shape position
stateMachine->pointerMove(rive::Vec2D(200.0f, 100.0f));
REQUIRE(toggle->value() == false);
// // Inactive shape position
stateMachine->pointerMove(rive::Vec2D(200.0f, 300.0f));
REQUIRE(toggle->value() == false);
// // Active shape position
stateMachine->pointerMove(rive::Vec2D(200.0f, 400.0f));
REQUIRE(toggle->value() == true);
}
TEST_CASE("hit test on nested artboards in solos", "[solo]")
{
auto file =
ReadRiveFile("assets/pointer_events_nested_artboards_in_solos.riv");
auto mainArtboard = file->artboard()->instance();
auto green_color = 0xFF00B511;
auto red_color = 0xFFC80000;
auto gray_color = 0xFF747474;
REQUIRE(mainArtboard->find("Parent-Artboard") != nullptr);
auto artboard = mainArtboard->find<rive::Artboard>("Parent-Artboard");
REQUIRE(artboard != nullptr);
artboard->updateComponents();
REQUIRE(artboard->is<rive::Artboard>());
REQUIRE(artboard->find("Nested-Artboard-Active") != nullptr);
auto nestedArtboardActive =
artboard->find<rive::NestedArtboard>("Nested-Artboard-Active");
REQUIRE(nestedArtboardActive->artboardInstance() != nullptr);
auto nestedArtboardActiveArtboardInstance =
nestedArtboardActive->artboardInstance();
auto activeRect = nestedArtboardActiveArtboardInstance->find<rive::Shape>(
"Clickable-Rectangle");
REQUIRE(activeRect != nullptr);
auto activeRectFill = activeRect->children()[1]->as<rive::Fill>();
REQUIRE(activeRectFill != nullptr);
auto activeRectFillSolidColor =
activeRectFill->paint()->as<rive::SolidColor>();
REQUIRE(activeRectFillSolidColor != nullptr);
REQUIRE(artboard->find("Nested-Artboard-Inactive") != nullptr);
auto nestedArtboardInactive =
artboard->find<rive::NestedArtboard>("Nested-Artboard-Inactive");
REQUIRE(nestedArtboardInactive->artboardInstance() != nullptr);
auto nestedArtboardInactiveArtboardInstance =
nestedArtboardInactive->artboardInstance();
auto inactiveRect =
nestedArtboardInactiveArtboardInstance->find<rive::Shape>(
"Clickable-Rectangle");
REQUIRE(inactiveRect != nullptr);
auto inactiveRectFill = inactiveRect->children()[1]->as<rive::Fill>();
REQUIRE(inactiveRectFill != nullptr);
auto inactiveRectFillSolidColor =
inactiveRectFill->paint()->as<rive::SolidColor>();
REQUIRE(inactiveRectFillSolidColor != nullptr);
REQUIRE(artboard->stateMachineCount() == 1);
auto stateMachine = mainArtboard->stateMachineAt(0);
REQUIRE(stateMachine != nullptr);
// Initialize state machine
stateMachine->advance(0.0f);
mainArtboard->advance(0.0f);
REQUIRE(nestedArtboardActive->isCollapsed() == false);
REQUIRE(nestedArtboardInactive->isCollapsed() == true);
REQUIRE(activeRectFillSolidColor->colorValue() == green_color);
REQUIRE(inactiveRectFillSolidColor->colorValue() == gray_color);
// Advance to a position in the timeline where the inactive artboard
// is active so it is rendered once
stateMachine->advance(0.1f);
mainArtboard->advance(0.1f);
REQUIRE(nestedArtboardActive->isCollapsed() == true);
REQUIRE(nestedArtboardInactive->isCollapsed() == false);
REQUIRE(inactiveRectFillSolidColor->colorValue() == green_color);
// Advance to a position in the timeline where the active artboard is active
stateMachine->advance(0.1f);
mainArtboard->advance(0.1f);
REQUIRE(nestedArtboardActive->isCollapsed() == false);
REQUIRE(nestedArtboardInactive->isCollapsed() == true);
// Apply pointer up
stateMachine->pointerUp(rive::Vec2D(200.0f, 200.0f));
stateMachine->advance(0.0f);
artboard->advance(0.0f);
// Advance to activate the inactive artboard again so it redraws
stateMachine->advance(0.1f);
artboard->advance(0.1f);
REQUIRE(nestedArtboardActive->isCollapsed() == true);
REQUIRE(nestedArtboardInactive->isCollapsed() == false);
// If the test succeeds the active nested artboard should have changed to
// red
REQUIRE(activeRectFillSolidColor->colorValue() == red_color);
// And the inactive artboard should have stayed green
REQUIRE(inactiveRectFillSolidColor->colorValue() == green_color);
}
TEST_CASE("solo index/name selection skips property-like children", "[solo]")
{
// Build an artboard with a solo whose children interleave real solo options
// (shapes) with property-like children (constraint, clipping shape, focus
// data, semantic data). Those property-like children must be skipped by
// index/name based selection so data binding targets only real options.
rive::NoOpFactory factory;
rive::Artboard artboard(&factory);
auto* solo = new rive::Solo();
auto* clip = new rive::ClippingShape();
auto* constraint = new rive::TranslationConstraint();
auto* blue = new rive::Shape();
auto* focus = new rive::FocusData();
auto* green = new rive::Shape();
auto* semantic = new rive::SemanticData();
auto* red = new rive::Shape();
blue->name("Blue");
green->name("Green");
red->name("Red");
artboard.addObject(&artboard); // id 0
artboard.addObject(solo); // id 1
artboard.addObject(clip); // id 2
artboard.addObject(constraint); // id 3
artboard.addObject(blue); // id 4
artboard.addObject(focus); // id 5
artboard.addObject(green); // id 6
artboard.addObject(semantic); // id 7
artboard.addObject(red); // id 8
solo->parentId(0);
clip->parentId(1);
constraint->parentId(1);
blue->parentId(1);
focus->parentId(1);
green->parentId(1);
semantic->parentId(1);
red->parentId(1);
// The clipping shape needs a valid source node to initialize.
clip->sourceId(artboard.idOf(blue));
REQUIRE(artboard.initialize() == rive::StatusCode::Ok);
// Index 0 must resolve to the first real option (Blue), NOT the clipping
// shape that physically precedes it in the child list.
solo->updateByIndex(0);
REQUIRE(artboard.resolve(solo->activeComponentId()) == blue);
REQUIRE(solo->getActiveChildIndex() == 0);
solo->updateByIndex(1);
REQUIRE(artboard.resolve(solo->activeComponentId()) == green);
REQUIRE(solo->getActiveChildIndex() == 1);
solo->updateByIndex(2);
REQUIRE(artboard.resolve(solo->activeComponentId()) == red);
REQUIRE(solo->getActiveChildIndex() == 2);
REQUIRE(solo->getActiveChildName() == "Red");
// Out of range for the solo set (there are only 3 options) is a no-op.
solo->updateByIndex(3);
REQUIRE(artboard.resolve(solo->activeComponentId()) == red);
// An index past the whole child list (e.g. a negative float cast to size_t
// by the data-binding path) is also a no-op.
solo->updateByIndex(SIZE_MAX);
REQUIRE(artboard.resolve(solo->activeComponentId()) == red);
// Name based selection skips the property-like children too.
solo->updateByName("Green");
REQUIRE(artboard.resolve(solo->activeComponentId()) == green);
REQUIRE(solo->getActiveChildIndex() == 1);
// With Green active, only the non-active solo options collapse. The
// property-like children follow the solo's own (uncollapsed) state.
REQUIRE(blue->isCollapsed() == true);
REQUIRE(green->isCollapsed() == false);
REQUIRE(red->isCollapsed() == true);
REQUIRE(clip->isCollapsed() == false);
REQUIRE(constraint->isCollapsed() == false);
REQUIRE(focus->isCollapsed() == false);
REQUIRE(semantic->isCollapsed() == false);
}
TEST_CASE("Data bound solos with enums work in both directions", "[silver]")
{
rive::SerializingFactory silver;
auto file = ReadRiveFile("assets/databind_solo_to_enum.riv", &silver);
auto artboard = file->artboardDefault();
REQUIRE(artboard != nullptr);
silver.frameSize(artboard->width(), artboard->height());
auto stateMachine = artboard->stateMachineAt(0);
REQUIRE(stateMachine != nullptr);
int viewModelId = artboard.get()->viewModelId();
auto vmi = viewModelId == -1
? file->createViewModelInstance(artboard.get())
: file->createViewModelInstance(viewModelId, 0);
REQUIRE(vmi != nullptr);
REQUIRE(vmi->propertyValue("enuToSource") != nullptr);
auto enuToSourceProp =
vmi->propertyValue("enuToSource")->as<rive::ViewModelInstanceEnum>();
CHECK(enuToSourceProp->propertyValue() == 3);
stateMachine->bindViewModelInstance(vmi);
stateMachine->advanceAndApply(0.0f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(425.0f, 70.0f));
stateMachine->pointerUp(rive::Vec2D(425.0f, 70.0f));
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
CHECK(enuToSourceProp->propertyValue() == 5);
CHECK(silver.matches("databind_solo_to_enum"));
}
TEST_CASE("Do not advance collapsed scripts", "[silver]")
{
auto file = ReadRiveFile("assets/script_advance_test.riv");
auto artboard = file->artboardDefault();
REQUIRE(artboard != nullptr);
auto stateMachine = artboard->stateMachineAt(0);
auto vmi = file->createDefaultViewModelInstance(artboard.get());
stateMachine->bindViewModelInstance(vmi);
auto soloIndexProp =
vmi->propertyValue("soloIndex")->as<rive::ViewModelInstanceNumber>();
auto advanceCountProp =
vmi->propertyValue("advanceCount")->as<rive::ViewModelInstanceNumber>();
REQUIRE(soloIndexProp->propertyValue() == 0);
REQUIRE(advanceCountProp->propertyValue() == 0);
stateMachine->advanceAndApply(0.016f);
REQUIRE(advanceCountProp->propertyValue() == 1);
stateMachine->advanceAndApply(0.016f);
REQUIRE(advanceCountProp->propertyValue() == 2);
// Toggles to another script
soloIndexProp->propertyValue(1);
stateMachine->advanceAndApply(0.016f);
REQUIRE(advanceCountProp->propertyValue() == 3);
// Toggles to a nested artboard with a script
soloIndexProp->propertyValue(2);
stateMachine->advanceAndApply(0.016f);
REQUIRE(advanceCountProp->propertyValue() == 4);
// Toggling to an index where no script advances
soloIndexProp->propertyValue(3);
stateMachine->advanceAndApply(0.016f);
// Value updates once more because advance is always off-by-one frame to
// the update cycle
REQUIRE(advanceCountProp->propertyValue() == 5);
stateMachine->advanceAndApply(0.016f);
// Now script does not advance anymore
REQUIRE(advanceCountProp->propertyValue() == 5);
soloIndexProp->propertyValue(0);
stateMachine->advanceAndApply(0.016f);
REQUIRE(advanceCountProp->propertyValue() == 5);
stateMachine->advanceAndApply(0.016f);
REQUIRE(advanceCountProp->propertyValue() == 6);
}
TEST_CASE("Data bind by index skipping non hierarchical children", "[silver]")
{
rive::SerializingFactory silver;
auto file = ReadRiveFile("assets/solo_index_test.riv", &silver);
auto artboard = file->artboardDefault();
REQUIRE(artboard != nullptr);
silver.frameSize(artboard->width(), artboard->height());
auto renderer = silver.makeRenderer();
auto stateMachine = artboard->stateMachineAt(0);
auto vmi = file->createDefaultViewModelInstance(artboard.get());
auto indexProp =
vmi->propertyValue("index")->as<rive::ViewModelInstanceNumber>();
stateMachine->bindViewModelInstance(vmi);
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
silver.addFrame();
indexProp->propertyValue(1);
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
silver.addFrame();
indexProp->propertyValue(2);
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
silver.addFrame();
indexProp->propertyValue(3);
stateMachine->advanceAndApply(0.1f);
artboard->draw(renderer.get());
CHECK(silver.matches("solo_index_test"));
}