| #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()); | |
| } |