Files
UnrealEngine/Engine/Source/Runtime/Slate/Private/Widgets/Layout/SResponsiveGridPanel.cpp
2025-05-18 13:04:45 +08:00

481 lines
15 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Widgets/Layout/SResponsiveGridPanel.h"
#include "Types/PaintArgs.h"
#include "Layout/ArrangedChildren.h"
#include "Layout/LayoutUtils.h"
SLATE_IMPLEMENT_WIDGET(SResponsiveGridPanel)
void SResponsiveGridPanel::PrivateRegisterAttributes(FSlateAttributeInitializer& AttributeInitializer)
{
FSlateWidgetSlotAttributeInitializer Initializer = SLATE_ADD_PANELCHILDREN_DEFINITION(AttributeInitializer, Slots);
FSlot::RegisterAttributes(Initializer);
}
/**
* !!!!!!!!!!!!!!!!! EXPERIMENTAL !!!!!!!!!!!!!!!!!
* The SResponsiveGridPanel is still in development and the API may change drastically in the future
* or maybe removed entirely.
*/
SResponsiveGridPanel::SResponsiveGridPanel()
: Slots(this, GET_MEMBER_NAME_CHECKED(SResponsiveGridPanel, Slots))
{}
SResponsiveGridPanel::~SResponsiveGridPanel() = default;
SResponsiveGridPanel::FScopedWidgetSlotArguments SResponsiveGridPanel::AddSlot(int32 Row)
{
TWeakPtr<SResponsiveGridPanel> WeakPanel = SharedThis(this);
TUniquePtr<FSlot> NewSlot = MakeUnique<FSlot>(Row);
NewSlot->Panel = WeakPanel;
int32 InsertLocation = FindInsertSlotLocation(NewSlot.Get());
return FScopedWidgetSlotArguments{ MoveTemp(NewSlot), this->Slots, InsertLocation};
}
bool SResponsiveGridPanel::RemoveSlot(const TSharedRef<SWidget>& SlotWidget)
{
return Slots.Remove(SlotWidget) != INDEX_NONE;
}
void SResponsiveGridPanel::ClearChildren()
{
Slots.Empty();
}
void SResponsiveGridPanel::Construct(const FArguments& InArgs, int32 InTotalColumns)
{
TotalColumns = InTotalColumns;
ColumnGutter = InArgs._ColumnGutter;
RowGutter = InArgs._RowGutter;
PreviousWidth = 0;
RowFillCoefficients = InArgs.RowFillCoefficients;
// Populate the slots such that they are sorted by Layer (order preserved within layers)
TWeakPtr<SResponsiveGridPanel> WeakPanel = SharedThis(this);
Slots.Reserve(InArgs._Slots.Num());
for (int32 SlotIndex = 0; SlotIndex < InArgs._Slots.Num(); ++SlotIndex)
{
int32 SlotLocation = FindInsertSlotLocation(InArgs._Slots[SlotIndex].GetSlot());
if (SlotLocation == INDEX_NONE)
{
SlotLocation = Slots.AddSlot(MoveTemp(const_cast<FSlot::FSlotArguments&>(InArgs._Slots[SlotIndex])));
}
else
{
Slots.InsertSlot(MoveTemp(const_cast<FSlot::FSlotArguments&>(InArgs._Slots[SlotIndex])), SlotLocation);
}
FSlot& NewSlot = Slots[SlotLocation];
NewSlot.Panel = WeakPanel;
}
}
int32 SResponsiveGridPanel::OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const
{
FArrangedChildren ArrangedChildren(EVisibility::All);
this->ArrangeChildren(AllottedGeometry, ArrangedChildren);
// Because we paint multiple children, we must track the maximum layer id that they produced in case one of our parents
// wants to an overlay for all of its contents.
int32 MaxLayerId = LayerId;
const FPaintArgs NewArgs = Args.WithNewParent(this);
// We need to iterate over slots, because slots know the GridLayers. This isn't available in the arranged children.
// Some slots do not show up (they are hidden/collapsed). We need a 2nd index to skip over them.
for (int32 ChildIndex = 0; ChildIndex < ArrangedChildren.Num(); ++ChildIndex)
{
FArrangedWidget& CurWidget = ArrangedChildren[ChildIndex];
if (CurWidget.Widget->GetVisibility().IsVisible())
{
const int32 CurWidgetsMaxLayerId = CurWidget.Widget->Paint(
NewArgs,
CurWidget.Geometry,
MyCullingRect,
OutDrawElements,
LayerId,
InWidgetStyle,
ShouldBeEnabled( bParentEnabled )
);
MaxLayerId = FMath::Max( MaxLayerId, CurWidgetsMaxLayerId );
}
}
#ifdef LAYOUT_DEBUG
LayerId = LayoutDebugPaint( AllottedGeometry, MyCullingRect, OutDrawElements, LayerId );
#endif
return MaxLayerId;
}
void SResponsiveGridPanel::OnArrangeChildren( const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren ) const
{
// Don't attempt to array anything if we don't have any slots allocated.
if ( Slots.Num() == 0 )
{
return;
}
// PREPARE PHASE
// Prepare some data for arranging children.
// FinalColumns will be populated with column sizes that include the stretched column sizes.
// Then we will build partial sums so that we can easily handle column spans.
const FVector2D LocalSize = AllottedGeometry.GetLocalSize();
FVector2D FlexSpace = LocalSize;
PreviousWidth = LocalSize.X;
const float FullColumnGutter = (ColumnGutter * 2);
const float FullRowGutter = (RowGutter * 2);
TArray<float> Rows;
TArray<float> RowToSlot;
TArray<float> Columns;
ComputeDesiredCellSizes(LocalSize.X, Columns, Rows, RowToSlot);
check(Rows.Num() == RowToSlot.Num());
TArray<float> FinalColumns;
if (Columns.Num() > 0)
{
FinalColumns.AddUninitialized(Columns.Num());
FinalColumns[FinalColumns.Num() - 1] = 0.0f;
// We need an extra cell at the end for easily figuring out the size across any number of cells
FinalColumns.AddZeroed((TotalColumns + 1) - Columns.Num());
}
// You can't remove the gutter space from the flexspace for the columns because you don't know how
// many columns there actually are in a single row at this point
float ColumnCoeffTotal = TotalColumns;
for (int32 ColIndex = 0; ColIndex < Columns.Num(); ++ColIndex)
{
// Figure out how big each column needs to be
FinalColumns[ColIndex] = (1.0f / ColumnCoeffTotal * FlexSpace.X);
}
// FinalColumns will be populated with column sizes that include the stretched column sizes.
// Then we will build partial sums so that we can easily handle column spans.
float RowCoeffTotal = 0.0f;
TArray<float> FinalRows;
if (Rows.Num() > 0)
{
FinalRows.AddUninitialized(Rows.Num());
FinalRows[FinalRows.Num() - 1] = 0.0f;
// We need an extra cell at the end for easily figuring out the size across any number of cells
FinalRows.AddZeroed(1);
}
FlexSpace.Y -= (RowGutter * 2) * (Slots[Slots.Num() - 1].RowParam);
const int32 RowFillCoeffsLength = RowFillCoefficients.Num();
for (int32 RowIndex = 0; RowIndex < Rows.Num(); ++RowIndex)
{
// Compute the total space available for stretchy rows.
if (RowToSlot[RowIndex] >= RowFillCoeffsLength || RowFillCoefficients[RowToSlot[RowIndex]] == 0)
{
FlexSpace.Y -= Rows[RowIndex];
}
else //(RowIndex < RowFillCoeffsLength)
{
// Compute the denominator for dividing up the stretchy row space
RowCoeffTotal += RowFillCoefficients[RowToSlot[RowIndex]];
}
}
for (int32 RowIndex = 0; RowIndex < Rows.Num(); ++RowIndex)
{
// Compute how big each row needs to be
FinalRows[RowIndex] = (RowToSlot[RowIndex] < RowFillCoeffsLength && RowFillCoefficients[RowToSlot[RowIndex]] != 0)
? (RowFillCoefficients[RowToSlot[RowIndex]] / RowCoeffTotal * FlexSpace.Y)
: Rows[RowIndex];
}
// Build up partial sums for row and column sizes so that we can handle column and row spans conveniently.
ComputePartialSums(FinalColumns);
ComputePartialSums(FinalRows);
// ARRANGE PHASE
int32 ColumnsSoFar = 0;
int32 CurrentRow = INDEX_NONE;
int32 LastRowParam = INDEX_NONE;
float RowGuttersSoFar = 0;
for (int32 SlotIndex = 0; SlotIndex < Slots.Num(); ++SlotIndex)
{
const FSlot& CurSlot = Slots[SlotIndex];
const EVisibility ChildVisibility = CurSlot.GetWidget()->GetVisibility();
if (ChildVisibility != EVisibility::Collapsed)
{
// Find the appropriate column layout for the slot
FSlot::FColumnLayout ColumnLayout;
ColumnLayout.Span = TotalColumns;
ColumnLayout.Offset = 0;
for (int32 Index = CurSlot.ColumnLayouts.Num() - 1; Index >= 0; Index--)
{
if (CurSlot.ColumnLayouts[Index].LayoutSize < LocalSize.X)
{
ColumnLayout = CurSlot.ColumnLayouts[Index];
break;
}
}
if (ColumnLayout.Span == 0)
{
continue;
}
if (CurSlot.RowParam != LastRowParam)
{
ColumnsSoFar = 0;
LastRowParam = CurSlot.RowParam;
++CurrentRow;
if (LastRowParam > 0)
{
RowGuttersSoFar += FullRowGutter;
}
}
// Figure out the position of this cell.
int32 StartColumn = ColumnsSoFar + ColumnLayout.Offset;
int32 EndColumn = StartColumn + ColumnLayout.Span;
ColumnsSoFar = FMath::Max(EndColumn, ColumnsSoFar);
if (ColumnsSoFar > TotalColumns)
{
StartColumn = 0;
EndColumn = StartColumn + ColumnLayout.Span;
ColumnsSoFar = EndColumn - StartColumn;
++CurrentRow;
}
FVector2D ThisCellOffset(FinalColumns[StartColumn], FinalRows[CurrentRow]);
// Account for the gutters applied to columns before the starting column of this cell
if (StartColumn > 0)
{
ThisCellOffset.X += FullColumnGutter;
}
// Figure out the size of this slot; takes row span into account.
// We use the properties of partial sums arrays to achieve this.
FVector2D CellSize(
FinalColumns[EndColumn] - ThisCellOffset.X,
FinalRows[CurrentRow + 1] - ThisCellOffset.Y);
// Do the standard arrangement of elements within a slot
// Takes care of alignment and padding.
FMargin SlotPadding(CurSlot.GetPadding());
AlignmentArrangeResult XAxisResult = AlignChild<Orient_Horizontal>(CellSize.X, CurSlot, SlotPadding);
AlignmentArrangeResult YAxisResult = AlignChild<Orient_Vertical>(CellSize.Y, CurSlot, SlotPadding);
// The row gutters have already been accounted for in the cell size by removing them from the flexspace,
// so we just need to offset the cells appropriately
ThisCellOffset.Y += RowGuttersSoFar;
// Output the result
ArrangedChildren.AddWidget(ChildVisibility, AllottedGeometry.MakeChild(
CurSlot.GetWidget(),
ThisCellOffset + FVector2D(XAxisResult.Offset, YAxisResult.Offset),
FVector2D(XAxisResult.Size, YAxisResult.Size)
));
}
}
}
void SResponsiveGridPanel::CacheDesiredSize(float LayoutScaleMultiplier)
{
// The desired size of the grid is the sum of the desires sizes for every row and column.
TArray<float> Columns;
TArray<float> Rows;
TArray<float> RowToSlot;
ComputeDesiredCellSizes(PreviousWidth, Columns, Rows, RowToSlot);
TotalDesiredSizes = FVector2D::ZeroVector;
if ( Slots.Num() > 0 )
{
for ( int ColId = 0; ColId < Columns.Num(); ++ColId )
{
TotalDesiredSizes.X += Columns[ColId];
}
TotalDesiredSizes.X += ( ColumnGutter * 2 ) * ( TotalColumns - 1 );
for ( int RowId = 0; RowId < Rows.Num(); ++RowId )
{
TotalDesiredSizes.Y += Rows[RowId];
}
TotalDesiredSizes.Y += ( RowGutter * 2 ) * ( Slots[Slots.Num() - 1].RowParam );
}
SPanel::CacheDesiredSize(LayoutScaleMultiplier);
}
FVector2D SResponsiveGridPanel::ComputeDesiredSize( float ) const
{
return TotalDesiredSizes;
}
void SResponsiveGridPanel::ComputeDesiredCellSizes(float AvailableWidth, TArray<float>& OutColumns, TArray<float>& OutRows, TArray<float>& OutRowToSlot) const
{
FMemory::Memzero(OutColumns.GetData(), OutColumns.Num() * sizeof(float));
FMemory::Memzero(OutRows.GetData(), OutRows.Num() * sizeof(float));
int32 ColumnsSoFar = 0;
int32 CurrentRow = INDEX_NONE;
int32 LastRowParam = INDEX_NONE;
for (int32 SlotIndex = 0; SlotIndex < Slots.Num(); ++SlotIndex)
{
const FSlot& CurSlot = Slots[SlotIndex];
if (CurSlot.GetWidget()->GetVisibility() != EVisibility::Collapsed)
{
// Find the appropriate column layout for the slot
FSlot::FColumnLayout ColumnLayout;
ColumnLayout.Span = TotalColumns;
ColumnLayout.Offset = 0;
for (int32 Index = CurSlot.ColumnLayouts.Num() - 1; Index >= 0; Index--)
{
if (CurSlot.ColumnLayouts[Index].LayoutSize < AvailableWidth)
{
ColumnLayout = CurSlot.ColumnLayouts[Index];
break;
}
}
if (ColumnLayout.Span == 0)
{
continue;
}
if (CurSlot.RowParam != LastRowParam)
{
ColumnsSoFar = 0;
LastRowParam = CurSlot.RowParam;
++CurrentRow;
OutRowToSlot.AddZeroed((CurrentRow + 1) - OutRowToSlot.Num());
OutRowToSlot[CurrentRow] = CurSlot.RowParam;
}
// The slots want to be as big as its content along with the required padding.
const FVector2D SlotDesiredSize = CurSlot.GetWidget()->GetDesiredSize() + CurSlot.GetPadding().GetDesiredSize();
// If the slot has a (colspan,rowspan) of (1,1) it will only affect that cell.
// For larger spans, the slots size will be evenly distributed across all the affected cells.
const FVector2D SizeContribution(SlotDesiredSize.X / ColumnLayout.Span, SlotDesiredSize.Y);
int32 StartColumnIndex = ColumnsSoFar + ColumnLayout.Offset;
int32 EndColumnIndex = StartColumnIndex + ColumnLayout.Span;
ColumnsSoFar = FMath::Max(EndColumnIndex, ColumnsSoFar);
if (ColumnsSoFar > TotalColumns)
{
StartColumnIndex = 0;
EndColumnIndex = StartColumnIndex + ColumnLayout.Span;
ColumnsSoFar = EndColumnIndex - StartColumnIndex;
++CurrentRow;
OutRowToSlot.AddZeroed((CurrentRow + 1) - OutRowToSlot.Num());
OutRowToSlot[CurrentRow] = CurSlot.RowParam;
}
OutColumns.AddZeroed(FMath::Max(0, ColumnsSoFar - OutColumns.Num()));
OutRows.AddZeroed((CurrentRow + 1) - OutRows.Num());
// Distribute the size contributions over all the columns and rows that this slot spans
DistributeSizeContributions(SizeContribution.X, OutColumns, StartColumnIndex, EndColumnIndex);
DistributeSizeContributions(SizeContribution.Y, OutRows, CurrentRow, CurrentRow + 1);
}
}
}
void SResponsiveGridPanel::DistributeSizeContributions(float SizeContribution, TArray<float>& DistributeOverMe, int32 StartIndex, int32 UpperBound)
{
for (int32 Index = StartIndex; Index < UpperBound; ++Index)
{
// Each column or row only needs to get bigger if its current size does not already accommodate it.
DistributeOverMe[Index] = FMath::Max(SizeContribution, DistributeOverMe[Index]);
}
}
FChildren* SResponsiveGridPanel::GetChildren()
{
return &Slots;
}
void SResponsiveGridPanel::ComputePartialSums( TArray<float>& TurnMeIntoPartialSums )
{
// We assume there is a 0-valued item already at the end of this array.
// We need it so that we can compute the original values
// by doing Array[N] - Array[N-1];
float LastValue = 0;
float SumSoFar = 0;
for (int32 Index = 0; Index < TurnMeIntoPartialSums.Num(); ++Index)
{
LastValue = TurnMeIntoPartialSums[Index];
TurnMeIntoPartialSums[Index] = SumSoFar;
SumSoFar += LastValue;
}
}
void SResponsiveGridPanel::SetRowFill(int32 RowId, float Coefficient)
{
if (RowFillCoefficients.Num() <= RowId)
{
RowFillCoefficients.AddZeroed(RowId - RowFillCoefficients.Num() + 1);
}
RowFillCoefficients[RowId] = Coefficient;
}
int32 SResponsiveGridPanel::FindInsertSlotLocation(SResponsiveGridPanel::FSlot* InSlot) const
{
// Insert the slot in the list such that slots are sorted by LayerOffset.
for (int32 SlotIndex = 0; SlotIndex < Slots.Num(); ++SlotIndex)
{
if (InSlot->RowParam < this->Slots[SlotIndex].RowParam)
{
return SlotIndex;
}
}
return INDEX_NONE;
}
int32 SResponsiveGridPanel::LayoutDebugPaint(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId ) const
{
//float XOffset = 0;
//for (int32 Column=0; Column<Columns.Num(); ++Column)
//{
// float YOffset = 0;
// for (int32 Row=0; Row<Rows.Num(); ++Row)
// {
// FSlateDrawElement::MakeDebugQuad
// (
// OutDrawElements,
// LayerId,
// AllottedGeometry.ToPaintGeometry( FVector2D(XOffset, YOffset), FVector2D( Columns[Column], Rows[Row] ) ),
// MyCullingRect
// );
// YOffset += Rows[Row];
// }
// XOffset += Columns[Column];
//}
return LayerId;
}