blob: 08bc816f0072cb74199e33a613a6a8632f56180e [file]
#include "rive/artboard.hpp"
#include "rive/layout/grid_track.hpp"
#include "rive/layout/layout_component_style.hpp"
#include "rive/layout/grid_item_placement.hpp"
#include "rive/layout_component.hpp"
#include "rive_file_reader.hpp"
#include "rive_testing.hpp"
#include "yoga/Yoga.h"
#include <catch.hpp>
#include <algorithm>
// Placement moved off the style onto an optional GridItemPlacement child, so
// identifying the spanning cell means asking that child rather than the style.
static bool spansTwoColumns(rive::LayoutComponent* layout)
{
if (layout->style() == nullptr)
{
return false;
}
auto* placement = rive::GridItemPlacement::from(layout);
return placement != nullptr && placement->gridColumnSpan() == 2;
}
// Asset generated by rive_core/test/layout_grid_export_test.dart:
// a 400x400 artboard with a display:grid layout, template columns
// [100px, 100px], template rows [50px, 50px], two auto-placed fill cells and
// one cell explicitly placed at row 2 spanning both columns.
TEST_CASE("grid layout places cells from a .riv file", "[layoutgrid]")
{
auto file = ReadRiveFile("assets/layout/grid_2x2.riv");
auto artboard = file->artboard();
REQUIRE(artboard != nullptr);
auto tracks = artboard->find<rive::GridTrack>();
REQUIRE(tracks.size() == 4);
artboard->advance(0.0f);
// The exporter strips names; identify layouts by structure.
auto layouts = artboard->find<rive::LayoutComponent>();
rive::LayoutComponent* grid = nullptr;
rive::LayoutComponent* wide = nullptr;
std::vector<rive::LayoutComponent*> cells;
for (auto layout : layouts)
{
if (layout->is<rive::Artboard>() || layout->style() == nullptr)
{
continue;
}
if (layout->style()->layoutTypeValue() == 1 /* grid */)
{
grid = layout;
}
else if (spansTwoColumns(layout))
{
wide = layout;
}
else
{
cells.push_back(layout);
}
}
REQUIRE(grid != nullptr);
REQUIRE(wide != nullptr);
REQUIRE(cells.size() == 2);
// Auto-placed cells fill row 1. (The container itself keeps its default
// fixed 0 size; explicit tracks place items regardless.)
std::sort(cells.begin(), cells.end(), [](auto a, auto b) {
return a->layoutX() < b->layoutX();
});
REQUIRE(cells[0]->layoutX() == 0.0f);
REQUIRE(cells[0]->layoutY() == 0.0f);
REQUIRE(cells[0]->layoutWidth() == 100.0f);
REQUIRE(cells[0]->layoutHeight() == 50.0f);
REQUIRE(cells[1]->layoutX() == 100.0f);
REQUIRE(cells[1]->layoutY() == 0.0f);
// Explicitly placed cell spans both columns in row 2.
REQUIRE(wide->layoutX() == 0.0f);
REQUIRE(wide->layoutY() == 50.0f);
REQUIRE(wide->layoutWidth() == 200.0f);
REQUIRE(wide->layoutHeight() == 50.0f);
}
// Asset generated by rive_core/test/layout_grid_export_test.dart:
// a grid with two explicit 100px columns, ONE explicit 50px row, and a 40px
// auto-row track. Five auto-placed fill cells overflow the single explicit row
// into two implicit rows, each sized by the auto-row track. This mirrors how an
// N-item ArtboardComponentList would overflow into as many auto-rows as needed.
TEST_CASE("grid auto rows size overflow cells", "[layoutgrid]")
{
auto file = ReadRiveFile("assets/layout/grid_auto_rows.riv");
auto artboard = file->artboard();
REQUIRE(artboard != nullptr);
artboard->advance(0.0f);
auto layouts = artboard->find<rive::LayoutComponent>();
std::vector<rive::LayoutComponent*> cells;
for (auto layout : layouts)
{
if (layout->is<rive::Artboard>() || layout->style() == nullptr)
{
continue;
}
// Skip the grid container itself.
if (layout->style()->layoutTypeValue() == 1 /* grid */)
{
continue;
}
cells.push_back(layout);
}
REQUIRE(cells.size() == 5);
// Sort by (row, column) so we can assert row-major auto-placement.
std::sort(cells.begin(), cells.end(), [](auto a, auto b) {
if (a->layoutY() != b->layoutY())
{
return a->layoutY() < b->layoutY();
}
return a->layoutX() < b->layoutX();
});
// Row 1 is the explicit 50px row.
REQUIRE(cells[0]->layoutX() == 0.0f);
REQUIRE(cells[0]->layoutY() == 0.0f);
REQUIRE(cells[0]->layoutHeight() == 50.0f);
REQUIRE(cells[1]->layoutX() == 100.0f);
REQUIRE(cells[1]->layoutY() == 0.0f);
REQUIRE(cells[1]->layoutHeight() == 50.0f);
// Rows 2 and 3 are implicit, each sized by the 40px auto-row track.
REQUIRE(cells[2]->layoutX() == 0.0f);
REQUIRE(cells[2]->layoutY() == 50.0f);
REQUIRE(cells[2]->layoutHeight() == 40.0f);
REQUIRE(cells[3]->layoutX() == 100.0f);
REQUIRE(cells[3]->layoutY() == 50.0f);
REQUIRE(cells[3]->layoutHeight() == 40.0f);
REQUIRE(cells[4]->layoutX() == 0.0f);
REQUIRE(cells[4]->layoutY() == 90.0f);
REQUIRE(cells[4]->layoutHeight() == 40.0f);
// Every cell fills its single 100px column.
for (auto cell : cells)
{
REQUIRE(cell->layoutWidth() == 100.0f);
}
}
TEST_CASE("animating a grid track value reflows the layout", "[layoutgrid]")
{
auto file = ReadRiveFile("assets/layout/grid_2x2.riv");
auto artboard = file->artboard();
artboard->advance(0.0f);
auto tracks = artboard->find<rive::GridTrack>();
REQUIRE(tracks.size() == 4);
// First template column: 100 -> 150.
rive::GridTrack* firstColumn = nullptr;
for (auto track : tracks)
{
if (track->gridCollection() ==
rive::GridTrackCollection::templateColumns)
{
firstColumn = track;
break;
}
}
REQUIRE(firstColumn != nullptr);
firstColumn->trackValue(150.0f);
artboard->advance(0.0f);
// The spanning item covers both columns: 150 + 100.
auto layouts = artboard->find<rive::LayoutComponent>();
rive::LayoutComponent* wide = nullptr;
for (auto layout : layouts)
{
if (!layout->is<rive::Artboard>() && spansTwoColumns(layout))
{
wide = layout;
}
}
REQUIRE(wide != nullptr);
REQUIRE(wide->layoutWidth() == 250.0f);
REQUIRE(wide->layoutY() == 50.0f);
}
// Asset generated by rive_core/test/layout_grid_export_test.dart: a 200-wide
// grid whose three columns exercise each track-size conversion —
// minmax(60,60)=60, percent 25%=50, fr 1=remaining 90.
TEST_CASE("grid track types (minmax/percent/fr) size their columns",
"[layoutgrid]")
{
auto file = ReadRiveFile("assets/layout/grid_track_types.riv");
auto artboard = file->artboard();
REQUIRE(artboard != nullptr);
artboard->advance(0.0f);
std::vector<rive::LayoutComponent*> cells;
for (auto layout : artboard->find<rive::LayoutComponent>())
{
if (layout->is<rive::Artboard>() || layout->style() == nullptr)
{
continue;
}
if (layout->style()->layoutTypeValue() == 1 /* grid */)
{
continue;
}
cells.push_back(layout);
}
REQUIRE(cells.size() == 3);
std::sort(cells.begin(), cells.end(), [](auto a, auto b) {
return a->layoutX() < b->layoutX();
});
REQUIRE(cells[0]->layoutWidth() == 60.0f); // minmax(60,60)
REQUIRE(cells[1]->layoutWidth() == 50.0f); // percent 25% of 200
REQUIRE(cells[2]->layoutWidth() == 90.0f); // fr 1 fills the remainder
}
// grid_2x2.riv: template columns [100px, 100px], rows [50px, 50px]. The engine
// exposes the resolved grid line positions in the grid's local space (the same
// space as child layout positions) for drag/drop hit-testing — columns at
// 0/100/200, rows at 0/50/100. Non-grid nodes report no lines.
TEST_CASE("grid line offsets are exposed after layout", "[layoutgrid]")
{
auto file = ReadRiveFile("assets/layout/grid_2x2.riv");
auto artboard = file->artboard();
REQUIRE(artboard != nullptr);
artboard->advance(0.0f);
rive::LayoutComponent* grid = nullptr;
for (auto layout : artboard->find<rive::LayoutComponent>())
{
if (layout->style() != nullptr &&
layout->style()->layoutTypeValue() == 1 /* grid */)
{
grid = layout;
}
}
REQUIRE(grid != nullptr);
auto* node = static_cast<YGNode*>(grid->layoutNode(0));
REQUIRE(node != nullptr);
REQUIRE(YGNodeLayoutGetGridColumnLineCount(node) == 3);
REQUIRE(YGNodeLayoutGetGridColumnLineOffset(node, 0) == Approx(0.0f));
REQUIRE(YGNodeLayoutGetGridColumnLineOffset(node, 1) == Approx(100.0f));
REQUIRE(YGNodeLayoutGetGridColumnLineOffset(node, 2) == Approx(200.0f));
REQUIRE(YGNodeLayoutGetGridRowLineCount(node) == 3);
REQUIRE(YGNodeLayoutGetGridRowLineOffset(node, 0) == Approx(0.0f));
REQUIRE(YGNodeLayoutGetGridRowLineOffset(node, 1) == Approx(50.0f));
REQUIRE(YGNodeLayoutGetGridRowLineOffset(node, 2) == Approx(100.0f));
// A non-grid (flex) node reports no grid lines. The artboard is a flex
// LayoutComponent.
auto* flex = static_cast<YGNode*>(artboard->layoutNode(0));
REQUIRE(flex != nullptr);
REQUIRE(YGNodeLayoutGetGridColumnLineCount(flex) == 0);
REQUIRE(YGNodeLayoutGetGridRowLineCount(flex) == 0);
}