blob: 402aefc05dfb500e74172032b8e62eca7c6a65fd [file]
#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());
}