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

466 lines
16 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Widgets/Layout/SWrapBox.h"
#include "Layout/LayoutUtils.h"
SLATE_IMPLEMENT_WIDGET(SWrapBox)
void SWrapBox::PrivateRegisterAttributes(FSlateAttributeInitializer& AttributeInitializer)
{
FSlateWidgetSlotAttributeInitializer SlotInitializer = SLATE_ADD_PANELCHILDREN_DEFINITION(AttributeInitializer, Slots);
FSlot::RegisterAttributes(SlotInitializer);
SLATE_ADD_MEMBER_ATTRIBUTE_DEFINITION(AttributeInitializer, PreferredSize, EInvalidateWidgetReason::Layout);
SLATE_ADD_MEMBER_ATTRIBUTE_DEFINITION(AttributeInitializer, HAlign, EInvalidateWidgetReason::Layout);
}
SWrapBox::SWrapBox()
: PreferredSize(*this, 100.f)
, HAlign(*this, EHorizontalAlignment::HAlign_Left)
, Slots(this, GET_MEMBER_NAME_CHECKED(SWrapBox, Slots))
{
}
SWrapBox::~SWrapBox() = default;
SWrapBox::FScopedWidgetSlotArguments SWrapBox::AddSlot()
{
return FScopedWidgetSlotArguments{ MakeUnique<FSlot>(), Slots, INDEX_NONE };
}
int32 SWrapBox::RemoveSlot( const TSharedRef<SWidget>& SlotWidget )
{
return Slots.Remove(SlotWidget);
}
void SWrapBox::Construct( const FArguments& InArgs )
{
PreferredSize.Assign(*this, InArgs._PreferredSize);
InnerSlotPadding = InArgs._InnerSlotPadding;
bUseAllottedSize = InArgs._UseAllottedSize;
Orientation = InArgs._Orientation;
HAlign.Assign(*this, InArgs._HAlign);
// Build the children from the declaration to the widget
Slots.AddSlots(MoveTemp(const_cast<TArray<FSlot::FSlotArguments>&>(InArgs._Slots)));
SetCanTick(bUseAllottedSize);
}
void SWrapBox::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime )
{
if (bUseAllottedSize)
{
PreferredSize.Set(*this, Orientation == EOrientation::Orient_Vertical ? AllottedGeometry.GetLocalSize().Y : AllottedGeometry.GetLocalSize().X);
}
}
/*
* Simple class for handling the somewhat complex state tracking for wrapping based on otherwise simple rules.
* Singular static method in public interface simplifies use to a single function call by encapsulating and hiding
* awkward inline helper object instantiation and method calls from user code.
*/
class SWrapBox::FChildArranger
{
public:
struct FArrangementData
{
FVector2D SlotOffset;
FVector2D SlotSize;
float WidthOfCurrentLine;
int ChildIndexRelativeToLine;
};
typedef TFunctionRef<void(const FSlot& Slot, const FArrangementData& ArrangementData)> FOnSlotArranged;
static void Arrange(const SWrapBox& WrapBox, const FOnSlotArranged& OnSlotArranged);
private:
FChildArranger(const SWrapBox& WrapBox, const FOnSlotArranged& OnSlotArranged);
void Arrange();
void FinalizeLine(int32 IndexOfLastChildInCurrentLine);
const SWrapBox& WrapBox;
const FOnSlotArranged& OnSlotArranged;
FVector2D Offset;
float MaximumSizeInCurrentLine;
float WidthOfCurrentLine;
int32 IndexOfFirstChildInCurrentLine;
TMap<int32, FArrangementData> OngoingArrangementDataMap;
};
SWrapBox::FChildArranger::FChildArranger(const SWrapBox& InWrapBox, const FOnSlotArranged& InOnSlotArranged)
: WrapBox(InWrapBox)
, OnSlotArranged(InOnSlotArranged)
, Offset(FVector2D::ZeroVector)
, MaximumSizeInCurrentLine(0.0f)
, WidthOfCurrentLine(0.0f)
, IndexOfFirstChildInCurrentLine(INDEX_NONE)
{
OngoingArrangementDataMap.Reserve(WrapBox.Slots.Num());
}
void SWrapBox::FChildArranger::Arrange()
{
int32 ChildIndex;
for (ChildIndex = 0; ChildIndex < WrapBox.Slots.Num(); ++ChildIndex)
{
const FSlot& Slot = WrapBox.Slots[ChildIndex];
const TSharedRef<SWidget>& Widget = Slot.GetWidget();
/*
* Simple utility lambda for determining if the current child index is the first child of the current line.
*/
const auto& IsFirstChildInCurrentLine = [&]() -> bool
{
return ChildIndex == IndexOfFirstChildInCurrentLine;
};
// Skip collapsed widgets.
if (Widget->GetVisibility() == EVisibility::Collapsed)
{
continue;
}
FArrangementData& ArrangementData = OngoingArrangementDataMap.Add(ChildIndex, FArrangementData());
// If there is no first child in the current line, we must be the first child.
if (IndexOfFirstChildInCurrentLine == INDEX_NONE)
{
IndexOfFirstChildInCurrentLine = ChildIndex;
}
/*
* Simple utility lambda for beginning a new line with the current child, updating it's offset for the new line.
*/
const auto& BeginNewLine = [&]()
{
FinalizeLine(ChildIndex - 1);
// Starting a new line.
IndexOfFirstChildInCurrentLine = ChildIndex;
// Update child's offset to new X and Y values for new line.
ArrangementData.SlotOffset.X = Offset.X;
ArrangementData.SlotOffset.Y = Offset.Y;
};
// Rule: If this child is not the first child in the line, "inner slot padding" needs to be injected left or top of it, dependently of the orientation.
if (!IsFirstChildInCurrentLine())
{
if (WrapBox.Orientation == EOrientation::Orient_Horizontal)
{
Offset.X += WrapBox.InnerSlotPadding.X;
WidthOfCurrentLine += WrapBox.InnerSlotPadding.X;
}
else
{
Offset.Y += WrapBox.InnerSlotPadding.Y;
}
}
const FVector2D DesiredSizeOfSlot = Slot.GetPadding().GetDesiredSize() + Widget->GetDesiredSize();
// Populate arrangement data with default size and offset at right end of current line.
ArrangementData.SlotOffset.X = Offset.X;
ArrangementData.SlotOffset.Y = Offset.Y;
ArrangementData.SlotSize.X = DesiredSizeOfSlot.X;
ArrangementData.SlotSize.Y = DesiredSizeOfSlot.Y;
if (WrapBox.Orientation == EOrientation::Orient_Vertical)
{
const float BottomBoundOfChild = ArrangementData.SlotOffset.Y + ArrangementData.SlotSize.Y;
if (Slot.GetForceNewLine())
{
// Begin a new line if the current one isn't empty, because the slot demanded a new line.
if (!IsFirstChildInCurrentLine())
{
BeginNewLine();
}
}
// Rule: If required due to a wrapping height under specified threshold, start a new line and allocate all of it to this child.
else if (Slot.GetFillLineWhenSizeLessThan().IsSet() && WrapBox.PreferredSize.Get() < Slot.GetFillLineWhenSizeLessThan().GetValue())
{
// Begin a new line if the current one isn't empty, because we demand a whole line to ourselves.
if (!IsFirstChildInCurrentLine())
{
BeginNewLine();
}
// Fill height of rest of wrap box.
ArrangementData.SlotSize.Y = WrapBox.PreferredSize.Get() - Offset.Y;
}
// Rule: If the end of a child would go beyond the width to wrap at, it should move to a new line.
else if (BottomBoundOfChild > WrapBox.PreferredSize.Get())
{
// Begin a new line if the current one isn't empty, because we demand a new line.
if (!IsFirstChildInCurrentLine())
{
BeginNewLine();
}
}
// Update current line maximum size.
MaximumSizeInCurrentLine = FMath::Max(MaximumSizeInCurrentLine, ArrangementData.SlotSize.X);
// Update offset to bottom bound of child.
Offset.Y = ArrangementData.SlotOffset.Y + ArrangementData.SlotSize.Y;
}
else
{
const float RightBoundOfChild = ArrangementData.SlotOffset.X + ArrangementData.SlotSize.X;
if (Slot.GetForceNewLine())
{
// Begin a new line if the current one isn't empty, because the slot demanded a new line.
if (!IsFirstChildInCurrentLine())
{
BeginNewLine();
}
}
// Rule: If required due to a wrapping width under specified threshold, start a new line and allocate all of it to this child.
else if (Slot.GetFillLineWhenSizeLessThan().IsSet() && WrapBox.PreferredSize.Get() < Slot.GetFillLineWhenSizeLessThan().GetValue())
{
// Begin a new line if the current one isn't empty, because we demand a whole line to ourselves.
if (!IsFirstChildInCurrentLine())
{
BeginNewLine();
}
// Fill width of rest of wrap box.
ArrangementData.SlotSize.X = WrapBox.PreferredSize.Get() - Offset.X;
}
// Rule: If the end of a child would go beyond the width to wrap at, it should move to a new line.
else if (RightBoundOfChild > WrapBox.PreferredSize.Get())
{
// Begin a new line if the current one isn't empty, because we demand a new line.
if (!IsFirstChildInCurrentLine())
{
BeginNewLine();
}
}
// Update current line maximum size.
MaximumSizeInCurrentLine = FMath::Max(MaximumSizeInCurrentLine, ArrangementData.SlotSize.Y);
WidthOfCurrentLine += ArrangementData.SlotSize.X;
// Update offset to right bound of child.
Offset.X = ArrangementData.SlotOffset.X + ArrangementData.SlotSize.X;
}
}
// Attempt to finalize the final line if there are any children in it.
if (IndexOfFirstChildInCurrentLine != INDEX_NONE)
{
FinalizeLine(ChildIndex - 1);
}
}
void SWrapBox::FChildArranger::FinalizeLine(int32 IndexOfLastChildInCurrentLine)
{
// Iterate backwards through children in this line. Iterate backwards because the last uncollapsed child may wish to fill the remaining empty space of the line.
for (; IndexOfLastChildInCurrentLine >= IndexOfFirstChildInCurrentLine; --IndexOfLastChildInCurrentLine)
{
if (WrapBox.Slots[IndexOfLastChildInCurrentLine].GetWidget()->GetVisibility() != EVisibility::Collapsed)
{
break;
}
}
// If we're doing this on a line where we fill the empty space,
// the widget of the current line will just be the whole line.
if (WrapBox.Slots.IsValidIndex(IndexOfLastChildInCurrentLine) && WrapBox.Slots[IndexOfLastChildInCurrentLine].GetFillEmptySpace())
{
WidthOfCurrentLine = WrapBox.PreferredSize.Get();
}
int32 RelativeChildIndex = 0;
// Now iterate forward so tab navigation works properly
for (int32 ChildIndex = IndexOfFirstChildInCurrentLine; ChildIndex <= IndexOfLastChildInCurrentLine; ++ChildIndex)
{
const FSlot& Slot = WrapBox.Slots[ChildIndex];
const TSharedRef<SWidget>& Widget = Slot.GetWidget();
// Skip collapsed widgets.
if (Widget->GetVisibility() == EVisibility::Collapsed)
{
continue;
}
FArrangementData& ArrangementData = OngoingArrangementDataMap[ChildIndex];
ArrangementData.ChildIndexRelativeToLine = RelativeChildIndex;
ArrangementData.WidthOfCurrentLine = WidthOfCurrentLine;
RelativeChildIndex++;
// Rule: The last uncollapsed child in a line may request to fill the remaining empty space in the line.
if (ChildIndex == IndexOfLastChildInCurrentLine && Slot.GetFillEmptySpace())
{
if (WrapBox.Orientation == EOrientation::Orient_Vertical)
{
ArrangementData.SlotSize.Y = WrapBox.PreferredSize.Get() - ArrangementData.SlotOffset.Y;
}
else
{
ArrangementData.SlotSize.X = WrapBox.PreferredSize.Get() - ArrangementData.SlotOffset.X;
}
}
// All slots on this line should now match to the tallest element's height, which they can then use to do their alignment in OnSlotArranged below (eg. center within that)
// If we left their height as is, then their slots would just be whatever their child's desired height was, and so a vertical alignment of "center" would actually
// leave the widget at the top of the line, since you couldn't calculate how much to offset by to actually reach the center of the "container"
if (WrapBox.Orientation == EOrientation::Orient_Vertical)
{
ArrangementData.SlotSize.X = MaximumSizeInCurrentLine;
}
else
{
ArrangementData.SlotSize.Y = MaximumSizeInCurrentLine;
}
// Flip horizontally if flow direction is set to RightToLeft
if (GSlateFlowDirection == EFlowDirection::RightToLeft)
{
ArrangementData.SlotOffset.X = WrapBox.PreferredSize.Get() - ArrangementData.SlotOffset.X - ArrangementData.SlotSize.X;
}
OnSlotArranged(Slot, ArrangementData);
}
if (WrapBox.Orientation == EOrientation::Orient_Vertical)
{
// Set initial state for new vertical line.
Offset.Y = 0.0f;
// Since this is the initial state for a new vertical line, this only happens after the first line, so the inner slot horizontal padding should always be added.
Offset.X += MaximumSizeInCurrentLine + WrapBox.InnerSlotPadding.X;
}
else
{
// Set initial state for horizontal new line.
Offset.X = 0.0f;
// Since this is the initial state for a new horizontal line, this only happens after the first line, so the inner slot vertical padding should always be added.
Offset.Y += MaximumSizeInCurrentLine + WrapBox.InnerSlotPadding.Y;
}
WidthOfCurrentLine = 0;
MaximumSizeInCurrentLine = 0.0f;
IndexOfFirstChildInCurrentLine = INDEX_NONE;
}
void SWrapBox::FChildArranger::Arrange(const SWrapBox& WrapBox, const FOnSlotArranged& OnSlotArranged)
{
FChildArranger(WrapBox, OnSlotArranged).Arrange();
}
void SWrapBox::OnArrangeChildren(const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren) const
{
const EHorizontalAlignment HAlignment = HAlign.Get();
float LeftStop = 0;
FChildArranger::Arrange(*this, [&](const FSlot& Slot, const FChildArranger::FArrangementData& ArrangementData)
{
FVector2D AllottedSlotSize = ArrangementData.SlotSize;
float SlotSizeAdjustment = 0;
if (Orientation == EOrientation::Orient_Horizontal)
{
// Reset leftstop when the line changes.
if (ArrangementData.ChildIndexRelativeToLine == 0)
{
LeftStop = 0;
}
switch (HAlignment)
{
case HAlign_Right:
LeftStop = FMath::FloorToFloat(AllottedGeometry.GetLocalSize().X - ArrangementData.WidthOfCurrentLine);
break;
case HAlign_Center:
LeftStop = FMath::FloorToFloat((AllottedGeometry.GetLocalSize().X - ArrangementData.WidthOfCurrentLine) / 2.0f);
break;
case HAlign_Fill:
const float NewSlotSize = AllottedSlotSize.X / ArrangementData.WidthOfCurrentLine * AllottedGeometry.GetLocalSize().X;
SlotSizeAdjustment = NewSlotSize - AllottedSlotSize.X;
AllottedSlotSize.X = NewSlotSize;
break;
}
}
// Calculate offset and size in slot using alignment.
const AlignmentArrangeResult XResult = AlignChild<Orient_Horizontal>(AllottedSlotSize.X, Slot, Slot.GetPadding());
const AlignmentArrangeResult YResult = AlignChild<Orient_Vertical>(AllottedSlotSize.Y, Slot, Slot.GetPadding());
// Note: Alignment offset is relative to slot offset.
const FVector2D PostAlignmentOffset = ArrangementData.SlotOffset + FVector2D(XResult.Offset, YResult.Offset) + FVector2D(LeftStop, 0);
const FVector2D PostAlignmentSize = FVector2D(XResult.Size, YResult.Size);
ArrangedChildren.AddWidget(AllottedGeometry.MakeChild(Slot.GetWidget(), PostAlignmentOffset, PostAlignmentSize));
LeftStop += SlotSizeAdjustment;
});
}
void SWrapBox::ClearChildren()
{
Slots.Empty();
}
FVector2D SWrapBox::ComputeDesiredSize( float ) const
{
FVector2D MyDesiredSize = FVector2D::ZeroVector;
FChildArranger::Arrange(*this, [&](const FSlot& Slot, const FChildArranger::FArrangementData& ArrangementData)
{
// Increase desired size to the maximum X and Y positions of any child widget.
MyDesiredSize.X = FMath::Max(MyDesiredSize.X, ArrangementData.SlotOffset.X + ArrangementData.SlotSize.X);
MyDesiredSize.Y = FMath::Max(MyDesiredSize.Y, ArrangementData.SlotOffset.Y + ArrangementData.SlotSize.Y);
});
return MyDesiredSize;
}
FChildren* SWrapBox::GetChildren()
{
return &Slots;
}
void SWrapBox::SetInnerSlotPadding(FVector2D InInnerSlotPadding)
{
if (InnerSlotPadding != InInnerSlotPadding)
{
InnerSlotPadding = InInnerSlotPadding;
Invalidate(EInvalidateWidgetReason::Layout);
}
}
void SWrapBox::SetWrapSize(TAttribute<float> InWrapSize)
{
PreferredSize.Assign(*this, MoveTemp(InWrapSize));
}
void SWrapBox::SetUseAllottedSize(bool bInUseAllottedSize)
{
if (bUseAllottedSize != bInUseAllottedSize)
{
bUseAllottedSize = bInUseAllottedSize;
SetCanTick(bUseAllottedSize);
Invalidate(EInvalidateWidgetReason::Layout);
}
}
void SWrapBox::SetOrientation(EOrientation InOrientation)
{
if (Orientation != InOrientation)
{
Orientation = InOrientation;
Invalidate(EInvalidateWidgetReason::Layout);
}
}
void SWrapBox::SetHorizontalAlignment(TAttribute<EHorizontalAlignment> InHAlignment)
{
HAlign.Assign(*this, MoveTemp(InHAlignment));
}