Files
2026-08-16 18:24:52 +07:00

541 lines
15 KiB
C++

// This file is part of VSTGUI. It is subject to the license terms
// in the LICENSE file found in the top-level directory of this
// distribution and at http://github.com/steinbergmedia/vstgui/LICENSE
#include "gridlayouter.h"
#include "../cviewcontainer.h"
//------------------------------------------------------------------------
namespace VSTGUI {
//------------------------------------------------------------------------
GridLayouter::GridLayouter (const GridLayoutProperties& props) : gridProps (props) {}
//------------------------------------------------------------------------
void GridLayouter::setProperties (const GridLayoutProperties& props) { gridProps = props; }
//------------------------------------------------------------------------
const GridLayoutProperties& GridLayouter::getProperties () const { return gridProps; }
//------------------------------------------------------------------------
GridLayoutProperties& GridLayouter::getProperties () { return gridProps; }
//------------------------------------------------------------------------
std::vector<CCoord> GridLayouter::calculateAutoRows (CCoord& usedRowHeight, CCoord availableHeight,
size_t& outNumAutoRows) const
{
std::vector<CCoord> autoRows;
if (!gridProps.autoRows.empty ())
{
for (const auto& el : gridProps.autoRows)
{
if (std::holds_alternative<CCoord> (el))
{
autoRows.push_back (std::get<CCoord> (el));
usedRowHeight += autoRows.back ();
}
else if (std::holds_alternative<GridLayoutProperties::Percentage> (el))
{
autoRows.push_back (
availableHeight *
(std::get<GridLayoutProperties::Percentage> (el).value / 100.0));
usedRowHeight += autoRows.back ();
}
else if (std::holds_alternative<GridLayoutProperties::Auto> (el))
{
autoRows.push_back (kAutoSize);
++outNumAutoRows;
}
}
}
return autoRows;
}
//------------------------------------------------------------------------
std::vector<CCoord> GridLayouter::calculateAutoColumns (CCoord& usedColWidth, CCoord availableWidth,
size_t& outNumAutoCols) const
{
std::vector<CCoord> autoColumns;
if (!gridProps.autoColumns.empty ())
{
for (const auto& el : gridProps.autoColumns)
{
if (std::holds_alternative<CCoord> (el))
{
autoColumns.push_back (std::get<CCoord> (el));
usedColWidth += autoColumns.back ();
}
else if (std::holds_alternative<GridLayoutProperties::Percentage> (el))
{
autoColumns.push_back (
availableWidth *
(std::get<GridLayoutProperties::Percentage> (el).value / 100.0));
usedColWidth += autoColumns.back ();
}
else if (std::holds_alternative<GridLayoutProperties::Auto> (el))
{
autoColumns.push_back (kAutoSize);
++outNumAutoCols;
}
}
}
return autoColumns;
}
//------------------------------------------------------------------------
std::optional<ViewLayout> GridLayouter::calculateLayout (const CViewContainer& /*view*/,
const Children& children,
const CRect& _newSize)
{
if (children.empty ())
return {{_newSize, LayoutData {}}};
size_t rows = gridProps.rows;
size_t cols = gridProps.columns;
CCoord rowGap = gridProps.rowGap;
CCoord colGap = gridProps.columnGap;
if (rows == 0 || cols == 0)
{
return std::nullopt;
}
auto newSize = _newSize;
newSize.originize ();
std::vector<CCoord> rowHeights (rows, 0.0);
std::vector<CCoord> colWidths (cols, 0.0);
CCoord totalRowGap = (rows - 1) * rowGap;
CCoord totalColGap = (cols - 1) * colGap;
CCoord availableHeight = newSize.getHeight () - totalRowGap;
CCoord availableWidth = newSize.getWidth () - totalColGap;
CCoord usedRowHeight = 0.0;
size_t numAutoRows = 0u;
CCoord usedColWidth = 0.0;
size_t numAutoCols = 0u;
std::vector<CCoord> autoRows = calculateAutoRows (usedRowHeight, availableHeight, numAutoRows);
std::vector<CCoord> autoColumns =
calculateAutoColumns (usedColWidth, availableWidth, numAutoCols);
CCoord sumAutoRows = 0.0;
for (size_t r = 0; r < rows; ++r)
{
if (r < autoRows.size ())
{
if (autoRows[r] == kAutoSize)
autoRows[r] = (availableHeight - usedRowHeight) / numAutoRows;
rowHeights[r] = autoRows[r];
sumAutoRows += rowHeights[r];
}
}
CCoord defaultRowHeight = (rows > autoRows.size ())
? (availableHeight - sumAutoRows) / (rows - autoRows.size ())
: 0.0;
for (size_t r = 0; r < rows; ++r)
{
if (r >= autoRows.size ())
rowHeights[r] = defaultRowHeight;
}
CCoord sumAutoCols = 0.0;
for (size_t c = 0; c < cols; ++c)
{
if (c < autoColumns.size ())
{
if (autoColumns[c] == kAutoSize)
autoColumns[c] = (availableWidth - usedColWidth) / numAutoCols;
colWidths[c] = autoColumns[c];
sumAutoCols += colWidths[c];
}
}
CCoord defaultColWidth = (cols > autoColumns.size ())
? (availableWidth - sumAutoCols) / (cols - autoColumns.size ())
: 0.0;
for (size_t c = 0; c < cols; ++c)
{
if (c >= autoColumns.size ())
colWidths[c] = defaultColWidth;
}
CCoord gridHeight = sumAutoRows + defaultRowHeight * (rows - autoRows.size ()) + totalRowGap;
CCoord gridWidth = sumAutoCols + defaultColWidth * (cols - autoColumns.size ()) + totalColGap;
CCoord offsetY = 0.0;
CCoord offsetX = 0.0;
CCoord extraSpaceY = newSize.getHeight () - gridHeight;
CCoord extraSpaceX = newSize.getWidth () - gridWidth;
CCoord spaceBetweenY = 0.0;
CCoord spaceAroundY = 0.0;
CCoord spaceBetweenX = 0.0;
CCoord spaceAroundX = 0.0;
switch (gridProps.alignContent)
{
case GridLayoutProperties::AlignContent::Center:
{
offsetY = extraSpaceY / 2.0;
break;
}
case GridLayoutProperties::AlignContent::End:
{
offsetY = extraSpaceY;
break;
}
case GridLayoutProperties::AlignContent::Stretch:
{
if (rows > 0 && extraSpaceY > 0.0)
{
CCoord add = extraSpaceY / rows;
for (size_t r = 0; r < rows; ++r)
rowHeights[r] += add;
}
offsetY = 0.0;
break;
}
case GridLayoutProperties::AlignContent::SpaceBetween:
{
spaceBetweenY = (rows > 1) ? (extraSpaceY / (rows - 1)) : 0.0;
offsetY = 0.0;
break;
}
case GridLayoutProperties::AlignContent::SpaceAround:
{
spaceAroundY = (rows > 0) ? (extraSpaceY / (rows + 1)) : 0.0;
offsetY = spaceAroundY;
break;
}
default:
{
offsetY = 0.0;
break;
}
}
switch (gridProps.justifyContent)
{
case GridLayoutProperties::JustifyContent::Center:
{
offsetX = (newSize.getWidth () - gridWidth) / 2.0;
break;
}
case GridLayoutProperties::JustifyContent::End:
{
offsetX = newSize.getWidth () - gridWidth;
break;
}
case GridLayoutProperties::JustifyContent::Stretch:
{
if (cols > 0 && extraSpaceX > 0.0)
{
CCoord add = extraSpaceX / cols;
for (size_t c = 0; c < cols; ++c)
colWidths[c] += add;
}
offsetX = 0.0;
break;
}
case GridLayoutProperties::JustifyContent::SpaceBetween:
{
spaceBetweenX = (cols > 1) ? (extraSpaceX / (cols - 1)) : 0.0;
offsetX = 0.0;
break;
}
case GridLayoutProperties::JustifyContent::SpaceAround:
{
spaceAroundX = (cols > 0) ? (extraSpaceX / (cols + 1)) : 0.0;
offsetX = spaceAroundX;
break;
}
default:
{
offsetX = 0.0;
break;
}
}
LayoutData layout;
layout.reserve (children.size ());
auto child = children.begin ();
if (!gridProps.gridAreas.empty ())
{
// Build an occupancy grid of explicit tracks
auto idx2D = [cols] (size_t r, size_t c) {
return r * cols + c;
};
std::vector<bool> occupied (rows * cols, false);
// Helper to compute the raw cell rectangle for an area [r0,r1) x [c0,c1)
auto computeCellRect = [&] (size_t r0, size_t c0, size_t r1, size_t c1) {
CCoord y = newSize.top + offsetY;
for (size_t r = 0; r < r0; ++r)
{
y += rowHeights[r] + rowGap;
if (r < rows - 1)
y += spaceBetweenY + spaceAroundY;
}
CCoord x = newSize.left + offsetX;
for (size_t c = 0; c < c0; ++c)
{
x += colWidths[c] + colGap;
if (c < cols - 1)
x += spaceBetweenX + spaceAroundX;
}
CCoord h = 0.0;
for (size_t r = r0; r < r1; ++r)
h += rowHeights[r];
if (r1 > r0)
{
auto count = r1 - r0 - 1;
h += spaceBetweenY * count + (rowGap + spaceAroundY) * count;
}
CCoord w = 0.0;
for (size_t c = c0; c < c1; ++c)
w += colWidths[c];
if (c1 > c0)
{
auto count = c1 - c0 - 1;
w += spaceBetweenX * count + (colGap + spaceAroundX) * count;
}
return CRect (x, y, x + w, y + h);
};
// Helper to place a single child for the given area with justify/align items handling
auto placeChildForArea = [&] (CView* child, size_t r0, size_t c0, size_t r1, size_t c1) {
CRect areaRect = computeCellRect (r0, c0, r1, c1);
CCoord x = areaRect.left;
CCoord y = areaRect.top;
CCoord w = areaRect.getWidth ();
CCoord h = areaRect.getHeight ();
CCoord itemX = x;
CCoord itemY = y;
CCoord itemWidth = w;
CCoord itemHeight = h;
// Determine intrinsic size when not stretching
CCoord intrinsicW = w;
CCoord intrinsicH = h;
CRect vs = child->getViewSize ();
intrinsicW = std::max<CCoord> (0.0, std::min (vs.getWidth (), w));
intrinsicH = std::max<CCoord> (0.0, std::min (vs.getHeight (), h));
// Horizontal sizing and positioning
switch (gridProps.justifyItems)
{
case GridLayoutProperties::JustifyItems::Stretch:
{
itemWidth = w;
itemX = x;
break;
}
case GridLayoutProperties::JustifyItems::Center:
{
itemWidth = intrinsicW;
itemX = x + (w - itemWidth) / 2.0;
break;
}
case GridLayoutProperties::JustifyItems::End:
{
itemWidth = intrinsicW;
itemX = x + (w - itemWidth);
break;
}
case GridLayoutProperties::JustifyItems::Start:
default:
{
itemWidth = intrinsicW;
itemX = x;
break;
}
}
// Vertical sizing and positioning
switch (gridProps.alignItems)
{
case GridLayoutProperties::AlignItems::Stretch:
{
itemHeight = h;
itemY = y;
break;
}
case GridLayoutProperties::AlignItems::Center:
{
itemHeight = intrinsicH;
itemY = y + (h - itemHeight) / 2.0;
break;
}
case GridLayoutProperties::AlignItems::End:
{
itemHeight = intrinsicH;
itemY = y + (h - itemHeight);
break;
}
case GridLayoutProperties::AlignItems::Start:
default:
{
itemHeight = intrinsicH;
itemY = y;
break;
}
}
CRect childRect (itemX, itemY, itemX + itemWidth, itemY + itemHeight);
std::optional<ViewLayout> childLayout;
if (auto childViewContainer = child->asViewContainer ())
childLayout = {childViewContainer->calculateViewLayout (childRect)};
layout.emplace_back (childRect, childLayout);
};
// Mark occupied cells for the areas that correspond to existing children
const size_t numAreaChildren = std::min (children.size (), gridProps.gridAreas.size ());
for (size_t childIdx = 0; childIdx < numAreaChildren; ++childIdx)
{
const auto& area = gridProps.gridAreas[childIdx];
size_t r0 = std::min (area.row, rows - 1);
size_t c0 = std::min (area.column, cols - 1);
size_t r1 = std::min (r0 + area.rowSpan, rows);
size_t c1 = std::min (c0 + area.colSpan, cols);
for (size_t r = r0; r < r1; ++r)
{
for (size_t c = c0; c < c1; ++c)
{
occupied[idx2D (r, c)] = true;
}
}
}
// Now place children in order: explicit areas first, then auto-place remaining
for (size_t childIdx = 0; childIdx < children.size (); ++childIdx, ++child)
{
if (childIdx < gridProps.gridAreas.size ())
{
const auto& area = gridProps.gridAreas[childIdx];
size_t r0 = std::min (area.row, rows - 1);
size_t c0 = std::min (area.column, cols - 1);
size_t r1 = std::min (r0 + area.rowSpan, rows);
size_t c1 = std::min (c0 + area.colSpan, cols);
placeChildForArea (*child, r0, c0, r1, c1);
}
else
{
// Auto-place into the next free 1x1 cell (row-major)
bool placed = false;
for (size_t r = 0; r < rows && !placed; ++r)
{
for (size_t c = 0; c < cols && !placed; ++c)
{
if (!occupied[idx2D (r, c)])
{
occupied[idx2D (r, c)] = true;
placeChildForArea (*child, r, c, r + 1, c + 1);
placed = true;
}
}
}
if (!placed)
{
break;
}
}
}
}
else
{
// Default: one cell per child
CCoord y = newSize.top + offsetY;
for (size_t r = 0; r < rows; ++r)
{
CCoord x = newSize.left + offsetX;
for (size_t c = 0; c < cols; ++c)
{
if (child == children.end ())
break;
CCoord cellWidth = colWidths[c];
CCoord cellHeight = rowHeights[r];
// Use autoColumns/autoRows if provided
CCoord itemWidth = autoColumns.size () > c ? autoColumns[c] : cellWidth;
CCoord itemHeight = autoRows.size () > r ? autoRows[r] : cellHeight;
CCoord itemX = x;
CCoord itemY = y;
if (gridProps.justifyItems == GridLayoutProperties::JustifyItems::Start)
itemX = x;
else if (gridProps.justifyItems == GridLayoutProperties::JustifyItems::Center)
itemX = x + (cellWidth - itemWidth) / 2.0;
else if (gridProps.justifyItems == GridLayoutProperties::JustifyItems::End)
itemX = x + (cellWidth - itemWidth);
else if (gridProps.justifyItems == GridLayoutProperties::JustifyItems::Stretch)
{
itemX = x;
itemWidth = cellWidth;
}
if (gridProps.alignItems == GridLayoutProperties::AlignItems::Start)
itemY = y;
else if (gridProps.alignItems == GridLayoutProperties::AlignItems::Center)
itemY = y + (cellHeight - itemHeight) / 2.0;
else if (gridProps.alignItems == GridLayoutProperties::AlignItems::End)
itemY = y + cellHeight - itemHeight;
else if (gridProps.alignItems == GridLayoutProperties::AlignItems::Stretch)
{
itemY = y;
itemHeight = cellHeight;
}
CRect childRect (itemX, itemY, itemX + itemWidth, itemY + itemHeight);
std::optional<ViewLayout> childLayout;
if (auto childViewContainer = (*child)->asViewContainer ())
childLayout = {childViewContainer->calculateViewLayout (childRect)};
layout.emplace_back (childRect, childLayout);
x += cellWidth + colGap;
if (spaceBetweenX > 0.0 && c < cols - 1)
x += spaceBetweenX;
else if (spaceAroundX > 0.0)
x += spaceAroundX;
++child;
}
y += rowHeights[r] + rowGap;
// Add extra space for SpaceBetween/SpaceAround
if (gridProps.alignContent == GridLayoutProperties::AlignContent::SpaceBetween &&
r < rows - 1)
y += spaceBetweenY;
else if (gridProps.alignContent == GridLayoutProperties::AlignContent::SpaceAround &&
r < rows - 1)
y += spaceAroundY;
}
}
return {{_newSize, std::move (layout)}};
}
//------------------------------------------------------------------------
bool GridLayouter::applyLayout (CViewContainer& container, const Children& children,
const ViewLayout& layout)
{
if (gridProps.rows == 0 || gridProps.columns == 0)
return false;
const auto layoutData = std::any_cast<LayoutData> (&layout.data);
if (!layoutData || layoutData->size () > children.size ())
return false;
size_t childIdx = 0;
for (auto& child : children)
{
if (childIdx >= layoutData->size ())
break;
auto rect = (*layoutData)[childIdx].first;
auto& childLayout = (*layoutData)[childIdx].second;
if (auto childContainer = child->asViewContainer (); childContainer && childLayout)
{
childContainer->applyViewLayout (*childLayout);
}
else
{
child->setViewSize (rect, true);
child->setMouseableArea (rect);
}
++childIdx;
}
container.setViewSize (layout.size);
container.setMouseableArea (layout.size);
return true;
}
//------------------------------------------------------------------------
} // namespace VSTGUI