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