| #include "rive/artboard.hpp" |
| #include "rive/layout/layout_component_style.hpp" |
| #include "rive/layout/layout_node_provider.hpp" |
| #include "rive/layout_component.hpp" |
| #include "rive/math/aabb.hpp" |
| #include "rive/nested_artboard_layout.hpp" |
| #include "rive/shapes/shape.hpp" |
| #include "rive_file_reader.hpp" |
| #include "rive_testing.hpp" |
| #include <catch.hpp> |
| |
| // Asset generated by rive_core/test/layout_stack_export_test.dart: |
| // a 400x400 artboard with a 200x200 stack (layoutType=stack, bottomRight |
| // alignment) containing a fill child and a fixed 40x40 box. Stack synthesizes a |
| // 1x1 grid, so every child shares the single cell. |
| TEST_CASE("stack overlaps children and aligns from a .riv file", |
| "[layoutstack]") |
| { |
| auto file = ReadRiveFile("assets/layout/stack.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| // The exporter strips names; identify layouts by structure. |
| auto layouts = artboard->find<rive::LayoutComponent>(); |
| rive::LayoutComponent* stack = nullptr; |
| rive::LayoutComponent* fill = nullptr; |
| rive::LayoutComponent* box = nullptr; |
| for (auto layout : layouts) |
| { |
| if (layout->is<rive::Artboard>() || layout->style() == nullptr) |
| { |
| continue; |
| } |
| if (layout->style()->isStack()) |
| { |
| stack = layout; |
| } |
| else if (layout->style()->widthScaleType() == |
| rive::LayoutScaleType::fill) |
| { |
| fill = layout; |
| } |
| else |
| { |
| box = layout; |
| } |
| } |
| REQUIRE(stack != nullptr); |
| REQUIRE(fill != nullptr); |
| REQUIRE(box != nullptr); |
| |
| // The fill child occupies the entire cell (the whole 200x200 stack), |
| // overlapping the box — proving both share the single stacked cell. |
| REQUIRE(fill->layoutX() == 0.0f); |
| REQUIRE(fill->layoutY() == 0.0f); |
| REQUIRE(fill->layoutWidth() == 200.0f); |
| REQUIRE(fill->layoutHeight() == 200.0f); |
| |
| // The fixed box keeps its size and sits at the bottom-right (alignment). |
| REQUIRE(box->layoutWidth() == 40.0f); |
| REQUIRE(box->layoutHeight() == 40.0f); |
| REQUIRE(box->layoutX() == 160.0f); |
| REQUIRE(box->layoutY() == 160.0f); |
| } |
| |
| // Sweep the 9-way alignment and confirm the fixed box lands in the right spot. |
| // Exercises start/center/end on both axes (grid uses End, not FlexEnd). |
| TEST_CASE("stack alignment positions a fixed child", "[layoutstack]") |
| { |
| auto file = ReadRiveFile("assets/layout/stack.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto layouts = artboard->find<rive::LayoutComponent>(); |
| rive::LayoutComponent* stack = nullptr; |
| rive::LayoutComponent* box = nullptr; |
| for (auto layout : layouts) |
| { |
| if (layout->is<rive::Artboard>() || layout->style() == nullptr) |
| { |
| continue; |
| } |
| if (layout->style()->isStack()) |
| { |
| stack = layout; |
| } |
| else if (layout->style()->widthScaleType() != |
| rive::LayoutScaleType::fill) |
| { |
| box = layout; |
| } |
| } |
| REQUIRE(stack != nullptr); |
| REQUIRE(box != nullptr); |
| |
| auto at = [&](rive::LayoutAlignmentType alignment, float x, float y) { |
| stack->style()->layoutAlignmentType((uint32_t)alignment); |
| artboard->advance(0.0f); |
| REQUIRE(box->layoutX() == x); |
| REQUIRE(box->layoutY() == y); |
| }; |
| |
| at(rive::LayoutAlignmentType::topLeft, 0.0f, 0.0f); |
| at(rive::LayoutAlignmentType::topCenter, 80.0f, 0.0f); |
| at(rive::LayoutAlignmentType::topRight, 160.0f, 0.0f); |
| at(rive::LayoutAlignmentType::centerLeft, 0.0f, 80.0f); |
| at(rive::LayoutAlignmentType::center, 80.0f, 80.0f); |
| at(rive::LayoutAlignmentType::centerRight, 160.0f, 80.0f); |
| at(rive::LayoutAlignmentType::bottomLeft, 0.0f, 160.0f); |
| at(rive::LayoutAlignmentType::bottomCenter, 80.0f, 160.0f); |
| at(rive::LayoutAlignmentType::bottomRight, 160.0f, 160.0f); |
| } |
| |
| // display() folds visibility (displayValue) + algorithm (layoutTypeValue) into |
| // the single YGDisplay the engine consumes: hidden wins, else flex vs grid. |
| // Uses an attached style (from a loaded file) since the property setters fire |
| // change handlers that walk to the owning layout. |
| TEST_CASE("engine display folds visibility and layout type", "[layoutstack]") |
| { |
| auto file = ReadRiveFile("assets/layout/stack.riv"); |
| auto artboard = file->artboard(); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| rive::LayoutComponentStyle* style = nullptr; |
| for (auto layout : artboard->find<rive::LayoutComponent>()) |
| { |
| if (!layout->is<rive::Artboard>() && layout->style() != nullptr && |
| layout->style()->isStack()) |
| { |
| style = layout->style(); |
| } |
| } |
| REQUIRE(style != nullptr); |
| |
| style->displayValue(0); // visible |
| style->layoutTypeValue(0 /* flex */); |
| REQUIRE(style->display() == YGDisplayFlex); |
| style->layoutTypeValue(1 /* grid */); |
| REQUIRE(style->display() == YGDisplayGrid); |
| style->layoutTypeValue(2 /* stack */); |
| REQUIRE(style->display() == YGDisplayGrid); |
| |
| style->displayValue(1); // hidden — wins regardless of type |
| style->layoutTypeValue(0); |
| REQUIRE(style->display() == YGDisplayNone); |
| style->layoutTypeValue(1); |
| REQUIRE(style->display() == YGDisplayNone); |
| style->layoutTypeValue(2); |
| REQUIRE(style->display() == YGDisplayNone); |
| } |
| |
| // Asset generated by tests/unit_tests/gen_layout_fixtures.py |
| // (gen_nested_artboard_stack): a 200x200 stack holding two NestedArtboardLayout |
| // instances of a 40x40 artboard, plus a MixedStack artboard where one nested |
| // artboard shares the cell with a fill/fill participant. |
| // |
| // A hosted artboard is laid out through the inner ArtboardInstance's own yoga |
| // node, and that instance has no parent() to walk for its container — the host |
| // pushes the stack state in instead. Without it the instances keep yoga's |
| // auto-placement and flow into implicit rows. |
| TEST_CASE("nested artboards share the stacked cell", "[layoutstack]") |
| { |
| auto file = ReadRiveFile("assets/layout/nested_artboard_stack.riv"); |
| auto artboard = file->artboardNamed("TwoNested"); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| auto nested = artboard->find<rive::NestedArtboardLayout>(); |
| REQUIRE(nested.size() == 2); |
| for (auto* item : nested) |
| { |
| rive::AABB bounds = item->layoutBounds(); |
| // Both land in cell 1,1 — overlapped, not flowed into a second row. |
| CHECK(bounds.left() == 0.0f); |
| CHECK(bounds.top() == 0.0f); |
| CHECK(bounds.width() == 40.0f); |
| CHECK(bounds.height() == 40.0f); |
| } |
| } |
| |
| // The single-nested-artboard case only breaks once something else occupies the |
| // cell: the participant is placed explicitly at 1,1, so an auto-placed nested |
| // artboard is pushed into an implicit second row instead of overlapping it. |
| TEST_CASE("a nested artboard overlaps a participant in a stack", |
| "[layoutstack]") |
| { |
| auto file = ReadRiveFile("assets/layout/nested_artboard_stack.riv"); |
| auto artboard = file->artboardNamed("MixedStack"); |
| 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::AABB fill = provider->layoutBounds(); |
| CHECK(fill.width() == 200.0f); |
| CHECK(fill.height() == 200.0f); |
| |
| auto nested = artboard->find<rive::NestedArtboardLayout>(); |
| REQUIRE(nested.size() == 1); |
| rive::AABB bounds = nested[0]->layoutBounds(); |
| CHECK(bounds.left() == 0.0f); |
| CHECK(bounds.top() == 0.0f); |
| CHECK(bounds.width() == 40.0f); |
| CHECK(bounds.height() == 40.0f); |
| } |
| |
| // Asset generated by tests/unit_tests/gen_layout_fixtures.py |
| // (gen_nested_artboard_fill). |
| // |
| // A hosted artboard is sized by the host's override, not by the scale type |
| // stored on its own style (fixed here). The item phase decides the inline |
| // stretch, and a stack takes its justify-items from its alignment (start / |
| // center / end — never stretch), so the override is the only thing that can |
| // say stretch. Read the stored scale type there instead and a fill instance |
| // keeps the source artboard's width in a stack while filling in a grid, whose |
| // justify-items defaults to stretch and covers for it. The two must agree. |
| TEST_CASE("a fill nested artboard fills a stack and a grid alike", |
| "[layoutstack]") |
| { |
| auto boundsIn = [](const char* artboardName) { |
| auto file = ReadRiveFile("assets/layout/nested_artboard_fill.riv"); |
| auto artboard = file->artboardNamed(artboardName); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| auto nested = artboard->find<rive::NestedArtboardLayout>(); |
| REQUIRE(nested.size() == 1); |
| return nested[0]->layoutBounds(); |
| }; |
| |
| // The instance is fill width / fixed 40 height in a 200x200 container. |
| rive::AABB inGrid = boundsIn("NestedInGrid"); |
| rive::AABB inStack = boundsIn("NestedInStack"); |
| |
| CHECK(inGrid.width() == 200.0f); |
| CHECK(inGrid.height() == 40.0f); |
| CHECK(inStack.width() == inGrid.width()); |
| CHECK(inStack.height() == inGrid.height()); |
| CHECK(inStack.left() == 0.0f); |
| CHECK(inStack.top() == 0.0f); |
| } |
| |
| // A stack synthesizes the same 1x1 grid, so it inherits the track's content |
| // floor. Same reflowing child as grid_min_content's ReflowGrid: the cell has to |
| // stay the stack's box and let the content wrap inside it. |
| TEST_CASE("a stack cell floors at min-content too", "[layoutstack]") |
| { |
| auto file = ReadRiveFile("assets/layout/grid_min_content.riv"); |
| auto artboard = file->artboardNamed("ReflowStack"); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| |
| rive::LayoutComponent* container = nullptr; |
| rive::LayoutComponent* child = nullptr; |
| for (auto layout : artboard->find<rive::LayoutComponent>()) |
| { |
| if (layout->is<rive::Artboard>()) |
| { |
| continue; |
| } |
| if (layout->name() == "Container") |
| { |
| container = layout; |
| } |
| else if (layout->name() == "Child") |
| { |
| child = layout; |
| } |
| } |
| REQUIRE(container != nullptr); |
| REQUIRE(child != nullptr); |
| |
| CHECK(container->layoutWidth() == 400.0f); |
| CHECK(child->layoutWidth() == 400.0f); |
| } |
| |
| // Hug is decided by justify-self, and the downgrade that keeps a hug item off |
| // stretch is guarded by the same effective-vs-stored scale type. Reading the |
| // stored one (fixed, on HugItem's own style) left a hug instance stretched to |
| // the whole column in a grid, while a stack hugged correctly -- the stack path |
| // never consults it. The two must agree: 120, the content HugItem holds. |
| TEST_CASE("a hug nested artboard hugs in a stack and a grid alike", |
| "[layoutstack]") |
| { |
| auto boundsIn = [](const char* artboardName) { |
| auto file = ReadRiveFile("assets/layout/nested_artboard_fill.riv"); |
| auto artboard = file->artboardNamed(artboardName); |
| REQUIRE(artboard != nullptr); |
| artboard->advance(0.0f); |
| auto nested = artboard->find<rive::NestedArtboardLayout>(); |
| REQUIRE(nested.size() == 1); |
| return nested[0]->layoutBounds(); |
| }; |
| |
| // The instance is hug width / fixed 40 height in a 200x200 container. |
| rive::AABB inStack = boundsIn("HugInStack"); |
| rive::AABB inGrid = boundsIn("HugInGrid"); |
| |
| CHECK(inStack.width() == 120.0f); |
| CHECK(inStack.height() == 40.0f); |
| CHECK(inGrid.width() == inStack.width()); |
| CHECK(inGrid.height() == inStack.height()); |
| } |