| #include "rive/artboard.hpp" |
| #include "rive/artboard_component_list.hpp" |
| #include "rive/layout_component.hpp" |
| #include "rive/node.hpp" |
| #include "rive/layout/layout_component_style.hpp" |
| #include "rive/layout/layout_node_provider.hpp" |
| #include "rive/math/aabb.hpp" |
| #include "rive/shapes/points_path.hpp" |
| #include "rive/shapes/shape.hpp" |
| #include "rive/solo.hpp" |
| #include "rive_file_reader.hpp" |
| #include "rive_testing.hpp" |
| #include <catch.hpp> |
| #include <algorithm> |
| #include <cmath> |
| #include <vector> |
| |
| // Asset generated by rive_core/test/layout_participant_export_test.dart: |
| // a 200x200 stack containing a single fill/fill parametric-shape participant |
| // (a LayoutParticipant child). The participant should fill the whole stacked |
| // cell. |
| TEST_CASE("a fill participant fills a stack cell from a .riv file", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/stack_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(shapes.size() == 1); |
| auto* provider = rive::LayoutNodeProvider::from(shapes[0]); |
| REQUIRE(provider != nullptr); |
| |
| // Per-axis fill stretches the participant to the full 200x200 cell. |
| auto bounds = provider->layoutBounds(); |
| REQUIRE(bounds.width() == 200.0f); |
| REQUIRE(bounds.height() == 200.0f); |
| } |
| |
| // Asset generated by rive_core/test/layout_participant_export_test.dart: |
| // a 200x200 flex container holding a Solo whose active child is a fill/fill |
| // parametric-shape participant. A Solo is transparent to layout — it provides |
| // no node of its own, and the layout descends into the active child, so that |
| // child is sized to the full cell while the inactive one is left out. |
| TEST_CASE("a Solo's active child is laid out through it from a .riv file", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/solo_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto solos = artboard->find<rive::Solo>(); |
| REQUIRE(solos.size() == 1); |
| auto* solo = solos[0]; |
| // The Solo itself is not a provider — it is transparent. |
| REQUIRE(rive::LayoutNodeProvider::from(solo) == nullptr); |
| |
| auto* active = solo->activeComponent(); |
| REQUIRE(active != nullptr); |
| auto* provider = rive::LayoutNodeProvider::from(active); |
| REQUIRE(provider != nullptr); |
| auto bounds = provider->layoutBounds(); |
| REQUIRE(bounds.width() == 200.0f); |
| REQUIRE(bounds.height() == 200.0f); |
| } |
| |
| // Asset generated by rive_core/test/layout_participant_export_test.dart: |
| // a 100x100 single-cell grid with a hug/hug parametric-shape participant (a |
| // 10x10 rectangle). The participant hugs its intrinsic 10x10 instead of |
| // stretching to the cell. |
| TEST_CASE("a hug participant hugs its content from a .riv file", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/hug_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| // A single pass is enough: layout solves before geometry builds, but a |
| // parametric path reports its declared box up front (tryPropertyBounds), so |
| // it measures correctly without waiting for a second advance — matching how |
| // it behaves as plain layout content. |
| artboard->advance(0.0f); |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(shapes.size() == 1); |
| auto* provider = rive::LayoutNodeProvider::from(shapes[0]); |
| REQUIRE(provider != nullptr); |
| |
| // Sized to the rectangle's intrinsic bounds, not the 100px cell. |
| auto bounds = provider->layoutBounds(); |
| REQUIRE(bounds.width() == Approx(10.0f)); |
| REQUIRE(bounds.height() == Approx(10.0f)); |
| } |
| |
| // Asset generated by rive_core/test/layout_participant_export_test.dart: |
| // a 200x200 flex container with a fixed 60x40 participant. |
| TEST_CASE("a fixed-size participant keeps its size from a .riv file", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/fixed_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(shapes.size() == 1); |
| auto* provider = rive::LayoutNodeProvider::from(shapes[0]); |
| REQUIRE(provider != nullptr); |
| |
| auto bounds = provider->layoutBounds(); |
| REQUIRE(bounds.width() == Approx(60.0f)); |
| REQUIRE(bounds.height() == Approx(40.0f)); |
| } |
| |
| // Asset generated by rive_core/test/layout_participant_export_test.dart: |
| // a 200x200 flex container with two fill participants, one display:none. The |
| // hidden one collapses and leaves the flow, so the other is the sole in-flow |
| // child and fills the container. |
| TEST_CASE("a display:none participant collapses and leaves the flow " |
| "from a .riv file", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/display_none_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(shapes.size() == 2); |
| |
| int collapsed = 0; |
| rive::Shape* shown = nullptr; |
| for (auto* shape : shapes) |
| { |
| if (shape->isCollapsed()) |
| { |
| collapsed++; |
| } |
| else |
| { |
| shown = shape; |
| } |
| } |
| REQUIRE(collapsed == 1); |
| REQUIRE(shown != nullptr); |
| |
| // The sole in-flow child fills the whole container. |
| auto* provider = rive::LayoutNodeProvider::from(shown); |
| REQUIRE(provider != nullptr); |
| auto bounds = provider->layoutBounds(); |
| REQUIRE(bounds.width() == Approx(200.0f)); |
| REQUIRE(bounds.height() == Approx(200.0f)); |
| } |
| |
| // Asset generated by rive_core/test/layout_participant_export_test.dart: |
| // a 200x200 flex container with a participant whose fixed width (10) is clamped |
| // up by minWidth (50) and whose fill height is clamped down by maxHeight (30). |
| TEST_CASE("min/max constraints clamp a participant slot from a .riv file", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/constrained_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(shapes.size() == 1); |
| auto* provider = rive::LayoutNodeProvider::from(shapes[0]); |
| REQUIRE(provider != nullptr); |
| |
| auto bounds = provider->layoutBounds(); |
| REQUIRE(bounds.width() == Approx(50.0f)); |
| REQUIRE(bounds.height() == Approx(30.0f)); |
| } |
| |
| // The Solo's active-child index helpers (used by data binding) resolve and |
| // switch the active soloable option. Reuses the solo forwarding fixture, whose |
| // Solo has two shape options with the first active. |
| TEST_CASE("a Solo's active-child index helpers work from a .riv file", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/solo_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto solos = artboard->find<rive::Solo>(); |
| REQUIRE(solos.size() == 1); |
| auto* solo = solos[0]; |
| |
| // The first soloable option is active. |
| REQUIRE(solo->getActiveChildIndex() == 0); |
| // Switch to the second option by index; the active index follows. |
| solo->updateByIndex(1); |
| REQUIRE(solo->getActiveChildIndex() == 1); |
| } |
| |
| // Asset generated by rive_core/test/layout_participant_export_test.dart: |
| // a 200x200 flex container with 1s linear layout interpolation and a fill/fill |
| // participant. Shrinking the container makes the participant animate toward the |
| // new slot rather than snapping. |
| TEST_CASE("a participant animates its slot under an animated layout", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/animated_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(shapes.size() == 1); |
| auto* provider = rive::LayoutNodeProvider::from(shapes[0]); |
| REQUIRE(provider != nullptr); |
| // First solve snaps to the full 200-wide container (no animate-in). |
| REQUIRE(provider->layoutBounds().width() == Approx(200.0f)); |
| |
| // Find the (non-artboard) container and shrink it to 100 wide. |
| rive::LayoutComponent* container = nullptr; |
| for (auto* lc : artboard->find<rive::LayoutComponent>()) |
| { |
| if (!lc->is<rive::Artboard>() && lc->style() != nullptr) |
| { |
| container = lc; |
| break; |
| } |
| } |
| REQUIRE(container != nullptr); |
| container->width(100.0f); |
| |
| // Drive the 1s linear interpolation in steps; the participant should pass |
| // through intermediate widths rather than jumping straight to 100. |
| float mid = 200.0f; |
| for (int i = 0; i < 5; i++) |
| { |
| artboard->advance(0.2f); |
| float w = provider->layoutBounds().width(); |
| if (w > 100.0f && w < 200.0f) |
| { |
| mid = w; |
| } |
| } |
| REQUIRE(mid < 200.0f); // animated (didn't stay at 200) |
| REQUIRE(mid > 100.0f); // caught mid-interpolation (didn't snap to 100) |
| |
| // Settle: the animation completes at the new slot. |
| for (int i = 0; i < 3; i++) |
| { |
| artboard->advance(1.0f); |
| } |
| REQUIRE(provider->layoutBounds().width() == Approx(100.0f)); |
| } |
| |
| // Re-targeting an in-flight layout animation exercises the smoothing / |
| // double-buffer path (LayoutParticipant::applyInterpolation isSmoothing block). |
| TEST_CASE("a participant re-targets an in-flight layout animation", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/animated_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(shapes.size() == 1); |
| auto* provider = rive::LayoutNodeProvider::from(shapes[0]); |
| REQUIRE(provider != nullptr); |
| |
| rive::LayoutComponent* container = nullptr; |
| for (auto* lc : artboard->find<rive::LayoutComponent>()) |
| { |
| if (!lc->is<rive::Artboard>() && lc->style() != nullptr) |
| { |
| container = lc; |
| break; |
| } |
| } |
| REQUIRE(container != nullptr); |
| |
| // Start animating toward 100 and advance until it's genuinely in flight. |
| // (The first tick only accrues elapsed before it visibly moves, and how |
| // many frames that takes varies, so loop until it drops below 200 rather |
| // than assuming a fixed frame count.) |
| container->width(100.0f); |
| float midWidth = 200.0f; |
| for (int i = 0; i < 8 && midWidth >= 200.0f; i++) |
| { |
| artboard->advance(0.1f); |
| midWidth = provider->layoutBounds().width(); |
| } |
| REQUIRE(midWidth < 200.0f); // in flight, not snapped |
| REQUIRE(midWidth > 100.0f); // not yet settled at the target |
| |
| // Re-target toward 50 while in flight (elapsed != 0), so the participant |
| // smooths from its current position rather than restarting. |
| container->width(50.0f); |
| for (int i = 0; i < 8; i++) |
| { |
| artboard->advance(1.0f); |
| } |
| REQUIRE(provider->layoutBounds().width() == Approx(50.0f)); |
| } |
| |
| // Turning a parent layout's interpolation off frees the participant's lazy |
| // animation state (LayoutParticipant::cascadeLayoutStyle free path) and it |
| // snaps thereafter. |
| TEST_CASE("disabling a layout's interpolation frees participant animation", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/animated_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); // allocates the participant's animation state |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(shapes.size() == 1); |
| auto* provider = rive::LayoutNodeProvider::from(shapes[0]); |
| REQUIRE(provider != nullptr); |
| |
| rive::LayoutComponent* container = nullptr; |
| for (auto* lc : artboard->find<rive::LayoutComponent>()) |
| { |
| if (!lc->is<rive::Artboard>() && lc->style() != nullptr) |
| { |
| container = lc; |
| break; |
| } |
| } |
| REQUIRE(container != nullptr); |
| |
| // Disable animation (interpolationTime -> 0) and let it re-cascade. |
| container->style()->interpolationTime(0.0f); |
| artboard->advance(0.0f); |
| |
| // Now a slot change snaps immediately instead of animating. |
| container->width(100.0f); |
| artboard->advance(0.016f); |
| REQUIRE(provider->layoutBounds().width() == Approx(100.0f)); |
| } |
| |
| // Asset generated by rive_core/test/layout_participant_export_test.dart: |
| // a 200x200 two-column grid (100px columns, one 200px row) with two |
| // participating shapes — one fill/fill, one fill-width/fixed-50-height. Covers |
| // the grid branch of syncStyleChanges for participants (justify-self stretch |
| // and both align-self states), which the LayoutComponent-cell grid tests don't. |
| TEST_CASE("participants size to grid cells from a .riv file", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/grid_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(shapes.size() == 2); |
| std::vector<rive::LayoutNodeProvider*> providers; |
| for (auto* shape : shapes) |
| { |
| auto* p = rive::LayoutNodeProvider::from(shape); |
| REQUIRE(p != nullptr); |
| providers.push_back(p); |
| } |
| // Order by column x: [0] is column 1, [1] is column 2. |
| std::sort(providers.begin(), providers.end(), [](auto* a, auto* b) { |
| return a->layoutBounds().left() < b->layoutBounds().left(); |
| }); |
| |
| // Column 1: fill/fill fills the whole 100x200 cell. |
| REQUIRE(providers[0]->layoutBounds().width() == Approx(100.0f)); |
| REQUIRE(providers[0]->layoutBounds().height() == Approx(200.0f)); |
| // Column 2: fill width, fixed 50 height. |
| REQUIRE(providers[1]->layoutBounds().width() == Approx(100.0f)); |
| REQUIRE(providers[1]->layoutBounds().height() == Approx(50.0f)); |
| } |
| |
| // Asset generated by rive_core/test/layout_participant_export_test.dart: |
| // like animated_participant but the layout uses a custom cubic (eased) |
| // interpolator, so the participant's interpolation runs through the |
| // KeyFrameInterpolator::transform path (uncovered by the linear fixture). |
| TEST_CASE("a participant animates its slot with a cubic interpolator", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/animated_cubic_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(shapes.size() == 1); |
| auto* provider = rive::LayoutNodeProvider::from(shapes[0]); |
| REQUIRE(provider != nullptr); |
| REQUIRE(provider->layoutBounds().width() == Approx(200.0f)); |
| |
| rive::LayoutComponent* container = nullptr; |
| for (auto* lc : artboard->find<rive::LayoutComponent>()) |
| { |
| if (!lc->is<rive::Artboard>() && lc->style() != nullptr) |
| { |
| container = lc; |
| break; |
| } |
| } |
| REQUIRE(container != nullptr); |
| container->width(100.0f); |
| |
| // The eased curve still moves 200 -> 100 over 1s, just non-linearly. |
| float mid = 200.0f; |
| for (int i = 0; i < 8; i++) |
| { |
| artboard->advance(0.15f); |
| float w = provider->layoutBounds().width(); |
| if (w > 100.0f && w < 200.0f) |
| { |
| mid = w; |
| } |
| } |
| REQUIRE(mid < 200.0f); // eased in-flight (didn't stay at 200) |
| REQUIRE(mid > 100.0f); // caught mid-interpolation (didn't snap) |
| |
| for (int i = 0; i < 3; i++) |
| { |
| artboard->advance(1.0f); |
| } |
| REQUIRE(provider->layoutBounds().width() == Approx(100.0f)); |
| } |
| |
| // Re-targeting an in-flight cubic animation a second time while it's already |
| // smoothing exercises the double-buffer copy in updateLayoutBounds and the |
| // eased smoothing branch of applyInterpolation. |
| TEST_CASE("a participant re-targets a cubic animation while smoothing", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/animated_cubic_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(shapes.size() == 1); |
| auto* provider = rive::LayoutNodeProvider::from(shapes[0]); |
| REQUIRE(provider != nullptr); |
| |
| rive::LayoutComponent* container = nullptr; |
| for (auto* lc : artboard->find<rive::LayoutComponent>()) |
| { |
| if (!lc->is<rive::Artboard>() && lc->style() != nullptr) |
| { |
| container = lc; |
| break; |
| } |
| } |
| REQUIRE(container != nullptr); |
| |
| // Get into flight toward 100. |
| container->width(100.0f); |
| float w = 200.0f; |
| for (int i = 0; i < 8 && w >= 200.0f; i++) |
| { |
| artboard->advance(0.1f); |
| w = provider->layoutBounds().width(); |
| } |
| REQUIRE(w < 200.0f); |
| |
| // Retarget toward 80 (begins smoothing), advance one small step (< 1s so it |
| // stays smoothing), then retarget again toward 50 while still smoothing — |
| // the second retarget copies the in-flight buffer. |
| container->width(80.0f); |
| artboard->advance(0.1f); |
| container->width(50.0f); |
| |
| for (int i = 0; i < 20; i++) |
| { |
| artboard->advance(1.0f); |
| } |
| REQUIRE(provider->layoutBounds().width() == Approx(50.0f)); |
| } |
| |
| // Asset generated by gen_layout_fixtures.py: a 200x200 stack whose only child |
| // is a group holding a fill/fill participant. Groups are transparent to layout, |
| // so the stack descends through and sizes the participant to the full cell — |
| // the same result as stack_participant.riv, where it is a direct child. |
| TEST_CASE("a participant inside a group is laid out through it", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/group_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(shapes.size() == 1); |
| // The group itself provides no layout node — it is transparent. |
| REQUIRE(rive::LayoutNodeProvider::from(shapes[0]->parent()) == nullptr); |
| |
| auto* provider = rive::LayoutNodeProvider::from(shapes[0]); |
| REQUIRE(provider != nullptr); |
| auto bounds = provider->layoutBounds(); |
| REQUIRE(bounds.width() == 200.0f); |
| REQUIRE(bounds.height() == 200.0f); |
| } |
| |
| // Asset generated by gen_layout_fixtures.py: a 200x200 stack containing a group |
| // that holds both a nested group wrapping a participant and a Solo whose active |
| // child is one. Covers multi-level descent and a Solo inside a group, which was |
| // impossible while a Solo forwarded a single provider. |
| TEST_CASE("participants nested in groups and in a grouped Solo are laid out", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/nested_group_participant.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto solos = artboard->find<rive::Solo>(); |
| REQUIRE(solos.size() == 1); |
| auto* solo = solos[0]; |
| auto* active = solo->activeComponent(); |
| REQUIRE(active != nullptr); |
| |
| // The Solo sits inside a group, and its active child is still collected. |
| auto* activeProvider = rive::LayoutNodeProvider::from(active); |
| REQUIRE(activeProvider != nullptr); |
| REQUIRE(activeProvider->layoutBounds().width() == 200.0f); |
| REQUIRE(activeProvider->layoutBounds().height() == 200.0f); |
| |
| // The inactive sibling is never exposed, so the stack never sizes it. |
| rive::Component* inactive = nullptr; |
| for (auto* child : solo->children()) |
| { |
| if (child != active && child->is<rive::Shape>()) |
| { |
| inactive = child; |
| } |
| } |
| REQUIRE(inactive != nullptr); |
| auto* inactiveProvider = rive::LayoutNodeProvider::from(inactive); |
| REQUIRE(inactiveProvider != nullptr); |
| REQUIRE(inactiveProvider->layoutBounds().width() != 200.0f); |
| |
| // The participant two groups deep is collected like a direct child. |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(shapes.size() == 3); |
| rive::Shape* deep = nullptr; |
| for (auto* shape : shapes) |
| { |
| if (shape->parent() != solo) |
| { |
| deep = shape; |
| } |
| } |
| REQUIRE(deep != nullptr); |
| auto* deepProvider = rive::LayoutNodeProvider::from(deep); |
| REQUIRE(deepProvider != nullptr); |
| REQUIRE(deepProvider->layoutBounds().width() == 200.0f); |
| REQUIRE(deepProvider->layoutBounds().height() == 200.0f); |
| } |
| |
| // clipping_and_draw_order.riv has an ArtboardComponentList inside a plain |
| // group. A list provides a layout node unconditionally — it never opted in the |
| // way a participant does — so a group between it and its layout is how a file |
| // asks for free-form items placed by x/y or a follow-path constraint. It has to |
| // stay opaque to it, or the list is pulled into the yoga tree and re-solved |
| // every frame. |
| TEST_CASE("an artboard component list inside a group stays out of the layout", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/clipping_and_draw_order.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto lists = artboard->find<rive::ArtboardComponentList>(); |
| REQUIRE(lists.size() == 1); |
| // It hangs off a group rather than the artboard... |
| // (copied to a local: REQUIRE binds by reference, and an in-class static |
| // const has no out-of-line definition to bind to.) |
| const uint16_t nodeTypeKey = rive::NodeBase::typeKey; |
| REQUIRE(lists[0]->parent()->coreType() == nodeTypeKey); |
| // ...so the artboard collects no layout children at all. |
| REQUIRE(artboard->isLeaf()); |
| } |
| |
| // list_in_group_joins_layout.riv is the same shape but the list carries the |
| // ParticipatesInLayout drawable flag — the opt-in a newly authored list |
| // gets. The group is then transparent to it, so the artboard collects it and is |
| // no longer a leaf. |
| TEST_CASE("a flagged artboard component list joins the layout through a group", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout/list_in_group_joins_layout.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto lists = artboard->find<rive::ArtboardComponentList>(); |
| REQUIRE(lists.size() == 1); |
| const uint16_t nodeTypeKey = rive::NodeBase::typeKey; |
| REQUIRE(lists[0]->parent()->coreType() == nodeTypeKey); |
| // The flag lets the layout see through the group to the list. |
| REQUIRE_FALSE(artboard->isLeaf()); |
| } |
| |
| TEST_CASE("a custom-path participant measures before its paths are built", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout_grid_stack.riv"); |
| auto artboard = file->artboardNamed("GridWithLayoutParticipants"); |
| REQUIRE(artboard != nullptr); |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(!shapes.empty()); |
| |
| // Deliberately before any advance(): no rawPath has been built yet, which |
| // is exactly the state the first layout pass observes. |
| size_t customPathShapes = 0; |
| for (auto* shape : shapes) |
| { |
| INFO("shape: " << shape->name()); |
| rive::AABB bounds = shape->computeIntrinsicBounds(); |
| // Never inverted, whatever the path type. |
| CHECK(bounds.width() >= 0.0f); |
| CHECK(bounds.height() >= 0.0f); |
| |
| bool hasCustomPath = false; |
| for (auto* path : shape->paths()) |
| { |
| if (path->is<rive::PointsPath>() && !path->isCollapsed()) |
| { |
| hasCustomPath = true; |
| break; |
| } |
| } |
| if (!hasCustomPath) |
| { |
| continue; |
| } |
| customPathShapes++; |
| // Without the on-demand build this measures empty. |
| CHECK(bounds.width() > 0.0f); |
| CHECK(bounds.height() > 0.0f); |
| } |
| REQUIRE(customPathShapes > 0); |
| } |
| |
| TEST_CASE("a participant with an empty path keeps a sane world transform", |
| "[layoutparticipant]") |
| { |
| auto file = ReadRiveFile("assets/layout_grid_stack.riv"); |
| auto artboard = file->artboardNamed("GridWithLayoutParticipants"); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto shapes = artboard->find<rive::Shape>(); |
| REQUIRE(!shapes.empty()); |
| |
| // Slots live inside the artboard; the sentinel leak lands at -3.4e38, so |
| // any generous bound separates the two. |
| const float limit = 1.0e6f; |
| for (auto* shape : shapes) |
| { |
| INFO("shape: " << shape->name()); |
| // The cause: bounds must never come back inverted. |
| rive::AABB intrinsic = shape->computeIntrinsicBounds(); |
| CHECK(intrinsic.width() >= 0.0f); |
| CHECK(intrinsic.height() >= 0.0f); |
| // The symptom: the anchor derived from them stays near the slot. |
| const rive::Mat2D& world = shape->worldTransform(); |
| CHECK(std::fabs(world[4]) < limit); |
| CHECK(std::fabs(world[5]) < limit); |
| } |
| } |